smart_proxy_ipam 0.0.18 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ # Module containing helper methods for use by all External IPAM provider implementations
2
+ module Proxy::Ipam::IpamHelper
3
+ include ::Proxy::Validations
4
+
5
+ MAX_IP_RETRIES = 5
6
+
7
+ def provider
8
+ @provider ||=
9
+ begin
10
+ unless client.authenticated?
11
+ halt 500, {error: 'Invalid credentials for External IPAM'}.to_json
12
+ end
13
+ client
14
+ end
15
+ end
16
+
17
+ # Called when next available IP from External IPAM has been cached by another user/host, but
18
+ # not actually persisted in External IPAM yet. This method will increment the IP, up to
19
+ # MAX_IP_RETRIES times, and check if it is available in External IPAM each iteration. It
20
+ # will return the original IP(the 'ip' param) if no new IP's are found after MAX_IP_RETRIES
21
+ # iterations.
22
+ def find_new_ip(ip_cache, subnet_id, ip, mac, cidr, group_name)
23
+ found_ip = nil
24
+ temp_ip = ip
25
+ retry_count = 0
26
+
27
+ loop do
28
+ new_ip = increment_ip(temp_ip)
29
+ ipam_ip = ip_exists?(new_ip, subnet_id, group_name)
30
+
31
+ # If new IP doesn't exist in IPAM and not in the cache
32
+ if !ipam_ip && !ip_cache.ip_exists(new_ip, cidr, group_name)
33
+ found_ip = new_ip.to_s
34
+ ip_cache.add(found_ip, mac, cidr, group_name)
35
+ break
36
+ end
37
+
38
+ temp_ip = new_ip
39
+ retry_count += 1
40
+ break if retry_count >= MAX_IP_RETRIES
41
+ end
42
+
43
+ return ip if found_ip.nil?
44
+
45
+ found_ip
46
+ end
47
+
48
+ # Checks the cache for existing ip, and returns it if it exists. If not exists, it will
49
+ # find a new ip (using find_new_ip), and it is added to the cache.
50
+ def cache_next_ip(ip_cache, ip, mac, cidr, subnet_id, group_name)
51
+ group = group_name.nil? ? '' : group_name
52
+ ip_cache.set_group(group, {}) if ip_cache.get_group(group).nil?
53
+ subnet_hash = ip_cache.get_cidr(group, cidr)
54
+ next_ip = nil
55
+
56
+ if subnet_hash&.key?(mac.to_sym)
57
+ next_ip = ip_cache.get_ip(group, cidr, mac)
58
+ else
59
+ new_ip = ip
60
+ ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
61
+
62
+ if ip_not_in_cache
63
+ next_ip = new_ip.to_s
64
+ ip_cache.add(new_ip, mac, cidr, group)
65
+ else
66
+ next_ip = find_new_ip(ip_cache, subnet_id, new_ip, mac, cidr, group)
67
+ end
68
+
69
+ unless usable_ip(next_ip, cidr)
70
+ return { error: "No free addresses found in subnet #{cidr}. Some available ip's may be cached. Try again in #{@ip_cache.get_cleanup_interval} seconds after cache is cleared." }
71
+ end
72
+ end
73
+
74
+ next_ip
75
+ end
76
+
77
+ def increment_ip(ip)
78
+ IPAddr.new(ip.to_s).succ.to_s
79
+ end
80
+
81
+ def usable_ip(ip, cidr)
82
+ network = IPAddr.new(cidr)
83
+ network.include?(IPAddr.new(ip)) && network.to_range.last != ip
84
+ end
85
+
86
+ def get_request_group(params)
87
+ group = params[:group] ? URI.escape(params[:group]) : nil
88
+ halt 500, { error: errors[:groups_not_supported] }.to_json if group && !provider.groups_supported?
89
+ group
90
+ end
91
+
92
+ def errors
93
+ {
94
+ cidr: "A 'cidr' parameter for the subnet must be provided(e.g. IPv4: 100.10.10.0/24, IPv6: 2001:db8:abcd:12::/124)",
95
+ mac: "A 'mac' address must be provided(e.g. 00:0a:95:9d:68:10)",
96
+ ip: "Missing 'ip' parameter. An IPv4 or IPv6 address must be provided(e.g. IPv4: 100.10.10.22, IPv6: 2001:db8:abcd:12::3)",
97
+ group_name: "A 'group_name' must be provided",
98
+ no_ip: 'IP address not found',
99
+ no_free_ips: 'No free addresses found',
100
+ no_connection: 'Unable to connect to External IPAM server',
101
+ no_group: 'Group not found in External IPAM',
102
+ no_groups: 'No groups found in External IPAM',
103
+ no_subnet: 'Subnet not found in External IPAM',
104
+ no_subnets_in_group: 'No subnets found in External IPAM group',
105
+ provider: "The IPAM provider must be specified(e.g. 'phpipam' or 'netbox')",
106
+ groups_not_supported: 'Groups are not supported',
107
+ add_ip: 'Error adding IP to External IPAM',
108
+ bad_mac: 'Mac address is invalid',
109
+ bad_ip: 'IP address is invalid',
110
+ bad_cidr: 'The network cidr is invalid'
111
+ }
112
+ end
113
+ end
@@ -1,6 +1,5 @@
1
-
2
- require 'smart_proxy_ipam/phpipam/phpipam_api'
1
+ require 'smart_proxy_ipam/ipam_api'
3
2
 
