smart_proxy_ipam 0.0.21 → 0.1.3

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