smart_proxy_ipam 0.0.20 → 0.1.2

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,110 @@
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
+ ERRORS = {
7
+ cidr: "A 'cidr' parameter for the subnet must be provided(e.g. IPv4: 100.10.10.0/24, IPv6: 2001:db8:abcd:12::/124)",
8
+ mac: "A 'mac' address must be provided(e.g. 00:0a:95:9d:68:10)",
9
+ 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)",
10
+ group_name: "A 'group_name' must be provided",
11
+ no_ip: 'IP address not found',
12
+ no_free_ips: 'No free addresses found',
13
+ no_connection: 'Unable to connect to External IPAM server',
14
+ no_group: 'Group not found in External IPAM',
15
+ no_groups: 'No groups found in External IPAM',
16
+ no_subnet: 'Subnet not found in External IPAM',
17
+ no_subnets_in_group: 'No subnets found in External IPAM group',
18
+ provider: "The IPAM provider must be specified(e.g. 'phpipam' or 'netbox')",
19
+ groups_not_supported: 'Groups are not supported',
20
+ add_ip: 'Error adding IP to External IPAM',
21
+ bad_mac: 'Mac address is invalid',
22
+ bad_ip: 'IP address is invalid',
23
+ bad_cidr: 'The network cidr is invalid'
24
+ }.freeze
25
+
26
+ def provider
27
+ @provider ||=
28
+ begin
29
+ unless client.authenticated?
30
+ halt 500, { error: 'Invalid credentials for External IPAM' }.to_json
31
+ end
32
+ client
33
+ end
34
+ end
35
+
36
+ # Called when next available IP from External IPAM has been cached by another user/host, but
37
+ # not actually persisted in External IPAM yet. This method will increment the IP, up to
38
+ # MAX_IP_RETRIES times, and check if it is available in External IPAM each iteration. It
39
+ # will return the original IP(the 'ip' param) if no new IP's are found after MAX_IP_RETRIES
40
+ # iterations.
41
+ def find_new_ip(ip_cache, subnet_id, ip, mac, cidr, group_name)
42
+ found_ip = nil
43
+ temp_ip = ip
44
+ retry_count = 0
45
+
46
+ loop do
47
+ new_ip = increment_ip(temp_ip)
48
+ ipam_ip = ip_exists?(new_ip, subnet_id, group_name)
49
+
50
+ # If new IP doesn't exist in IPAM and not in the cache
51
+ if !ipam_ip && !ip_cache.ip_exists(new_ip, cidr, group_name)
52
+ found_ip = new_ip.to_s
53
+ ip_cache.add(found_ip, mac, cidr, group_name)
54
+ break
55
+ end
56
+
57
+ temp_ip = new_ip
58
+ retry_count += 1
59
+ break if retry_count >= MAX_IP_RETRIES
60
+ end
61
+
62
+ return ip if found_ip.nil?
63
+
64
+ found_ip
65
+ end
66
+
67
+ # Checks the cache for existing ip, and returns it if it exists. If not exists, it will
68
+ # find a new ip (using find_new_ip), and it is added to the cache.
69
+ def cache_next_ip(ip_cache, ip, mac, cidr, subnet_id, group_name)
70
+ group = group_name.nil? ? '' : group_name
71
+ ip_cache.set_group(group, {}) if ip_cache.get_group(group).nil?
72
+ subnet_hash = ip_cache.get_cidr(group, cidr)
73
+ next_ip = nil
74
+
75
+ if subnet_hash&.key?(mac.to_sym)
76
+ next_ip = ip_cache.get_ip(group, cidr, mac)
77
+ else
78
+ new_ip = ip
79
+ ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
80
+
81
+ if ip_not_in_cache
82
+ next_ip = new_ip.to_s
83
+ ip_cache.add(new_ip, mac, cidr, group)
84
+ else
85
+ next_ip = find_new_ip(ip_cache, subnet_id, new_ip, mac, cidr, group)
86
+ end
87
+
88
+ unless usable_ip(next_ip, cidr)
89
+ 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." }
90
+ end
91
+ end
92
+
93
+ next_ip
94
+ end
95
+
96
+ def increment_ip(ip)
97
+ IPAddr.new(ip.to_s).succ.to_s
98
+ end
99
+
100
+ def usable_ip(ip, cidr)
101
+ network = IPAddr.new(cidr)
102
+ network.include?(IPAddr.new(ip)) && network.to_range.last != ip
103
+ end
104
+
105
+ def get_request_group(params)
106
+ group = params[:group] ? URI.escape(params[:group]) : nil
107
+ halt 500, { error: errors[:groups_not_supported] }.to_json if group && !provider.groups_supported?
108
+ group
109
+ end
110
+ 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,198 @@
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
+ def initialize(conf)
20
+ @api_base = "#{conf[:url]}/api/"
21
+ @token = conf[:token]
22
+ @api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: "Token #{@token}")
23
+ @ip_cache = Proxy::Ipam::IpCache.instance
24
+ @ip_cache.set_provider('netbox')
25
+ end
26
+
27
+ def get_ipam_subnet(cidr, group_name = nil)
28
+ if group_name.nil? || group_name.empty?
29
+ get_ipam_subnet_by_cidr(cidr)
30
+ else
31
+ group_id = get_group_id(group_name)
32
+ get_ipam_subnet_by_group(cidr, group_id)
33
+ end
34
+ end
35
+
36
+ def get_ipam_subnet_by_group(cidr, group_id)
37
+ params = URI.encode_www_form({ status: 'active', prefix: cidr, vrf_id: group_id })
38
+ response = @api_resource.get("ipam/prefixes/?#{params}")
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
+ params = URI.encode_www_form({ status: 'active', prefix: cidr })
47
+ response = @api_resource.get("ipam/prefixes/?#{params}")
48
+ json_body = JSON.parse(response.body)
49
+ return nil if json_body['count'].zero?
50
+ subnet = subnet_from_result(json_body['results'][0])
51
+ return subnet if json_body['results']
52
+ end
53
+
54
+ def get_ipam_groups
55
+ response = @api_resource.get('ipam/vrfs/')
56
+ json_body = JSON.parse(response.body)
57
+ groups = []
58
+
59
+ return groups if json_body['count'].zero?
60
+
61
+ json_body['results'].each do |group|
62
+ groups.push({
63
+ name: group['name'],
64
+ description: group['description']
65
+ })
66
+ end
67
+
68
+ groups
69
+ end
70
+
71
+ def get_ipam_group(group_name)
72
+ raise ERRORS[:groups_not_supported] unless groups_supported?
73
+ params = URI.encode_www_form({ name: group_name })
74
+ response = @api_resource.get("ipam/vrfs/?#{params}")
75
+ json_body = JSON.parse(response.body)
76
+ return nil if json_body['count'].zero?
77
+
78
+ group = {
79
+ id: json_body['results'][0]['id'],
80
+ name: json_body['results'][0]['name'],
81
+ description: json_body['results'][0]['description']
82
+ }
83
+
84
+ return group if json_body['results']
85
+ end
86
+
87
+ def get_group_id(group_name)
88
+ return nil if group_name.nil? || group_name.empty?
89
+ group = get_ipam_group(group_name)
90
+ raise ERRORS[:no_group] if group.nil?
91
+ group[:id]
92
+ end
93
+
94
+ def get_ipam_subnets(group_name)
95
+ if group_name.nil?
96
+ params = URI.encode_www_form({ status: 'active' })
97
+ else
98
+ group_id = get_group_id(group_name)
99
+ params = URI.encode_www_form({ status: 'active', vrf_id: group_id })
100
+ end
101
+
102
+ response = @api_resource.get("ipam/prefixes/?#{params}")
103
+ json_body = JSON.parse(response.body)
104
+ return nil if json_body['count'].zero?
105
+ subnets = []
106
+
107
+ json_body['results'].each do |subnet|
108
+ subnets.push({
109
+ subnet: subnet['prefix'].split('/').first,
110
+ mask: subnet['prefix'].split('/').last,
111
+ description: subnet['description'],
112
+ id: subnet['id']
113
+ })
114
+ end
115
+
116
+ return subnets if json_body['results']
117
+ end
118
+
119
+ def ip_exists?(ip, subnet_id, group_name)
120
+ group_id = get_group_id(group_name)
121
+ url = "ipam/ip-addresses/?#{URI.encode_www_form({ address: ip })}"
122
+ url += "&#{URI.encode_www_form({ prefix_id: subnet_id })}" unless subnet_id.nil?
123
+ url += "&#{URI.encode_www_form({ vrf_id: group_id })}" unless group_id.nil?
124
+ response = @api_resource.get(url)
125
+ json_body = JSON.parse(response.body)
126
+ return false if json_body['count'].zero?
127
+ true
128
+ end
129
+
130
+ def add_ip_to_subnet(ip, params)
131
+ desc = 'Address auto added by Foreman'
132
+ address = "#{ip}/#{params[:cidr].split('/').last}"
133
+ group_name = params[:group_name]
134
+
135
+ if group_name.nil? || group_name.empty?
136
+ data = { address: address, nat_outside: 0, description: desc }
137
+ else
138
+ group_id = get_group_id(group_name)
139
+ data = { vrf: group_id, address: address, nat_outside: 0, description: desc }
140
+ end
141
+
142
+ response = @api_resource.post('ipam/ip-addresses/', data.to_json)
143
+ return nil if response.code == '201'
144
+ { error: "Unable to add #{address} in External IPAM server" }
145
+ end
146
+
147
+ def delete_ip_from_subnet(ip, params)
148
+ group_name = params[:group_name]
149
+
150
+ if group_name.nil? || group_name.empty?
151
+ params = URI.encode_www_form({ address: ip })
152
+ else
153
+ group_id = get_group_id(group_name)
154
+ params = URI.encode_www_form({ address: ip, vrf_id: group_id })
155
+ end
156
+
157
+ response = @api_resource.get("ipam/ip-addresses/?#{params}")
158
+ json_body = JSON.parse(response.body)
159
+
160
+ return { error: ERRORS[:no_ip] } if json_body['count'].zero?
161
+
162
+ address_id = json_body['results'][0]['id']
163
+ response = @api_resource.delete("ipam/ip-addresses/#{address_id}/")
164
+ return nil if response.code == '204'
165
+ { error: "Unable to delete #{ip} in External IPAM server" }
166
+ end
167
+
168
+ def get_next_ip(mac, cidr, group_name)
169
+ subnet = get_ipam_subnet(cidr, group_name)
170
+ raise ERRORS[:no_subnet] if subnet.nil?
171
+ response = @api_resource.get("ipam/prefixes/#{subnet[:id]}/available-ips/?limit=1")
172
+ json_body = JSON.parse(response.body)
173
+ return nil if json_body.empty?
174
+ ip = json_body[0]['address'].split('/').first
175
+ next_ip = cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
176
+ { data: next_ip }
177
+ end
178
+
179
+ def groups_supported?
180
+ true
181
+ end
182
+
183
+ def authenticated?
184
+ !@token.nil?
185
+ end
186
+
187
+ private
188
+
189
+ def subnet_from_result(result)
190
+ {
191
+ subnet: result['prefix'].split('/').first,
192
+ mask: result['prefix'].split('/').last,
193
+ description: result['description'],
194
+ id: result['id']
195
+ }
196
+ end
197
+ end
198
+ 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,341 +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
21
-
22
- def initialize
23
- @conf = Proxy::Ipam.get_config[:phpipam]
19
+ def initialize(conf)
20
+ @conf = conf
24
21
  @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?