4
3
  map '/ipam' do
5
- run Proxy::Phpipam::Api
4
+ run Proxy::Ipam::Api
6
5
  end
@@ -0,0 +1,48 @@
1
+ require 'resolv'
2
+
3
+ # Module containing validation methods for use by all External IPAM provider implementations
4
+ module Proxy::Ipam::IpamValidator
5
+ include ::Proxy::Validations
6
+ include Proxy::Ipam::IpamHelper
7
+
8
+ def validate_required_params!(required_params, params)
9
+ err = []
10
+ required_params.each do |param|
11
+ unless params[param.to_sym]
12
+ err.push errors[param.to_sym]
13
+ end
14
+ end
15
+ raise Proxy::Validations::Error, err unless err.empty?
16
+ end
17
+
18
+ def validate_ip!(ip)
19
+ good_ip = ip =~ Regexp.union([Resolv::IPv4::Regex, Resolv::IPv6::Regex])
20
+ raise Proxy::Validations::Error, errors[:bad_ip] if good_ip.nil?
21
+ ip
22
+ end
23
+
24
+ def validate_cidr!(address, prefix)
25
+ cidr = "#{address}/#{prefix}"
26
+ network = IPAddr.new(cidr).to_s
27
+ if IPAddr.new(cidr).to_s != IPAddr.new(address).to_s
28
+ raise Proxy::Validations::Error, "Network address #{address} should be #{network} with prefix #{prefix}"
29
+ end
30
+ cidr
31
+ rescue IPAddr::Error => e
32
+ raise Proxy::Validations::Error, e.to_s
33
+ end
34
+
35
+ def validate_ip_in_cidr!(ip, cidr)
36
+ unless IPAddr.new(cidr).include?(IPAddr.new(ip))
37
+ raise Proxy::Validations::Error.new, "IP #{ip} is not in #{cidr}"
38
+ end
39
+ end
40
+
41
+ def validate_mac!(mac)
42
+ raise Proxy::Validations::Error.new, errors[:mac] if mac.nil? || mac.empty?
43
+ unless mac.match(/^([0-9a-fA-F]{2}[:]){5}[0-9a-fA-F]{2}$/i)
44
+ raise Proxy::Validations::Error.new, errors[:bad_mac]
45
+ end
46
+ mac
47
+ end
48
+ end
@@ -0,0 +1,193 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'sinatra'
6
+ require 'smart_proxy_ipam/ipam'
7
+ require 'smart_proxy_ipam/ipam_helper'
8
+ require 'smart_proxy_ipam/ipam_validator'
9
+ require 'smart_proxy_ipam/api_resource'
10
+ require 'smart_proxy_ipam/ip_cache'
11
+
12
+ module Proxy::Netbox
13
+ # Implementation class for External IPAM provider Netbox
14
+ class NetboxClient
15
+ include Proxy::Log
16
+ include Proxy::Ipam::IpamHelper
17
+ include Proxy::Ipam::IpamValidator
18
+
19
+ @ip_cache = nil
20
+
21
+ def initialize(conf)
22
+ @api_base = "#{conf[:url]}/api/"
23
+ @token = conf[:token]
24
+ @api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: 'Token ' + @token)
25
+ @ip_cache = Proxy::Ipam::IpCache.new(provider: 'netbox')
26
+ end
27
+
28
+ def get_ipam_subnet(cidr, group_name = nil)
29
+ if group_name.nil? || group_name.empty?
30
+ get_ipam_subnet_by_cidr(cidr)
31
+ else
32
+ group_id = get_group_id(group_name)
33
+ get_ipam_subnet_by_group(cidr, group_id)
34
+ end
35
+ end
36
+
37
+ def get_ipam_subnet_by_group(cidr, group_id)
38
+ response = @api_resource.get("ipam/prefixes/?status=active&prefix=#{cidr}&vrf_id=#{group_id}")
39
+ json_body = JSON.parse(response.body)
40
+ return nil if json_body['count'].zero?
41
+ subnet = subnet_from_result(json_body['results'][0])
42
+ return subnet if json_body['results']
43
+ end
44
+
45
+ def get_ipam_subnet_by_cidr(cidr)
46
+ response = @api_resource.get("ipam/prefixes/?status=active&prefix=#{cidr}")
47
+ json_body = JSON.parse(response.body)
48
+ return nil if json_body['count'].zero?
49
+ subnet = subnet_from_result(json_body['results'][0])
50
+ return subnet if json_body['results']
51
+ end
52
+
53
+ def get_ipam_groups
54
+ response = @api_resource.get('ipam/vrfs/')
55
+ json_body = JSON.parse(response.body)
56
+ groups = []
57
+
58
+ return nil if json_body['count'].zero?
59
+
60
+ json_body['results'].each do |group|
61
+ groups.push({
62
+ name: group['name'],
63
+ description: group['description']
64
+ })
65
+ end
66
+
67
+ groups
68
+ end
69
+
70
+ def get_ipam_group(group_name)
71
+ raise errors[:groups_not_supported] unless groups_supported?
72
+ response = @api_resource.get("ipam/vrfs/?name=#{group_name}")
73
+ json_body = JSON.parse(response.body)
74
+ return nil if json_body['count'].zero?
75
+
76
+ group = {
77
+ id: json_body['results'][0]['id'],
78
+ name: json_body['results'][0]['name'],
79
+ description: json_body['results'][0]['description']
80
+ }
81
+
82
+ return group if json_body['results']
83
+ end
84
+
85
+ def get_group_id(group_name)
86
+ return nil if group_name.nil? || group_name.empty?
87
+ group = get_ipam_group(group_name)
88
+ raise errors[:no_group] if group.nil?
89
+ group[:id]
90
+ end
91
+
92
+ def get_ipam_subnets(group_name)
93
+ if group_name.nil?
94
+ response = @api_resource.get('ipam/prefixes/?status=active')
95
+ else
96
+ group_id = get_group_id(group_name)
97
+ response = @api_resource.get("ipam/prefixes/?status=active&vrf_id=#{group_id}")
98
+ end
99
+
100
+ json_body = JSON.parse(response.body)
101
+ return nil if json_body['count'].zero?
102
+ subnets = []
103
+
104
+ json_body['results'].each do |subnet|
105
+ subnets.push({
106
+ subnet: subnet['prefix'].split('/').first,
107
+ mask: subnet['prefix'].split('/').last,
108
+ description: subnet['description'],
109
+ id: subnet['id']
110
+ })
111
+ end
112
+
113
+ return subnets if json_body['results']
114
+ end
115
+
116
+ def ip_exists?(ip, subnet_id, group_name)
117
+ group_id = get_group_id(group_name)
118
+ url = "ipam/ip-addresses/?address=#{ip}"
119
+ url += "&prefix_id=#{subnet_id}" unless subnet_id.nil?
120
+ url += "&vrf_id=#{group_id}" unless group_id.nil?
121
+ response = @api_resource.get(url)
122
+ json_body = JSON.parse(response.body)
123
+ return false if json_body['count'].zero?
124
+ true
125
+ end
126
+
127
+ def add_ip_to_subnet(ip, params)
128
+ desc = 'Address auto added by Foreman'
129
+ address = "#{ip}/#{params[:cidr].split('/').last}"
130
+ group_name = params[:group_name]
131
+
132
+ if group_name.nil? || group_name.empty?
133
+ data = { address: address, nat_outside: 0, description: desc }
134
+ else
135
+ group_id = get_group_id(group_name)
136
+ data = { vrf: group_id, address: address, nat_outside: 0, description: desc }
137
+ end
138
+
139
+ response = @api_resource.post('ipam/ip-addresses/', data.to_json)
140
+ return nil if response.code == '201'
141
+ { error: "Unable to add #{address} in External IPAM server" }
142
+ end
143
+
144
+ def delete_ip_from_subnet(ip, params)
145
+ group_name = params[:group_name]
146
+
147
+ if group_name.nil? || group_name.empty?
148
+ response = @api_resource.get("ipam/ip-addresses/?address=#{ip}")
149
+ else
150
+ group_id = get_group_id(group_name)
151
+ response = @api_resource.get("ipam/ip-addresses/?address=#{ip}&vrf_id=#{group_id}")
152
+ end
153
+
154
+ json_body = JSON.parse(response.body)
155
+
156
+ return { error: errors[:no_ip] } if json_body['count'].zero?
157
+
158
+ address_id = json_body['results'][0]['id']
159
+ response = @api_resource.delete("ipam/ip-addresses/#{address_id}/")
160
+ return nil if response.code == '204'
161
+ { error: "Unable to delete #{ip} in External IPAM server" }
162
+ end
163
+
164
+ def get_next_ip(mac, cidr, group_name)
165
+ subnet = get_ipam_subnet(cidr, group_name)
166
+ raise errors[:no_subnet] if subnet.nil?
167
+ response = @api_resource.get("ipam/prefixes/#{subnet[:id]}/available-ips/?limit=1")
168
+ json_body = JSON.parse(response.body)
169
+ return nil if json_body.empty?
170
+ ip = json_body[0]['address'].split('/').first
171
+ cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
172
+ end
173
+
174
+ def groups_supported?
175
+ true
176
+ end
177
+
178
+ def authenticated?
179
+ !@token.nil?
180
+ end
181
+
182
+ private
183
+
184
+ def subnet_from_result(result)
185
+ {
186
+ subnet: result['prefix'].split('/').first,
187
+ mask: result['prefix'].split('/').last,
188
+ description: result['description'],
189
+ id: result['id']
190
+ }
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,17 @@
1
+ module Proxy::Netbox
2
+ class Plugin < ::Proxy::Provider
3
+ plugin :externalipam_netbox, Proxy::Ipam::VERSION
4
+
5
+ requires :externalipam, Proxy::Ipam::VERSION
6
+ validate :url, url: true
7
+ validate_presence :token
8
+
9
+ load_classes(proc do
10
+ require 'smart_proxy_ipam/netbox/netbox_client'
11
+ end)
12
+
13
+ load_dependency_injection_wirings(proc do |container_instance, settings|
14
+ container_instance.dependency :externalipam_client, -> { ::Proxy::Netbox::NetboxClient.new(settings) }
15
+ end)
16
+ end
17
+ end
@@ -1,334 +1,184 @@
1
1
  require 'yaml'
