smart_proxy_ipam 0.0.22 → 0.1.0

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,113 @@
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
+
7
+ def provider
8
+ @provider ||=
9
+ begin
10
+ unless client.authenticated?
11
+ halt 500, {error: 'Invalid credentials for External IPAM'}.to_json
12
+ end
13
+ client
14
+ end
15
+ end
16
+
17
+ # Called when next available IP from External IPAM has been cached by another user/host, but
18
+ # not actually persisted in External IPAM yet. This method will increment the IP, up to
19
+ # MAX_IP_RETRIES times, and check if it is available in External IPAM each iteration. It
20
+ # will return the original IP(the 'ip' param) if no new IP's are found after MAX_IP_RETRIES
21
+ # iterations.
22
+ def find_new_ip(ip_cache, subnet_id, ip, mac, cidr, group_name)
23
+ found_ip = nil
24
+ temp_ip = ip
25
+ retry_count = 0
26
+
27
+ loop do
28
+ new_ip = increment_ip(temp_ip)
29
+ ipam_ip = ip_exists?(new_ip, subnet_id, group_name)
30
+
31
+ # If new IP doesn't exist in IPAM and not in the cache
32
+ if !ipam_ip && !ip_cache.ip_exists(new_ip, cidr, group_name)
33
+ found_ip = new_ip.to_s
34
+ ip_cache.add(found_ip, mac, cidr, group_name)
35
+ break
36
+ end
37
+
38
+ temp_ip = new_ip
39
+ retry_count += 1
40
+ break if retry_count >= MAX_IP_RETRIES
41
+ end
42
+
43
+ return ip if found_ip.nil?
44
+
45
+ found_ip
46
+ end
47
+
48
+ # Checks the cache for existing ip, and returns it if it exists. If not exists, it will
49
+ # find a new ip (using find_new_ip), and it is added to the cache.
50
+ def cache_next_ip(ip_cache, ip, mac, cidr, subnet_id, group_name)
51
+ group = group_name.nil? ? '' : group_name
52
+ ip_cache.set_group(group, {}) if ip_cache.get_group(group).nil?
53
+ subnet_hash = ip_cache.get_cidr(group, cidr)
54
+ next_ip = nil
55
+
56
+ if subnet_hash&.key?(mac.to_sym)
57
+ next_ip = ip_cache.get_ip(group, cidr, mac)
58
+ else
59
+ new_ip = ip
60
+ ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
61
+
62
+ if ip_not_in_cache
63
+ next_ip = new_ip.to_s
64
+ ip_cache.add(new_ip, mac, cidr, group)
65
+ else
66
+ next_ip = find_new_ip(ip_cache, subnet_id, new_ip, mac, cidr, group)
67
+ end
68
+
69
+ unless usable_ip(next_ip, cidr)
70
+ 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." }
71
+ end
72
+ end
73
+
74
+ next_ip
75
+ end
76
+
77
+ def increment_ip(ip)
78
+ IPAddr.new(ip.to_s).succ.to_s
79
+ end
80
+
81
+ def usable_ip(ip, cidr)
82
+ network = IPAddr.new(cidr)
83
+ network.include?(IPAddr.new(ip)) && network.to_range.last != ip
84
+ end
85
+
86
+ def get_request_group(params)
87
+ group = params[:group] ? URI.escape(params[:group]) : nil
88
+ halt 500, { error: errors[:groups_not_supported] }.to_json if group && !provider.groups_supported?
89
+ group
90
+ end
91
+
92
+ def errors
93
+ {
94
+ cidr: "A 'cidr' parameter for the subnet must be provided(e.g. IPv4: 100.10.10.0/24, IPv6: 2001:db8:abcd:12::/124)",
95
+ mac: "A 'mac' address must be provided(e.g. 00:0a:95:9d:68:10)",
96
+ 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)",
97
+ group_name: "A 'group_name' must be provided",
98
+ no_ip: 'IP address not found',
99
+ no_free_ips: 'No free addresses found',
100
+ no_connection: 'Unable to connect to External IPAM server',
101
+ no_group: 'Group not found in External IPAM',
102
+ no_groups: 'No groups found in External IPAM',
103
+ no_subnet: 'Subnet not found in External IPAM',
104
+ no_subnets_in_group: 'No subnets found in External IPAM group',
105
+ provider: "The IPAM provider must be specified(e.g. 'phpipam' or 'netbox')",
106
+ groups_not_supported: 'Groups are not supported',
107
+ add_ip: 'Error adding IP to External IPAM',
108
+ bad_mac: 'Mac address is invalid',
109
+ bad_ip: 'IP address is invalid',
110
+ bad_cidr: 'The network cidr is invalid'
111
+ }
112
+ end
113
+ 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,193 @@
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
+ @ip_cache = nil
20
+
21
+ def initialize(conf)
22
+ @api_base = "#{conf[:url]}/api/"
23
+ @token = conf[:token]
24
+ @api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: 'Token ' + @token)
25
+ @ip_cache = Proxy::Ipam::IpCache.new(provider: 'netbox')
26
+ end
27
+
28
+ def get_ipam_subnet(cidr, group_name = nil)
29
+ if group_name.nil? || group_name.empty?
30
+ get_ipam_subnet_by_cidr(cidr)
31
+ else
32
+ group_id = get_group_id(group_name)
33
+ get_ipam_subnet_by_group(cidr, group_id)
34
+ end
35
+ end
36
+
37
+ def get_ipam_subnet_by_group(cidr, group_id)
38
+ response = @api_resource.get("ipam/prefixes/?status=active&prefix=#{cidr}&vrf_id=#{group_id}")
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
+ response = @api_resource.get("ipam/prefixes/?status=active&prefix=#{cidr}")
47
+ json_body = JSON.parse(response.body)
48
+ return nil if json_body['count'].zero?
49
+ subnet = subnet_from_result(json_body['results'][0])
50
+ return subnet if json_body['results']
51
+ end
52
+
53
+ def get_ipam_groups
54
+ response = @api_resource.get('ipam/vrfs/')
55
+ json_body = JSON.parse(response.body)
56
+ groups = []
57
+
58
+ return nil if json_body['count'].zero?
59
+
60
+ json_body['results'].each do |group|
61
+ groups.push({
62
+ name: group['name'],
63
+ description: group['description']
64
+ })
65
+ end
66
+
67
+ groups
68
+ end
69
+
70
+ def get_ipam_group(group_name)
71
+ raise errors[:groups_not_supported] unless groups_supported?
72
+ response = @api_resource.get("ipam/vrfs/?name=#{group_name}")
73
+ json_body = JSON.parse(response.body)
74
+ return nil if json_body['count'].zero?
75
+
76
+ group = {
77
+ id: json_body['results'][0]['id'],
78
+ name: json_body['results'][0]['name'],
79
+ description: json_body['results'][0]['description']
80
+ }
81
+
82
+ return group if json_body['results']
83
+ end
84
+
85
+ def get_group_id(group_name)
86
+ return nil if group_name.nil? || group_name.empty?
87
+ group = get_ipam_group(group_name)
88
+ raise errors[:no_group] if group.nil?
89
+ group[:id]
90
+ end
91
+
92
+ def get_ipam_subnets(group_name)
93
+ if group_name.nil?
94
+ response = @api_resource.get('ipam/prefixes/?status=active')
95
+ else
96
+ group_id = get_group_id(group_name)
97
+ response = @api_resource.get("ipam/prefixes/?status=active&vrf_id=#{group_id}")
98
+ end
99
+
100
+ json_body = JSON.parse(response.body)
101
+ return nil if json_body['count'].zero?
102
+ subnets = []
103
+
104
+ json_body['results'].each do |subnet|
105
+ subnets.push({
106
+ subnet: subnet['prefix'].split('/').first,
107
+ mask: subnet['prefix'].split('/').last,
108
+ description: subnet['description'],
109
+ id: subnet['id']
110
+ })
111
+ end
112
+
113
+ return subnets if json_body['results']
114
+ end
115
+
116
+ def ip_exists?(ip, subnet_id, group_name)
117
+ group_id = get_group_id(group_name)
118
+ url = "ipam/ip-addresses/?address=#{ip}"
119
+ url += "&prefix_id=#{subnet_id}" unless subnet_id.nil?
120
+ url += "&vrf_id=#{group_id}" unless group_id.nil?
121
+ response = @api_resource.get(url)
122
+ json_body = JSON.parse(response.body)
123
+ return false if json_body['count'].zero?
124
+ true
125
+ end
126
+
127
+ def add_ip_to_subnet(ip, params)
128
+ desc = 'Address auto added by Foreman'
129
+ address = "#{ip}/#{params[:cidr].split('/').last}"
130
+ group_name = params[:group_name]
131
+
132
+ if group_name.nil? || group_name.empty?
133
+ data = { address: address, nat_outside: 0, description: desc }
134
+ else
135
+ group_id = get_group_id(group_name)
136
+ data = { vrf: group_id, address: address, nat_outside: 0, description: desc }
137
+ end
138
+
139
+ response = @api_resource.post('ipam/ip-addresses/', data.to_json)
140
+ return nil if response.code == '201'
141
+ { error: "Unable to add #{address} in External IPAM server" }
142
+ end
143
+
144
+ def delete_ip_from_subnet(ip, params)
145
+ group_name = params[:group_name]
146
+
147
+ if group_name.nil? || group_name.empty?
148
+ response = @api_resource.get("ipam/ip-addresses/?address=#{ip}")
149
+ else
150
+ group_id = get_group_id(group_name)
151
+ response = @api_resource.get("ipam/ip-addresses/?address=#{ip}&vrf_id=#{group_id}")
152
+ end
153
+
154
+ json_body = JSON.parse(response.body)
155
+
156
+ return { error: errors[:no_ip] } if json_body['count'].zero?
157
+
158
+ address_id = json_body['results'][0]['id']
159
+ response = @api_resource.delete("ipam/ip-addresses/#{address_id}/")
160
+ return nil if response.code == '204'
161
+ { error: "Unable to delete #{ip} in External IPAM server" }
162
+ end
163
+
164
+ def get_next_ip(mac, cidr, group_name)
165
+ subnet = get_ipam_subnet(cidr, group_name)
166
+ raise errors[:no_subnet] if subnet.nil?
167
+ response = @api_resource.get("ipam/prefixes/#{subnet[:id]}/available-ips/?limit=1")
168
+ json_body = JSON.parse(response.body)
169
+ return nil if json_body.empty?
170
+ ip = json_body[0]['address'].split('/').first
171
+ cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
172
+ end
173
+
174
+ def groups_supported?
175
+ true
176
+ end
177
+
178
+ def authenticated?
179
+ !@token.nil?
180
+ end
181
+
182
+ private
183
+
184
+ def subnet_from_result(result)
185
+ {
186
+ subnet: result['prefix'].split('/').first,
187
+ mask: result['prefix'].split('/').last,
188
+ description: result['description'],
189
+ id: result['id']
190
+ }
191
+ end
192
+ end
193
+ 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
19
+ @ip_cache = nil
21
20
 