22
+ @token = authenticate
23
+ @api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: @token, auth_header: 'Token')
24
+ @ip_cache = Proxy::Ipam::IpCache.instance
25
+ @ip_cache.set_provider('phpipam')
29
26
  end
30
27
 
31
- def get_subnet(cidr, section_name = nil)
32
- if section_name.nil? || section_name.empty?
33
- get_subnet_by_cidr(cidr)
28
+ def get_ipam_subnet(cidr, group_name = nil)
29
+ if group_name.nil? || group_name.empty?
30
+ get_ipam_subnet_by_cidr(cidr)
34
31
  else
35
- get_subnet_by_section(cidr, section_name)
32
+ group = get_ipam_group(group_name)
33
+ get_ipam_subnet_by_group(cidr, group[:id])
36
34
  end
37
35
  end
38
36
 
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))
37
+ def get_ipam_subnet_by_group(cidr, group_id)
38
+ subnets = get_ipam_subnets(group_id)
39
+ return nil if subnets.nil?
42
40
  subnet_id = nil
43
41
 
44
- subnets['data'].each do |subnet|
45
- subnet_cidr = subnet['subnet'] + '/' + subnet['mask']
46
- subnet_id = subnet['id'] if subnet_cidr == cidr
42
+ subnets.each do |subnet|
43
+ subnet_cidr = "#{subnet[:subnet]}/#{subnet[:mask]}"
44
+ subnet_id = subnet[:id] if subnet_cidr == cidr
47
45
  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