2
- require 'json'
2
+ require 'json'
3
3
  require 'net/http'
4
- require 'monitor'
5
- require 'concurrent'
6
- require 'time'
7
4
  require 'uri'
5
+ require 'sinatra'
8
6
  require 'smart_proxy_ipam/ipam'
9
- require 'smart_proxy_ipam/ipam_main'
10
- require 'smart_proxy_ipam/phpipam/phpipam_helper'
7
+ require 'smart_proxy_ipam/ipam_helper'
8
+ require 'smart_proxy_ipam/ipam_validator'
9
+ require 'smart_proxy_ipam/api_resource'
10
+ require 'smart_proxy_ipam/ip_cache'
11
11
 
12
12
  module Proxy::Phpipam
13
+ # Implementation class for External IPAM provider phpIPAM
13
14
  class PhpipamClient
14
15
  include Proxy::Log
15
- include PhpipamHelper
16
+ include Proxy::Ipam::IpamHelper
17
+ include Proxy::Ipam::IpamValidator
16
18
 
17
- MAX_RETRIES = 5
18
- DEFAULT_CLEANUP_INTERVAL = 60 # 2 mins
19
- @@ip_cache = nil
20
- @@timer_task = nil
19
+ @ip_cache = nil
21
20
 
22
- def initialize
23
- @conf = Proxy::Ipam.get_config[:phpipam]
21
+ def initialize(conf)
22
+ @conf = conf
24
23
  @api_base = "#{@conf[:url]}/api/#{@conf[:user]}/"
