smart_proxy_ipam 0.0.21 → 0.1.3

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
+ 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