46
 
60
- def get_subnet_by_cidr(cidr)
61
- response = get("subnets/cidr/#{cidr.to_s}")
47
+ return nil if subnet_id.nil?
48
+ response = @api_resource.get("subnets/#{subnet_id}/")
62
49
  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
-
70
- def get_section(section_name)
71
- response = get("sections/#{section_name}/")
72
- 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
50
 
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
51
+ data = {
52
+ id: json_body['data']['id'],
53
+ subnet: json_body['data']['subnet'],
54
+ mask: json_body['data']['mask'],
55
+ description: json_body['data']['description']
56
+ }
89
57
 
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
58
+ return data if json_body['data']
100
59
  end
101
60
 
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
61
+ def get_ipam_subnet_by_cidr(cidr)
62
+ subnet = @api_resource.get("subnets/cidr/#{cidr}")
63
+ json_body = JSON.parse(subnet.body)
64
+ return nil if json_body['data'].nil?
111
65
 
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
66
+ data = {
67
+ id: json_body['data'][0]['id'],
68
+ subnet: json_body['data'][0]['subnet'],
69
+ mask: json_body['data'][0]['mask'],
70
+ description: json_body['data'][0]['description']
71
+ }
121
72
 
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
73
+ return data if json_body['data']
129
74
  end
130
75
 
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']}
76
+ def get_ipam_group(group_name)
77
+ return nil if group_name.nil?
78
+ group = @api_resource.get("sections/#{group_name}/")
79
+ json_body = JSON.parse(group.body)
80
+ raise ERRORS[:no_group] if json_body['data'].nil?
161
81
 