25
- @token = nil
26
- @@m = Monitor.new
27
- init_cache if @@ip_cache.nil?
28
- start_cleanup_task if @@timer_task.nil?
24
+ @token = authenticate
25
+ @api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: @token, auth_header: 'Token')
26
+ @ip_cache = Proxy::Ipam::IpCache.new(provider: 'phpipam')
29
27
  end
30
28
 
31
- def get_subnet(cidr, section_name = nil)
32
- if section_name.nil? || section_name.empty?
33
- get_subnet_by_cidr(cidr)
29
+ def get_ipam_subnet(cidr, group_name = nil)
30
+ if group_name.nil? || group_name.empty?
31
+ get_ipam_subnet_by_cidr(cidr)
34
32
  else
35
- get_subnet_by_section(cidr, section_name)
33
+ group = get_ipam_group(group_name)
34
+ get_ipam_subnet_by_group(cidr, group[:id])
36
35
  end
37
36
  end
38
37
 
39
- def get_subnet_by_section(cidr, section_name, include_id = true)
40
- section = JSON.parse(get_section(section_name))
41
- subnets = JSON.parse(get_subnets(section['data']['id'], include_id))
38
+ def get_ipam_subnet_by_group(cidr, group_id)
39
+ subnets = get_ipam_subnets(group_id)
40
+ return nil if subnets.nil?
42
41
  subnet_id = nil