22
- def initialize
23
- @conf = Proxy::Ipam.get_config[:phpipam]
21
+ def initialize(conf)
22
+ @conf = conf
24
23
  @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
24
+ @token = authenticate
25
+ @api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: @token, auth_header: 'Token')
26
+ @ip_cache = Proxy::Ipam::IpCache.new(provider: 'phpipam')
30
27
  end
31
28
 
32
- def get_subnet(cidr, section_name = nil)
33
- if section_name.nil? || section_name.empty?
34
- get_subnet_by_cidr(cidr)
29
+ def get_ipam_subnet(cidr, group_name = nil)
30
+ if group_name.nil? || group_name.empty?
31
+ get_ipam_subnet_by_cidr(cidr)
35
32
  else
36
- get_subnet_by_section(cidr, section_name)
33
+ group = get_ipam_group(group_name)
34
+ get_ipam_subnet_by_group(cidr, group[:id])
37
35
  end
38
36
  end
39
37
 
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))
38
+ def get_ipam_subnet_by_group(cidr, group_id)
39
+ subnets = get_ipam_subnets(group_id)
40
+ return nil if subnets.nil?
46
41
  subnet_id = nil
47
42
 
48
- subnets['data'].each do |subnet|
49
- subnet_cidr = subnet['subnet'] + '/' + subnet['mask']
50
- subnet_id = subnet['id'] if subnet_cidr == cidr
43
+ subnets.each do |subnet|
44
+ subnet_cidr = subnet[:subnet] + '/' + subnet[:mask]
45
+ subnet_id = subnet[:id] if subnet_cidr == cidr
51
46
  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