162
- response.body = json_body.to_json
163
- response.header['Content-Length'] = json_body.to_s.length
164
- response.body
165
- end
82
+ data = {
83
+ id: json_body['data']['id'],
84
+ name: json_body['data']['name'],
85
+ description: json_body['data']['description']
86
+ }
166
87
 
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
88
+ return data if json_body['data']
171
89
  end
172
90
 
173
- private
91
+ def get_ipam_groups
92
+ groups = @api_resource.get('sections/')
93
+ json_body = JSON.parse(groups.body)
94
+ return [] if json_body['data'].nil?
174
95
 
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. IP's
180
- # are cached using one of two possible keys:
181
- # 1). Mac Address
182
- # 2). UUID (Used when Mac Address not specified)
183
- #
184
- # {
185
- # "": {
186
- # "100.55.55.0/24":{
187
- # "00:0a:95:9d:68:10": {"ip": "100.55.55.1", "timestamp": "2019-09-17 12:03:43 -D400"},
188
- # "906d8bdc-dcc0-4b59-92cb-665935e21662": {"ip": "100.55.55.2", "timestamp": "2019-09-17 11:43:22 -D400"}
189
- # },
190
- # },
191
- # "IPAM Group Name": {
192
- # "123.11.33.0/24":{
193
- # "00:0a:95:9d:68:33": {"ip": "123.11.33.1", "timestamp": "2019-09-17 12:04:43 -0400"},
194
- # "00:0a:95:9d:68:34": {"ip": "123.11.33.2", "timestamp": "2019-09-17 12:05:48 -0400"},
195
- # "00:0a:95:9d:68:35": {"ip": "123.11.33.3", "timestamp:: "2019-09-17 12:06:50 -0400"}
196
- # }
197
- # },
198
- # "Another IPAM Group": {
199
- # "185.45.39.0/24":{
200
- # "00:0a:95:9d:68:55": {"ip": "185.45.39.1", "timestamp": "2019-09-17 12:04:43 -0400"},
201
- # "00:0a:95:9d:68:56": {"ip": "185.45.39.2", "timestamp": "2019-09-17 12:05:48 -0400"}
202
- # }
203
- # }
204
- # }
205
- def init_cache
206
- @@m.synchronize do
207
- if @@ip_cache and not @@ip_cache.empty?
208
- logger.debug("Processing ip cache.")
209
- @@ip_cache.each do |section, subnets|
210
- subnets.each do |cidr, macs|
211
- macs.each do |mac, ip|
212
- if Time.now - Time.parse(ip[:timestamp]) > DEFAULT_CLEANUP_INTERVAL
213
- @@ip_cache[section][cidr].delete(mac)
214
- end
215
- end
216
- @@ip_cache[section].delete(cidr) if @@ip_cache[section][cidr].nil? or @@ip_cache[section][cidr].empty?
217
- end
218
- end
219
- else
220
- logger.debug("Clearing ip cache.")
221
- @@ip_cache = {:"" => {}}
222
- end
96
+ data = []
97
+ json_body['data'].each do |group|
98
+ data.push({
99
+ id: group['id'],
100
+ name: group['name'],
101
+ description: group['description']
102
+ })
223
103
  end