43
42
 
44
- subnets['data'].each do |subnet|
45
- subnet_cidr = subnet['subnet'] + '/' + subnet['mask']
46
- subnet_id = subnet['id'] if subnet_cidr == cidr
43
+ subnets.each do |subnet|
44
+ subnet_cidr = subnet[:subnet] + '/' + subnet[:mask]
45
+ subnet_id = subnet[:id] if subnet_cidr == cidr
47
46
  end
48
-
49
- return {:code => 404, :error => "No subnet #{cidr} found in section #{URI.unescape(section_name)}"}.to_json if subnet_id.nil?
50
-
51
- response = get("subnets/#{subnet_id.to_s}/")
52
- json_body = JSON.parse(response.body)
53
- json_body['data'] = filter_hash(json_body['data'], [:id, :subnet, :mask, :description]) if json_body['data']
54
- json_body = filter_hash(json_body, [:code, :data, :error, :message])
55
- response.body = json_body.to_json
56
- response.header['Content-Length'] = json_body.to_s.length
57
- response.body
58
- end
59
-
60
- def get_subnet_by_cidr(cidr)
61
- response = get("subnets/cidr/#{cidr.to_s}")
62
- json_body = JSON.parse(response.body)
63
- json_body['data'] = filter_fields(json_body, [:id, :subnet, :description, :mask])[0]
64
- json_body = filter_hash(json_body, [:code, :data, :error, :message])
65
- response.body = json_body.to_json
66
- response.header['Content-Length'] = json_body.to_s.length
67
- response.body
68
- end
69
47
 
70
- def get_section(section_name)
71
- response = get("sections/#{section_name}/")
48
+ return nil if subnet_id.nil?
49
+ response = @api_resource.get("subnets/#{subnet_id}/")
72
50
  json_body = JSON.parse(response.body)
73
- json_body['data'] = filter_hash(json_body['data'], [:id, :name, :description]) if json_body['data']
74
- json_body = filter_hash(json_body, [:code, :data, :error, :message])
75
- response.body = json_body.to_json
76
- response.header['Content-Length'] = json_body.to_s.length
77
- response.body
78
- end
79
51
 
80
- def get_sections
81
- response = get('sections/')
82
- json_body = JSON.parse(response.body)
83
- json_body['data'] = filter_fields(json_body, [:id, :name, :description]) if json_body['data']
84
- json_body = filter_hash(json_body, [:code, :data, :error, :message])
85
- response.body = json_body.to_json
86
- response.header['Content-Length'] = json_body.to_s.length
87
- response.body
88
- end
52
+ data = {
53
+ id: json_body['data']['id'],
54
+ subnet: json_body['data']['subnet'],
55
+ mask: json_body['data']['mask'],
56
+ description: json_body['data']['description']
57
+ }
89
58
 
90
- def get_subnets(section_id, include_id = true)
91
- response = get("sections/#{section_id}/subnets/")
92
- fields = [:subnet, :mask, :description]
93
- fields.push(:id) if include_id
94
- json_body = JSON.parse(response.body)
95
- json_body['data'] = filter_fields(json_body, fields) if json_body['data']
96
- json_body = filter_hash(json_body, [:code, :data, :error, :message])
97
- response.body = json_body.to_json
98
- response.header['Content-Length'] = json_body.to_s.length
99
- response.body
59
+ return data if json_body['data']
100
60
  end
101
61
 
102
- def ip_exists(ip, subnet_id)
103
- response = get("subnets/#{subnet_id.to_s}/addresses/#{ip}/")
104
- json_body = JSON.parse(response.body)
105
- json_body['data'] = filter_fields(json_body, [:ip]) if json_body['data']
106
- json_body = filter_hash(json_body, [:code, :data, :error, :message])
107
- response.body = json_body.to_json
108
- response.header['Content-Length'] = json_body.to_s.length
109
- response.body
110
- end
62
+ def get_ipam_subnet_by_cidr(cidr)
63
+ subnet = @api_resource.get("subnets/cidr/#{cidr}")
64
+ json_body = JSON.parse(subnet.body)
65
+ return nil if json_body['data'].nil?
111
66
 