47
 
64
- def get_subnet_by_cidr(cidr)
65
- response = get("subnets/cidr/#{cidr.to_s}")
48
+ return nil if subnet_id.nil?
49
+ response = @api_resource.get("subnets/#{subnet_id}/")
66
50
  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
51
 
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
52
+ data = {
53
+ id: json_body['data']['id'],
54
+ subnet: json_body['data']['subnet'],
55
+ mask: json_body['data']['mask'],
56
+ description: json_body['data']['description']
57
+ }
84
58
 
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
59
+ return data if json_body['data']
93
60
  end
94
61
 
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
105
- end
106
-
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
62
+ def get_ipam_subnet_by_cidr(cidr)
63
+ subnet = @api_resource.get("subnets/cidr/#{cidr}")
64
+ json_body = JSON.parse(subnet.body)
65
+ return nil if json_body['data'].nil?
116
66
 
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
67
+ data = {
68
+ id: json_body['data'][0]['id'],
69
+ subnet: json_body['data'][0]['subnet'],
70
+ mask: json_body['data'][0]['mask'],
71
+ description: json_body['data'][0]['description']
72
+ }
126
73
 
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
74
+ return data if json_body['data']
134
75
  end
135
76
 
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
77
+ def get_ipam_group(group_name)
78
+ return nil if group_name.nil?
79
+ group = @api_resource.get("sections/#{group_name}/")
80
+ json_body = JSON.parse(group.body)
81
+ raise errors[:no_group] if json_body['data'].nil?
171
82
 
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
83
+ data = {
84
+ id: json_body['data']['id'],
85
+ name: json_body['data']['name'],
86
+ description: json_body['data']['description']
87
+ }
177
88
 