224
- end
225
104
 
226
- def add_ip_to_cache(ip, mac, cidr, section_name)
227
- logger.debug("Adding IP #{ip} to cache for subnet #{cidr} in section #{section_name}")
228
- @@m.synchronize do
229
- # Clear cache data which has the same mac and ip with the new one
230
-
231
- mac_addr = (mac.nil? || mac.empty?) ? SecureRandom.uuid : mac
232
- section_hash = @@ip_cache[section_name.to_sym]
233
-
234
- section_hash.each do |key, values|
235
- if values.keys.include? mac_addr.to_sym
236
- @@ip_cache[section_name.to_sym][key].delete(mac_addr.to_sym)
237
- end
238
- @@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?
239
- end
240
-
241
- if section_hash.key?(cidr.to_sym)
242
- @@ip_cache[section_name.to_sym][cidr.to_sym][mac_addr.to_sym] = {:ip => ip.to_s, :timestamp => Time.now.to_s}
243
- else
244
- @@ip_cache = @@ip_cache.merge({section_name.to_sym => {cidr.to_sym => {mac_addr.to_sym => {:ip => ip.to_s, :timestamp => Time.now.to_s}}}})
245
- end
246
- end
105
+ return data if json_body['data']
247
106
  end
248
107
 
249
- # Called when next available IP from external IPAM has been cached by another user/host, but
250
- # not actually persisted in external IPAM. Will increment the IP(MAX_RETRIES times), and
251
- # see if it is available in external IPAM.
252
- def find_new_ip(subnet_id, ip, mac, cidr, section_name)
253
- found_ip = nil
254
- temp_ip = ip
255
- retry_count = 0
108
+ def get_ipam_subnets(group_name)
109
+ group = get_ipam_group(group_name)
110
+ raise ERRORS[:no_group] if group.nil?
111
+ subnets = @api_resource.get("sections/#{group[:id]}/subnets/")
112
+ json_body = JSON.parse(subnets.body)
113
+ return nil if json_body['data'].nil?
256
114
 
257
- loop do
258
- new_ip = increment_ip(temp_ip)
259
- verify_ip = JSON.parse(ip_exists(new_ip, subnet_id))
260
-
261
- # If new IP doesn't exist in IPAM and not in the cache
262
- if ip_not_found_in_ipam?(verify_ip) && !ip_exists_in_cache(new_ip, cidr, mac, section_name)
263
- found_ip = new_ip.to_s
264
- add_ip_to_cache(found_ip, mac, cidr, section_name)
265
- break
266
- end
267
-
268
- temp_ip = new_ip
269
- retry_count += 1
270
- break if retry_count >= MAX_RETRIES
115
+ data = []
116
+ json_body['data'].each do |subnet|
117
+ data.push({
118
+ id: subnet['id'],
119
+ subnet: subnet['subnet'],
120
+ mask: subnet['mask'],
121
+ description: subnet['description']
122
+ })
271
123
  end
272
124
 
