smart_proxy_ipam 0.0.22 → 0.1.4

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