112
- def add_ip_to_subnet(ip, subnet_id, desc)
113
- data = {:subnetId => subnet_id, :ip => ip, :description => desc}
114
- response = post('addresses/', data)
115
- json_body = JSON.parse(response.body)
116
- json_body = filter_hash(json_body, [:code, :error, :message])
117
- response.body = json_body.to_json
118
- response.header['Content-Length'] = json_body.to_s.length
119
- response.body
120
- end
67
+ data = {
68
+ id: json_body['data'][0]['id'],
69
+ subnet: json_body['data'][0]['subnet'],
70
+ mask: json_body['data'][0]['mask'],
71
+ description: json_body['data'][0]['description']
72
+ }
121
73
 
122
- def delete_ip_from_subnet(ip, subnet_id)
123
- response = delete("addresses/#{ip}/#{subnet_id.to_s}/")
124
- json_body = JSON.parse(response.body)
125
- json_body = filter_hash(json_body, [:code, :error, :message])
126
- response.body = json_body.to_json
127
- response.header['Content-Length'] = json_body.to_s.length
128
- response.body
74
+ return data if json_body['data']
129
75
  end
130
76
 
131
- def get_next_ip(subnet_id, mac, cidr, section_name)
132
- response = get("subnets/#{subnet_id.to_s}/first_free/")
133
- json_body = JSON.parse(response.body)
134
- section = section_name.nil? ? "" : section_name
135
- @@ip_cache[section.to_sym] = {} if @@ip_cache[section.to_sym].nil?
136
- subnet_hash = @@ip_cache[section.to_sym][cidr.to_sym]
137
-
138
- return {:code => json_body['code'], :error => json_body['message']}.to_json if json_body['message']
139
-
140
- if subnet_hash && subnet_hash.key?(mac.to_sym)
141
- json_body['data'] = @@ip_cache[section_name.to_sym][cidr.to_sym][mac.to_sym][:ip]
142
- else
143
- next_ip = nil
144
- new_ip = json_body['data']
145
- ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
146
-
147
- if ip_not_in_cache
148
- next_ip = new_ip.to_s
149
- add_ip_to_cache(new_ip, mac, cidr, section)
150
- else
151
- next_ip = find_new_ip(subnet_id, new_ip, mac, cidr, section)
152
- end
153
-
154
- return {:code => 404, :error => "Unable to find another available IP address in subnet #{cidr}"}.to_json if next_ip.nil?
155
- return {:code => 404, :error => "It is possible that there are no more free addresses in subnet #{cidr}. Available IP's may be cached, and could become available after in-memory IP cache is cleared(up to #{DEFAULT_CLEANUP_INTERVAL} seconds)."}.to_json unless usable_ip(next_ip, cidr)
156
-
157
- json_body['data'] = next_ip
158
- end
159
-
160
- json_body = {:code => json_body['code'], :data => json_body['data']}
77
+ def get_ipam_group(group_name)
78
+ return nil if group_name.nil?
79
+ group = @api_resource.get("sections/#{group_name}/")
80
+ json_body = JSON.parse(group.body)
81
+ raise errors[:no_group] if json_body['data'].nil?
161
82
 
162
- response.body = json_body.to_json
163
- response.header['Content-Length'] = json_body.to_s.length
164
- response.body
165
- end
83
+ data = {
84
+ id: json_body['data']['id'],
85
+ name: json_body['data']['name'],
86
+ description: json_body['data']['description']
87
+ }
166
88
 
167
- def start_cleanup_task
168
- logger.info("Starting allocated ip address maintenance (used by get_next_ip call).")
169
- @@timer_task = Concurrent::TimerTask.new(:execution_interval => DEFAULT_CLEANUP_INTERVAL) { init_cache }
170
- @@timer_task.execute
89
+ return data if json_body['data']
171
90
  end
172
91
 
173
- private
92
+ def get_ipam_groups
93
+ groups = @api_resource.get('sections/')
94
+ json_body = JSON.parse(groups.body)
95
+ return nil if json_body['data'].nil?
174
96
 
