smart_proxy_ipam 0.0.19 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ {data: 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