273
- # Return the original IP found in external ipam if no new ones found after MAX_RETRIES
274
- return ip if found_ip.nil?
275
-
276
- found_ip
125
+ return data if json_body['data']
277
126
  end
278
127
 
279
- def increment_ip(ip)
280
- IPAddr.new(ip.to_s).succ.to_s
128
+ def ip_exists?(ip, subnet_id, _group_name)
129
+ ip = @api_resource.get("subnets/#{subnet_id}/addresses/#{ip}/")
130
+ json_body = JSON.parse(ip.body)
131
+ json_body['success']
281
132
  end
282
133
 
283
- def ip_exists_in_cache(ip, cidr, mac, section_name)
284
- @@ip_cache[section_name.to_sym][cidr.to_sym] && @@ip_cache[section_name.to_sym][cidr.to_sym].to_s.include?(ip.to_s)
134
+ def add_ip_to_subnet(ip, params)
135
+ data = { subnetId: params[:subnet_id], ip: ip, description: 'Address auto added by Foreman' }
136
+ subnet = @api_resource.post('addresses/', data.to_json)
137
+ json_body = JSON.parse(subnet.body)
138
+ return nil if json_body['code'] == 201
139
+ { error: 'Unable to add IP to External IPAM' }
285
140
  end
286
141
 
287
- # Checks if given IP is within a subnet. Broadcast address is considered unusable
288
- def usable_ip(ip, cidr)
289
- network = IPAddr.new(cidr)
290
- network.include?(IPAddr.new(ip)) && network.to_range.last != ip
142
+ def delete_ip_from_subnet(ip, params)
143
+ subnet = @api_resource.delete("addresses/#{ip}/#{params[:subnet_id]}/")
144
+ json_body = JSON.parse(subnet.body)
145
+ return nil if json_body['success']
146
+ { error: 'Unable to delete IP from External IPAM' }
291
147
  end
292
148
 
293
- def get(path)
294
- authenticate
295
- uri = URI(@api_base + path)
296
- request = Net::HTTP::Get.new(uri)
297
- request['token'] = @token
298
-
299
- Net::HTTP.start(uri.hostname, uri.port) {|http|
300
- http.request(request)
301
- }
149
+ def get_next_ip(mac, cidr, group_name)
150
+ subnet = get_ipam_subnet(cidr, group_name)
151
+ raise ERRORS[:no_subnet] if subnet.nil?
152
+ response = @api_resource.get("subnets/#{subnet[:id]}/first_free/")
153
+ json_body = JSON.parse(response.body)
154
+ return { error: json_body['message'] } if json_body['message']
155
+ ip = json_body['data']
156
+ next_ip = cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
157
+ { data: next_ip }
302
158
  end
303
159
 
304
- def delete(path, body=nil)
305
- authenticate
306
- uri = URI(@api_base + path)
307
- uri.query = URI.encode_www_form(body) if body
308
- request = Net::HTTP::Delete.new(uri)
309
- request['token'] = @token
310
-
311
- Net::HTTP.start(uri.hostname, uri.port) {|http|
312
- http.request(request)
313
- }
160
+ def groups_supported?
161
+ true
314
162
  end
315
163
 
316
- def post(path, body=nil)
317
- authenticate
318
- uri = URI(@api_base + path)
319
- uri.query = URI.encode_www_form(body) if body
320
- request = Net::HTTP::Post.new(uri)
321
- request['token'] = @token
322
-
323
- Net::HTTP.start(uri.hostname, uri.port) {|http|
324
- http.request(request)
325
- }
164
+ def authenticated?
165
+ !@token.nil?
326
166
  end
327
167
 
168
+ private
169
+
328
170
  def authenticate
329
- auth_uri = URI(@api_base + '/user/')
171
+ auth_uri = URI("#{@api_base}/user/")
330
172
  request = Net::HTTP::Post.new(auth_uri)
331
173
  request.basic_auth @conf[:user], @conf[:password]
332
174
 
333
- 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|
334
176
  http.request(request)
335
- }
177
+ end
336
178
 
337
179
  response = JSON.parse(response.body)
338
- @token = response['data']['token']
339
- end
180
+ logger.warn(response['message']) if response['message']
181
+ response.dig('data', 'token')
182
+ end
340
183
  end
341
184
  end