175
- # @@ip_cache structure
176
- #
177
- # Groups of subnets are cached under the External IPAM Group name. For example,
178
- # "IPAM Group Name" would be the section name in phpIPAM. All IP's cached for subnets
179
- # that do not have an External IPAM group specified, they are cached under the "" key.
180
- #
181
- # {
182
- # "": {
183
- # "100.55.55.0/24":{
184
- # "00:0a:95:9d:68:10": {"ip": "100.55.55.1", "timestamp": "2019-09-17 12:03:43 -D400"}
185
- # },
186
- # },
187
- # "IPAM Group Name": {
188
- # "123.11.33.0/24":{
189
- # "00:0a:95:9d:68:33": {"ip": "123.11.33.1", "timestamp": "2019-09-17 12:04:43 -0400"},
190
- # "00:0a:95:9d:68:34": {"ip": "123.11.33.2", "timestamp": "2019-09-17 12:05:48 -0400"},
191
- # "00:0a:95:9d:68:35": {"ip": "123.11.33.3", "timestamp:: "2019-09-17 12:06:50 -0400"}
192
- # }
193
- # },
194
- # "Another IPAM Group": {
195
- # "185.45.39.0/24":{
196
- # "00:0a:95:9d:68:55": {"ip": "185.45.39.1", "timestamp": "2019-09-17 12:04:43 -0400"},
197
- # "00:0a:95:9d:68:56": {"ip": "185.45.39.2", "timestamp": "2019-09-17 12:05:48 -0400"}
198
- # }
199
- # }
200
- # }
201
- def init_cache
202
- @@m.synchronize do
203
- if @@ip_cache and not @@ip_cache.empty?
204
- logger.debug("Processing ip cache.")
205
- @@ip_cache.each do |section, subnets|
206
- subnets.each do |cidr, macs|
207
- macs.each do |mac, ip|
208
- if Time.now - Time.parse(ip[:timestamp]) > DEFAULT_CLEANUP_INTERVAL
209
- @@ip_cache[section][cidr].delete(mac)
210
- end
211
- end
212
- @@ip_cache[section].delete(cidr) if @@ip_cache[section][cidr].nil? or @@ip_cache[section][cidr].empty?
213
- end
214
- end
215
- else
216
- logger.debug("Clearing ip cache.")
217
- @@ip_cache = {:"" => {}}
218
- end
97
+ data = []
98
+ json_body['data'].each do |group|
99
+ data.push({
100
+ id: group['id'],
101
+ name: group['name'],
102
+ description: group['description']
103
+ })
219
104
  end
220
- end
221
105
 
222
- def add_ip_to_cache(ip, mac, cidr, section_name)
223
- logger.debug("Adding IP #{ip} to cache for subnet #{cidr} in section #{section_name}")
224
- @@m.synchronize do
225
- # Clear cache data which has the same mac and ip with the new one
226
- section_hash = @@ip_cache[section_name.to_sym]
227
- section_hash.each do |key, values|
228
- if values.keys.include? mac.to_sym
229
- @@ip_cache[section_name.to_sym][key].delete(mac.to_sym)
230
- end
231
- @@ip_cache[section_name.to_sym].delete(key) if @@ip_cache[section_name.to_sym][key].nil? or @@ip_cache[section_name.to_sym][key].empty?
232
- end
233
-
234
- if section_hash.key?(cidr.to_sym)
235
- @@ip_cache[section_name.to_sym][cidr.to_sym][mac.to_sym] = {:ip => ip.to_s, :timestamp => Time.now.to_s}
236
- else
237
- @@ip_cache = @@ip_cache.merge({section_name.to_sym => {cidr.to_sym => {mac.to_sym => {:ip => ip.to_s, :timestamp => Time.now.to_s}}}})
238
- end
239
- end
106
+ return data if json_body['data']
240
107
  end
241
108
 
242
- # Called when next available IP from external IPAM has been cached by another user/host, but
243
- # not actually persisted in external IPAM. Will increment the IP(MAX_RETRIES times), and
244
- # see if it is available in external IPAM.
245
- def find_new_ip(subnet_id, ip, mac, cidr, section_name)
246
- found_ip = nil
247
- temp_ip = ip
248
- retry_count = 0
109
+ def get_ipam_subnets(group_name)
110
+ group = get_ipam_group(group_name)
111
+ raise errors[:no_group] if group.nil?
112
+ subnets = @api_resource.get("sections/#{group[:id]}/subnets/")
113
+ json_body = JSON.parse(subnets.body)
114
+ return nil if json_body['data'].nil?
249
115
 
250
- loop do
251
- new_ip = increment_ip(temp_ip)
252
- verify_ip = JSON.parse(ip_exists(new_ip, subnet_id))
253
-
254
- # If new IP doesn't exist in IPAM and not in the cache
255
- if ip_not_found_in_ipam?(verify_ip) && !ip_exists_in_cache(new_ip, cidr, mac, section_name)
256
- found_ip = new_ip.to_s
257
- add_ip_to_cache(found_ip, mac, cidr, section_name)
258
- break
259
- end
260
-
261
- temp_ip = new_ip
262
- retry_count += 1
263
- break if retry_count >= MAX_RETRIES
116
+ data = []
117
+ json_body['data'].each do |subnet|
118
+ data.push({
119
+ id: subnet['id'],
120
+ subnet: subnet['subnet'],
121
+ mask: subnet['mask'],
122
+ description: subnet['description']
123
+ })
264
124
  end