178
- def authenticated?
179
- !@token.nil?
89
+ return data if json_body['data']
180
90
  end
181
91
 
182
- private
92
+ def get_ipam_groups
93
+ groups = @api_resource.get('sections/')
94
+ json_body = JSON.parse(groups.body)
95
+ return nil if json_body['data'].nil?
183
96
 
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
97
+ data = []
98
+ json_body['data'].each do |group|
99
+ data.push({
100
+ id: group['id'],
101
+ name: group['name'],
102
+ description: group['description']
103
+ })
232
104
  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
-
240
- mac_addr = (mac.nil? || mac.empty?) ? SecureRandom.uuid : mac
241
- section_hash = @@ip_cache[section_name.to_sym]
242
105
 
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
106
+ return data if json_body['data']
256
107
  end
257
108
 
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)
109
+ def get_ipam_subnets(group_name)
110
+ group = get_ipam_group(group_name)
111
+ raise errors[:no_group] if group.nil?
112
+ subnets = @api_resource.get("sections/#{group[:id]}/subnets/")
113
+ json_body = JSON.parse(subnets.body)
114
+ return nil if json_body['data'].nil?
269
115
 
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
276
-
277
- temp_ip = new_ip
278
- retry_count += 1
279
- break if retry_count >= MAX_RETRIES
116
+ data = []
117
+ json_body['data'].each do |subnet|
118
+ data.push({
119
+ id: subnet['id'],
120
+ subnet: subnet['subnet'],
121
+ mask: subnet['mask'],
122
+ description: subnet['description']
123
+ })
280
124
  end
281
125
 
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
126
+ return data if json_body['data']
286
127
  end
287
128
 
288
- def increment_ip(ip)
289
- IPAddr.new(ip.to_s).succ.to_s
129
+ def ip_exists?(ip, subnet_id, _group_name)
130
+ ip = @api_resource.get("subnets/#{subnet_id}/addresses/#{ip}/")
131
+ json_body = JSON.parse(ip.body)
132
+ json_body['success']
290
133
  end
291
134
 
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)
135
+ def add_ip_to_subnet(ip, params)
136
+ data = { subnetId: params[:subnet_id], ip: ip, description: 'Address auto added by Foreman' }
137
+ subnet = @api_resource.post('addresses/', data.to_json)
138
+ json_body = JSON.parse(subnet.body)
139
+ return nil if json_body['code'] == 201
140
+ { error: 'Unable to add IP to External IPAM' }
294
141
  end
295
142
 
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
143
+ def delete_ip_from_subnet(ip, params)
144
+ subnet = @api_resource.delete("addresses/#{ip}/#{params[:subnet_id]}/")
145
+ json_body = JSON.parse(subnet.body)
146
+ return nil if json_body['success']
147
+ { error: 'Unable to delete IP from External IPAM' }
300
148
  end
301
149
 
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
- }
150
+ def get_next_ip(mac, cidr, group_name)
151
+ subnet = get_ipam_subnet(cidr, group_name)
152
+ raise errors[:no_subnet] if subnet.nil?
153
+ response = @api_resource.get("subnets/#{subnet[:id]}/first_free/")
154
+ json_body = JSON.parse(response.body)
155
+ return { error: json_body['message'] } if json_body['message']
156
+ ip = json_body['data']
157
+ cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
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
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