smart_proxy_ipam 0.0.22 → 0.1.4

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