265
125
 
266
- # Return the original IP found in external ipam if no new ones found after MAX_RETRIES
267
- return ip if found_ip.nil?
268
-
269
- found_ip
126
+ return data if json_body['data']
270
127
  end
271
128
 
272
- def increment_ip(ip)
273
- IPAddr.new(ip.to_s).succ.to_s
129
+ def ip_exists?(ip, subnet_id, _group_name)
130
+ ip = @api_resource.get("subnets/#{subnet_id}/addresses/#{ip}/")
131
+ json_body = JSON.parse(ip.body)
132
+ json_body['success']
274
133
  end
275
134
 
276
- def ip_exists_in_cache(ip, cidr, mac, section_name)
277
- @@ip_cache[section_name.to_sym][cidr.to_sym] && @@ip_cache[section_name.to_sym][cidr.to_sym].to_s.include?(ip.to_s)
135
+ def add_ip_to_subnet(ip, params)
136
+ data = { subnetId: params[:subnet_id], ip: ip, description: 'Address auto added by Foreman' }
137
+ subnet = @api_resource.post('addresses/', data.to_json)
138
+ json_body = JSON.parse(subnet.body)
139
+ return nil if json_body['code'] == 201
140
+ { error: 'Unable to add IP to External IPAM' }
278
141
  end
279
142
 
280
- # Checks if given IP is within a subnet. Broadcast address is considered unusable
281
- def usable_ip(ip, cidr)
282
- network = IPAddr.new(cidr)
283
- network.include?(IPAddr.new(ip)) && network.to_range.last != ip
143
+ def delete_ip_from_subnet(ip, params)
144
+ subnet = @api_resource.delete("addresses/#{ip}/#{params[:subnet_id]}/")
145
+ json_body = JSON.parse(subnet.body)
146
+ return nil if json_body['success']
147
+ { error: 'Unable to delete IP from External IPAM' }
284
148
  end
285
149
 
286
- def get(path)
287
- authenticate
288
- uri = URI(@api_base + path)
289
- request = Net::HTTP::Get.new(uri)
290
- request['token'] = @token
291
-
292
- Net::HTTP.start(uri.hostname, uri.port) {|http|
293
- http.request(request)
294
- }
150
+ def get_next_ip(mac, cidr, group_name)
151
+ subnet = get_ipam_subnet(cidr, group_name)
152
+ raise errors[:no_subnet] if subnet.nil?
153
+ response = @api_resource.get("subnets/#{subnet[:id]}/first_free/")
154
+ json_body = JSON.parse(response.body)
155
+ return { error: json_body['message'] } if json_body['message']
156
+ ip = json_body['data']
157
+ cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
295
158
  end
296
159
 
297
- def delete(path, body=nil)
298
- authenticate
299
- uri = URI(@api_base + path)
300
- uri.query = URI.encode_www_form(body) if body
301
- request = Net::HTTP::Delete.new(uri)
302
- request['token'] = @token
303
-
304
- Net::HTTP.start(uri.hostname, uri.port) {|http|
305
- http.request(request)
306
- }
160
+ def groups_supported?
161
+ true
307
162
  end
308
163
 
309
- def post(path, body=nil)
310
- authenticate
311
- uri = URI(@api_base + path)
312
- uri.query = URI.encode_www_form(body) if body
313
- request = Net::HTTP::Post.new(uri)
314
- request['token'] = @token
315
-
316
- Net::HTTP.start(uri.hostname, uri.port) {|http|
317
- http.request(request)
318
- }
164
+ def authenticated?
165
+ !@token.nil?
319
166
  end
320
167
 
168
+ private
169
+
321
170
  def authenticate
322
171
  auth_uri = URI(@api_base + '/user/')
323
172
  request = Net::HTTP::Post.new(auth_uri)
324
173
  request.basic_auth @conf[:user], @conf[:password]
325
174
 
326
- response = Net::HTTP.start(auth_uri.hostname, auth_uri.port) {|http|
175
+ response = Net::HTTP.start(auth_uri.hostname, auth_uri.port, use_ssl: auth_uri.scheme == 'https') do |http|
327
176
  http.request(request)
328
- }
177
+ end
329
178
 
330
179
  response = JSON.parse(response.body)
331
- @token = response['data']['token']
332
- end
180
+ logger.warn(response['message']) if response['message']
181
+ response.dig('data', 'token')
182
+ end
333
183
  end
334
184
  end