smart_proxy_ipam 0.0.21 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +31 -26
- data/bundler.d/ipam.rb +0 -1
- data/lib/smart_proxy_ipam.rb +2 -0
- data/lib/smart_proxy_ipam/api_resource.rb +54 -0
- data/lib/smart_proxy_ipam/dependency_injection.rb +9 -0
- data/lib/smart_proxy_ipam/ip_cache.rb +137 -0
- data/lib/smart_proxy_ipam/ipam.rb +11 -6
- data/lib/smart_proxy_ipam/ipam_api.rb +390 -0
- data/lib/smart_proxy_ipam/ipam_helper.rb +110 -0
- data/lib/smart_proxy_ipam/ipam_http_config.ru +2 -3
- data/lib/smart_proxy_ipam/ipam_validator.rb +48 -0
- data/lib/smart_proxy_ipam/netbox/netbox_client.rb +199 -0
- data/lib/smart_proxy_ipam/netbox/netbox_plugin.rb +17 -0
- data/lib/smart_proxy_ipam/phpipam/phpipam_client.rb +119 -279
- data/lib/smart_proxy_ipam/phpipam/phpipam_plugin.rb +17 -0
- data/lib/smart_proxy_ipam/version.rb +1 -2
- data/settings.d/externalipam.yml.example +1 -6
- data/settings.d/externalipam_netbox.yml.example +3 -0
- data/settings.d/externalipam_phpipam.yml.example +4 -0
- metadata +19 -11
- data/lib/smart_proxy_ipam/ipam_main.rb +0 -11
- data/lib/smart_proxy_ipam/phpipam/phpipam_api.rb +0 -393
- data/lib/smart_proxy_ipam/phpipam/phpipam_helper.rb +0 -66
@@ -0,0 +1,110 @@
|
|
1
|
+
# Module containing helper methods for use by all External IPAM provider implementations
|
2
|
+
module Proxy::Ipam::IpamHelper
|
3
|
+
include ::Proxy::Validations
|
4
|
+
|
5
|
+
MAX_IP_RETRIES = 5
|
6
|
+
ERRORS = {
|
7
|
+
cidr: "A 'cidr' parameter for the subnet must be provided(e.g. IPv4: 100.10.10.0/24, IPv6: 2001:db8:abcd:12::/124)",
|
8
|
+
mac: "A 'mac' address must be provided(e.g. 00:0a:95:9d:68:10)",
|
9
|
+
ip: "Missing 'ip' parameter. An IPv4 or IPv6 address must be provided(e.g. IPv4: 100.10.10.22, IPv6: 2001:db8:abcd:12::3)",
|
10
|
+
group_name: "A 'group_name' must be provided",
|
11
|
+
no_ip: 'IP address not found',
|
12
|
+
no_free_ips: 'No free addresses found',
|
13
|
+
no_connection: 'Unable to connect to External IPAM server',
|
14
|
+
no_group: 'Group not found in External IPAM',
|
15
|
+
no_groups: 'No groups found in External IPAM',
|
16
|
+
no_subnet: 'Subnet not found in External IPAM',
|
17
|
+
no_subnets_in_group: 'No subnets found in External IPAM group',
|
18
|
+
provider: "The IPAM provider must be specified(e.g. 'phpipam' or 'netbox')",
|
19
|
+
groups_not_supported: 'Groups are not supported',
|
20
|
+
add_ip: 'Error adding IP to External IPAM',
|
21
|
+
bad_mac: 'Mac address is invalid',
|
22
|
+
bad_ip: 'IP address is invalid',
|
23
|
+
bad_cidr: 'The network cidr is invalid'
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def provider
|
27
|
+
@provider ||=
|
28
|
+
begin
|
29
|
+
unless client.authenticated?
|
30
|
+
halt 500, { error: 'Invalid credentials for External IPAM' }.to_json
|
31
|
+
end
|
32
|
+
client
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Called when next available IP from External IPAM has been cached by another user/host, but
|
37
|
+
# not actually persisted in External IPAM yet. This method will increment the IP, up to
|
38
|
+
# MAX_IP_RETRIES times, and check if it is available in External IPAM each iteration. It
|
39
|
+
# will return the original IP(the 'ip' param) if no new IP's are found after MAX_IP_RETRIES
|
40
|
+
# iterations.
|
41
|
+
def find_new_ip(ip_cache, subnet_id, ip, mac, cidr, group_name)
|
42
|
+
found_ip = nil
|
43
|
+
temp_ip = ip
|
44
|
+
retry_count = 0
|
45
|
+
|
46
|
+
loop do
|
47
|
+
new_ip = increment_ip(temp_ip)
|
48
|
+
ipam_ip = ip_exists?(new_ip, subnet_id, group_name)
|
49
|
+
|
50
|
+
# If new IP doesn't exist in IPAM and not in the cache
|
51
|
+
if !ipam_ip && !ip_cache.ip_exists(new_ip, cidr, group_name)
|
52
|
+
found_ip = new_ip.to_s
|
53
|
+
ip_cache.add(found_ip, mac, cidr, group_name)
|
54
|
+
break
|
55
|
+
end
|
56
|
+
|
57
|
+
temp_ip = new_ip
|
58
|
+
retry_count += 1
|
59
|
+
break if retry_count >= MAX_IP_RETRIES
|
60
|
+
end
|
61
|
+
|
62
|
+
return ip if found_ip.nil?
|
63
|
+
|
64
|
+
found_ip
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks the cache for existing ip, and returns it if it exists. If not exists, it will
|
68
|
+
# find a new ip (using find_new_ip), and it is added to the cache.
|
69
|
+
def cache_next_ip(ip_cache, ip, mac, cidr, subnet_id, group_name)
|
70
|
+
group = group_name.nil? ? '' : group_name
|
71
|
+
ip_cache.set_group(group, {}) if ip_cache.get_group(group).nil?
|
72
|
+
subnet_hash = ip_cache.get_cidr(group, cidr)
|
73
|
+
next_ip = nil
|
74
|
+
|
75
|
+
if subnet_hash&.key?(mac.to_sym)
|
76
|
+
next_ip = ip_cache.get_ip(group, cidr, mac)
|
77
|
+
else
|
78
|
+
new_ip = ip
|
79
|
+
ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
|
80
|
+
|
81
|
+
if ip_not_in_cache
|
82
|
+
next_ip = new_ip.to_s
|
83
|
+
ip_cache.add(new_ip, mac, cidr, group)
|
84
|
+
else
|
85
|
+
next_ip = find_new_ip(ip_cache, subnet_id, new_ip, mac, cidr, group)
|
86
|
+
end
|
87
|
+
|
88
|
+
unless usable_ip(next_ip, cidr)
|
89
|
+
return { error: "No free addresses found in subnet #{cidr}. Some available ip's may be cached. Try again in #{@ip_cache.get_cleanup_interval} seconds after cache is cleared." }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
next_ip
|
94
|
+
end
|
95
|
+
|
96
|
+
def increment_ip(ip)
|
97
|
+
IPAddr.new(ip.to_s).succ.to_s
|
98
|
+
end
|
99
|
+
|
100
|
+
def usable_ip(ip, cidr)
|
101
|
+
network = IPAddr.new(cidr)
|
102
|
+
network.include?(IPAddr.new(ip)) && network.to_range.last != ip
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_request_group(params)
|
106
|
+
group = params[:group] ? URI.escape(URI.decode(params[:group])) : nil
|
107
|
+
halt 500, { error: errors[:groups_not_supported] }.to_json if group && !provider.groups_supported?
|
108
|
+
group
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
|
3
|
+
# Module containing validation methods for use by all External IPAM provider implementations
|
4
|
+
module Proxy::Ipam::IpamValidator
|
5
|
+
include ::Proxy::Validations
|
6
|
+
include Proxy::Ipam::IpamHelper
|
7
|
+
|
8
|
+
def validate_required_params!(required_params, params)
|
9
|
+
err = []
|
10
|
+
required_params.each do |param|
|
11
|
+
unless params[param.to_sym]
|
12
|
+
err.push errors[param.to_sym]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
raise Proxy::Validations::Error, err unless err.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_ip!(ip)
|
19
|
+
good_ip = ip =~ Regexp.union([Resolv::IPv4::Regex, Resolv::IPv6::Regex])
|
20
|
+
raise Proxy::Validations::Error, ERRORS[:bad_ip] if good_ip.nil?
|
21
|
+
ip
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_cidr!(address, prefix)
|
25
|
+
cidr = "#{address}/#{prefix}"
|
26
|
+
network = IPAddr.new(cidr).to_s
|
27
|
+
if IPAddr.new(cidr).to_s != IPAddr.new(address).to_s
|
28
|
+
raise Proxy::Validations::Error, "Network address #{address} should be #{network} with prefix #{prefix}"
|
29
|
+
end
|
30
|
+
cidr
|
31
|
+
rescue IPAddr::Error => e
|
32
|
+
raise Proxy::Validations::Error, e.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_ip_in_cidr!(ip, cidr)
|
36
|
+
unless IPAddr.new(cidr).include?(IPAddr.new(ip))
|
37
|
+
raise Proxy::Validations::Error.new, "IP #{ip} is not in #{cidr}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_mac!(mac)
|
42
|
+
raise Proxy::Validations::Error.new, ERRORS[:mac] if mac.nil? || mac.empty?
|
43
|
+
unless mac.match(/^([0-9a-fA-F]{2}[:]){5}[0-9a-fA-F]{2}$/i)
|
44
|
+
raise Proxy::Validations::Error.new, ERRORS[:bad_mac]
|
45
|
+
end
|
46
|
+
mac
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'sinatra'
|
6
|
+
require 'smart_proxy_ipam/ipam'
|
7
|
+
require 'smart_proxy_ipam/ipam_helper'
|
8
|
+
require 'smart_proxy_ipam/ipam_validator'
|
9
|
+
require 'smart_proxy_ipam/api_resource'
|
10
|
+
require 'smart_proxy_ipam/ip_cache'
|
11
|
+
|
12
|
+
module Proxy::Netbox
|
13
|
+
# Implementation class for External IPAM provider Netbox
|
14
|
+
class NetboxClient
|
15
|
+
include Proxy::Log
|
16
|
+
include Proxy::Ipam::IpamHelper
|
17
|
+
include Proxy::Ipam::IpamValidator
|
18
|
+
|
19
|
+
def initialize(conf)
|
20
|
+
@api_base = "#{conf[:url]}/api/"
|
21
|
+
@token = conf[:token]
|
22
|
+
@api_resource = Proxy::Ipam::ApiResource.new(api_base: @api_base, token: "Token #{@token}")
|
23
|
+
@ip_cache = Proxy::Ipam::IpCache.instance
|
24
|
+
@ip_cache.set_provider_name('netbox')
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_ipam_subnet(cidr, group_name = nil)
|
28
|
+
if group_name.nil? || group_name.empty?
|
29
|
+
get_ipam_subnet_by_cidr(cidr)
|
30
|
+
else
|
31
|
+
group_id = get_group_id(group_name)
|
32
|
+
get_ipam_subnet_by_group(cidr, group_id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_ipam_subnet_by_group(cidr, group_id)
|
37
|
+
params = URI.encode_www_form({ status: 'active', prefix: cidr, vrf_id: group_id })
|
38
|
+
response = @api_resource.get("ipam/prefixes/?#{params}")
|
39
|
+
json_body = JSON.parse(response.body)
|
40
|
+
return nil if json_body['count'].zero?
|
41
|
+
subnet = subnet_from_result(json_body['results'][0])
|
42
|
+
return subnet if json_body['results']
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_ipam_subnet_by_cidr(cidr)
|
46
|
+
params = URI.encode_www_form({ status: 'active', prefix: cidr })
|
47
|
+
response = @api_resource.get("ipam/prefixes/?#{params}")
|
48
|
+
json_body = JSON.parse(response.body)
|
49
|
+
return nil if json_body['count'].zero?
|
50
|
+
subnet = subnet_from_result(json_body['results'][0])
|
51
|
+
return subnet if json_body['results']
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_ipam_groups
|
55
|
+
response = @api_resource.get('ipam/vrfs/')
|
56
|
+
json_body = JSON.parse(response.body)
|
57
|
+
groups = []
|
58
|
+
|
59
|
+
return groups if json_body['count'].zero?
|
60
|
+
|
61
|
+
json_body['results'].each do |group|
|
62
|
+
groups.push({
|
63
|
+
name: group['name'],
|
64
|
+
description: group['description']
|
65
|
+
})
|
66
|
+
end
|
67
|
+
|
68
|
+
groups
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_ipam_group(group_name)
|
72
|
+
raise ERRORS[:groups_not_supported] unless groups_supported?
|
73
|
+
# TODO: Fix encoding of params in a common way for all providers
|
74
|
+
params = URI.encode_www_form({ name: URI.decode(group_name) })
|
75
|
+
response = @api_resource.get("ipam/vrfs/?#{params}")
|
76
|
+
json_body = JSON.parse(response.body)
|
77
|
+
return nil if json_body['count'].zero?
|
78
|
+
|
79
|
+
group = {
|
80
|
+
id: json_body['results'][0]['id'],
|
81
|
+
name: json_body['results'][0]['name'],
|
82
|
+
description: json_body['results'][0]['description']
|
83
|
+
}
|
84
|
+
|
85
|
+
return group if json_body['results']
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_group_id(group_name)
|
89
|
+
return nil if group_name.nil? || group_name.empty?
|
90
|
+
group = get_ipam_group(group_name)
|
91
|
+
raise ERRORS[:no_group] if group.nil?
|
92
|
+
group[:id]
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_ipam_subnets(group_name)
|
96
|
+
if group_name.nil?
|
97
|
+
params = URI.encode_www_form({ status: 'active' })
|
98
|
+
else
|
99
|
+
group_id = get_group_id(group_name)
|
100
|
+
params = URI.encode_www_form({ status: 'active', vrf_id: group_id })
|
101
|
+
end
|
102
|
+
|
103
|
+
response = @api_resource.get("ipam/prefixes/?#{params}")
|
104
|
+
json_body = JSON.parse(response.body)
|
105
|
+
return nil if json_body['count'].zero?
|
106
|
+
subnets = []
|
107
|
+
|
108
|
+
json_body['results'].each do |subnet|
|
109
|
+
subnets.push({
|
110
|
+
subnet: subnet['prefix'].split('/').first,
|
111
|
+
mask: subnet['prefix'].split('/').last,
|
112
|
+
description: subnet['description'],
|
113
|
+
id: subnet['id']
|
114
|
+
})
|
115
|
+
end
|
116
|
+
|
117
|
+
return subnets if json_body['results']
|
118
|
+
end
|
119
|
+
|
120
|
+
def ip_exists?(ip, subnet_id, group_name)
|
121
|
+
group_id = get_group_id(group_name)
|
122
|
+
url = "ipam/ip-addresses/?#{URI.encode_www_form({ address: ip })}"
|
123
|
+
url += "&#{URI.encode_www_form({ prefix_id: subnet_id })}" unless subnet_id.nil?
|
124
|
+
url += "&#{URI.encode_www_form({ vrf_id: group_id })}" unless group_id.nil?
|
125
|
+
response = @api_resource.get(url)
|
126
|
+
json_body = JSON.parse(response.body)
|
127
|
+
return false if json_body['count'].zero?
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_ip_to_subnet(ip, params)
|
132
|
+
desc = 'Address auto added by Foreman'
|
133
|
+
address = "#{ip}/#{params[:cidr].split('/').last}"
|
134
|
+
group_name = params[:group_name]
|
135
|
+
|
136
|
+
if group_name.nil? || group_name.empty?
|
137
|
+
data = { address: address, nat_outside: 0, description: desc }
|
138
|
+
else
|
139
|
+
group_id = get_group_id(group_name)
|
140
|
+
data = { vrf: group_id, address: address, nat_outside: 0, description: desc }
|
141
|
+
end
|
142
|
+
|
143
|
+
response = @api_resource.post('ipam/ip-addresses/', data.to_json)
|
144
|
+
return nil if response.code == '201'
|
145
|
+
{ error: "Unable to add #{address} in External IPAM server" }
|
146
|
+
end
|
147
|
+
|
148
|
+
def delete_ip_from_subnet(ip, params)
|
149
|
+
group_name = params[:group_name]
|
150
|
+
|
151
|
+
if group_name.nil? || group_name.empty?
|
152
|
+
params = URI.encode_www_form({ address: ip })
|
153
|
+
else
|
154
|
+
group_id = get_group_id(group_name)
|
155
|
+
params = URI.encode_www_form({ address: ip, vrf_id: group_id })
|
156
|
+
end
|
157
|
+
|
158
|
+
response = @api_resource.get("ipam/ip-addresses/?#{params}")
|
159
|
+
json_body = JSON.parse(response.body)
|
160
|
+
|
161
|
+
return { error: ERRORS[:no_ip] } if json_body['count'].zero?
|
162
|
+
|
163
|
+
address_id = json_body['results'][0]['id']
|
164
|
+
response = @api_resource.delete("ipam/ip-addresses/#{address_id}/")
|
165
|
+
return nil if response.code == '204'
|
166
|
+
{ error: "Unable to delete #{ip} in External IPAM server" }
|
167
|
+
end
|
168
|
+
|
169
|
+
def get_next_ip(mac, cidr, group_name)
|
170
|
+
subnet = get_ipam_subnet(cidr, group_name)
|
171
|
+
raise ERRORS[:no_subnet] if subnet.nil?
|
172
|
+
response = @api_resource.get("ipam/prefixes/#{subnet[:id]}/available-ips/?limit=1")
|
173
|
+
json_body = JSON.parse(response.body)
|
174
|
+
return nil if json_body.empty?
|
175
|
+
ip = json_body[0]['address'].split('/').first
|
176
|
+
next_ip = cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
|
177
|
+
{ data: next_ip }
|
178
|
+
end
|
179
|
+
|
180
|
+
def groups_supported?
|
181
|
+
true
|
182
|
+
end
|
183
|
+
|
184
|
+
def authenticated?
|
185
|
+
!@token.nil?
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def subnet_from_result(result)
|
191
|
+
{
|
192
|
+
subnet: result['prefix'].split('/').first,
|
193
|
+
mask: result['prefix'].split('/').last,
|
194
|
+
description: result['description'],
|
195
|
+
id: result['id']
|
196
|
+
}
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Proxy::Netbox
|
2
|
+
class Plugin < ::Proxy::Provider
|
3
|
+
plugin :externalipam_netbox, Proxy::Ipam::VERSION
|
4
|
+
|
5
|
+
requires :externalipam, Proxy::Ipam::VERSION
|
6
|
+
validate :url, url: true
|
7
|
+
validate_presence :token
|
8
|
+
|
9
|
+
load_classes(proc do
|
10
|
+
require 'smart_proxy_ipam/netbox/netbox_client'
|
11
|
+
end)
|
12
|
+
|
13
|
+
load_dependency_injection_wirings(proc do |container_instance, settings|
|
14
|
+
container_instance.dependency :externalipam_client, -> { ::Proxy::Netbox::NetboxClient.new(settings) }
|
15
|
+
end)
|
16
|
+
end
|
17
|
+
end
|
@@ -1,344 +1,184 @@
|
|
1
1
|
require 'yaml'
|
2
|
-
require 'json'
|
2
|
+
require 'json'
|
3
3
|
require 'net/http'
|
4
|
-
require 'monitor'
|
5
|
-
require 'concurrent'
|
6
|
-
require 'time'
|
7
4
|
require 'uri'
|
5
|
+
require 'sinatra'
|
8
6
|
require 'smart_proxy_ipam/ipam'
|
9
|
-
require 'smart_proxy_ipam/
|
10
|
-
require 'smart_proxy_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
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
|
16
|
+
include Proxy::Ipam::IpamHelper
|
17
|
+
include Proxy::Ipam::IpamValidator
|
16
18
|
|
17
|
-
|
18
|
-
|
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 =
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
33
|
-
if
|
34
|
-
|
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
|
-
|
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
|
41
|
-
|
42
|
-
|
37
|
+
def get_ipam_subnet_by_group(cidr, group_id)
|
38
|
+
subnets = get_ipam_subnets(group_id)
|
39
|
+
return nil if subnets.nil?
|
43
40
|
subnet_id = nil
|
44
41
|
|
45
|
-
subnets
|
46
|
-
subnet_cidr = subnet[
|
47
|
-
subnet_id = subnet[
|
42
|
+
subnets.each do |subnet|
|
43
|
+
subnet_cidr = "#{subnet[:subnet]}/#{subnet[:mask]}"
|
44
|
+
subnet_id = subnet[:id] if subnet_cidr == cidr
|
48
45
|
end
|
49
|
-
|
50
|
-
return {:code => 404, :error => "No subnet #{cidr} found in section #{URI.unescape(section_name)}"}.to_json if subnet_id.nil?
|
51
|
-
|
52
|
-
response = get("subnets/#{subnet_id.to_s}/")
|
53
|
-
json_body = JSON.parse(response.body)
|
54
|
-
json_body['data'] = filter_hash(json_body['data'], [:id, :subnet, :mask, :description]) if json_body['data']
|
55
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
56
|
-
response.body = json_body.to_json
|
57
|
-
response.header['Content-Length'] = json_body.to_s.length
|
58
|
-
response.body
|
59
|
-
end
|
60
|
-
|
61
|
-
def get_subnet_by_cidr(cidr)
|
62
|
-
response = get("subnets/cidr/#{cidr.to_s}")
|
63
|
-
json_body = JSON.parse(response.body)
|
64
|
-
json_body['data'] = filter_fields(json_body, [:id, :subnet, :description, :mask])[0]
|
65
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
66
|
-
response.body = json_body.to_json
|
67
|
-
response.header['Content-Length'] = json_body.to_s.length
|
68
|
-
response.body
|
69
|
-
end
|
70
46
|
|
71
|
-
|
72
|
-
response = get("
|
47
|
+
return nil if subnet_id.nil?
|
48
|
+
response = @api_resource.get("subnets/#{subnet_id}/")
|
73
49
|
json_body = JSON.parse(response.body)
|
74
|
-
json_body['data'] = filter_hash(json_body['data'], [:id, :name, :description]) if json_body['data']
|
75
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
76
|
-
response.body = json_body.to_json
|
77
|
-
response.header['Content-Length'] = json_body.to_s.length
|
78
|
-
response.body
|
79
|
-
end
|
80
50
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
response.header['Content-Length'] = json_body.to_s.length
|
88
|
-
response.body
|
89
|
-
end
|
51
|
+
data = {
|
52
|
+
id: json_body['data']['id'],
|
53
|
+
subnet: json_body['data']['subnet'],
|
54
|
+
mask: json_body['data']['mask'],
|
55
|
+
description: json_body['data']['description']
|
56
|
+
}
|
90
57
|
|
91
|
-
|
92
|
-
response = get("sections/#{section_id}/subnets/")
|
93
|
-
fields = [:subnet, :mask, :description]
|
94
|
-
fields.push(:id) if include_id
|
95
|
-
json_body = JSON.parse(response.body)
|
96
|
-
json_body['data'] = filter_fields(json_body, fields) if json_body['data']
|
97
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
98
|
-
response.body = json_body.to_json
|
99
|
-
response.header['Content-Length'] = json_body.to_s.length
|
100
|
-
response.body
|
58
|
+
return data if json_body['data']
|
101
59
|
end
|
102
60
|
|
103
|
-
def
|
104
|
-
|
105
|
-
json_body = JSON.parse(
|
106
|
-
|
107
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
108
|
-
response.body = json_body.to_json
|
109
|
-
response.header['Content-Length'] = json_body.to_s.length
|
110
|
-
response.body
|
111
|
-
end
|
61
|
+
def get_ipam_subnet_by_cidr(cidr)
|
62
|
+
subnet = @api_resource.get("subnets/cidr/#{cidr}")
|
63
|
+
json_body = JSON.parse(subnet.body)
|
64
|
+
return nil if json_body['data'].nil?
|
112
65
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
response.header['Content-Length'] = json_body.to_s.length
|
120
|
-
response.body
|
121
|
-
end
|
66
|
+
data = {
|
67
|
+
id: json_body['data'][0]['id'],
|
68
|
+
subnet: json_body['data'][0]['subnet'],
|
69
|
+
mask: json_body['data'][0]['mask'],
|
70
|
+
description: json_body['data'][0]['description']
|
71
|
+
}
|
122
72
|
|
123
|
-
|
124
|
-
response = delete("addresses/#{ip}/#{subnet_id.to_s}/")
|
125
|
-
json_body = JSON.parse(response.body)
|
126
|
-
json_body = filter_hash(json_body, [:code, :error, :message])
|
127
|
-
response.body = json_body.to_json
|
128
|
-
response.header['Content-Length'] = json_body.to_s.length
|
129
|
-
response.body
|
73
|
+
return data if json_body['data']
|
130
74
|
end
|
131
75
|
|
132
|
-
def
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
subnet_hash = @@ip_cache[section.to_sym][cidr.to_sym]
|
138
|
-
|
139
|
-
return {:code => json_body['code'], :error => json_body['message']}.to_json if json_body['message']
|
140
|
-
|
141
|
-
if subnet_hash && subnet_hash.key?(mac.to_sym)
|
142
|
-
json_body['data'] = @@ip_cache[section_name.to_sym][cidr.to_sym][mac.to_sym][:ip]
|
143
|
-
else
|
144
|
-
next_ip = nil
|
145
|
-
new_ip = json_body['data']
|
146
|
-
ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
|
147
|
-
|
148
|
-
if ip_not_in_cache
|
149
|
-
next_ip = new_ip.to_s
|
150
|
-
add_ip_to_cache(new_ip, mac, cidr, section)
|
151
|
-
else
|
152
|
-
next_ip = find_new_ip(subnet_id, new_ip, mac, cidr, section)
|
153
|
-
end
|
76
|
+
def get_ipam_group(group_name)
|
77
|
+
return nil if group_name.nil?
|
78
|
+
group = @api_resource.get("sections/#{group_name}/")
|
79
|
+
json_body = JSON.parse(group.body)
|
80
|
+
raise ERRORS[:no_group] if json_body['data'].nil?
|
154
81
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
json_body['data']
|
159
|
-
|
160
|
-
|
161
|
-
json_body = {:code => json_body['code'], :data => json_body['data']}
|
162
|
-
|
163
|
-
response.body = json_body.to_json
|
164
|
-
response.header['Content-Length'] = json_body.to_s.length
|
165
|
-
response.body
|
166
|
-
end
|
167
|
-
|
168
|
-
def start_cleanup_task
|
169
|
-
logger.info("Starting allocated ip address maintenance (used by get_next_ip call).")
|
170
|
-
@@timer_task = Concurrent::TimerTask.new(:execution_interval => DEFAULT_CLEANUP_INTERVAL) { init_cache }
|
171
|
-
@@timer_task.execute
|
172
|
-
end
|
82
|
+
data = {
|
83
|
+
id: json_body['data']['id'],
|
84
|
+
name: json_body['data']['name'],
|
85
|
+
description: json_body['data']['description']
|
86
|
+
}
|
173
87
|
|
174
|
-
|
175
|
-
!@token.nil?
|
88
|
+
return data if json_body['data']
|
176
89
|
end
|
177
90
|
|
178
|
-
|
91
|
+
def get_ipam_groups
|
92
|
+
groups = @api_resource.get('sections/')
|
93
|
+
json_body = JSON.parse(groups.body)
|
94
|
+
return [] if json_body['data'].nil?
|
179
95
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
# 2). UUID (Used when Mac Address not specified)
|
188
|
-
#
|
189
|
-
# {
|
190
|
-
# "": {
|
191
|
-
# "100.55.55.0/24":{
|
192
|
-
# "00:0a:95:9d:68:10": {"ip": "100.55.55.1", "timestamp": "2019-09-17 12:03:43 -D400"},
|
193
|
-
# "906d8bdc-dcc0-4b59-92cb-665935e21662": {"ip": "100.55.55.2", "timestamp": "2019-09-17 11:43:22 -D400"}
|
194
|
-
# },
|
195
|
-
# },
|
196
|
-
# "IPAM Group Name": {
|
197
|
-
# "123.11.33.0/24":{
|
198
|
-
# "00:0a:95:9d:68:33": {"ip": "123.11.33.1", "timestamp": "2019-09-17 12:04:43 -0400"},
|
199
|
-
# "00:0a:95:9d:68:34": {"ip": "123.11.33.2", "timestamp": "2019-09-17 12:05:48 -0400"},
|
200
|
-
# "00:0a:95:9d:68:35": {"ip": "123.11.33.3", "timestamp:: "2019-09-17 12:06:50 -0400"}
|
201
|
-
# }
|
202
|
-
# },
|
203
|
-
# "Another IPAM Group": {
|
204
|
-
# "185.45.39.0/24":{
|
205
|
-
# "00:0a:95:9d:68:55": {"ip": "185.45.39.1", "timestamp": "2019-09-17 12:04:43 -0400"},
|
206
|
-
# "00:0a:95:9d:68:56": {"ip": "185.45.39.2", "timestamp": "2019-09-17 12:05:48 -0400"}
|
207
|
-
# }
|
208
|
-
# }
|
209
|
-
# }
|
210
|
-
def init_cache
|
211
|
-
@@m.synchronize do
|
212
|
-
if @@ip_cache and not @@ip_cache.empty?
|
213
|
-
logger.debug("Processing ip cache.")
|
214
|
-
@@ip_cache.each do |section, subnets|
|
215
|
-
subnets.each do |cidr, macs|
|
216
|
-
macs.each do |mac, ip|
|
217
|
-
if Time.now - Time.parse(ip[:timestamp]) > DEFAULT_CLEANUP_INTERVAL
|
218
|
-
@@ip_cache[section][cidr].delete(mac)
|
219
|
-
end
|
220
|
-
end
|
221
|
-
@@ip_cache[section].delete(cidr) if @@ip_cache[section][cidr].nil? or @@ip_cache[section][cidr].empty?
|
222
|
-
end
|
223
|
-
end
|
224
|
-
else
|
225
|
-
logger.debug("Clearing ip cache.")
|
226
|
-
@@ip_cache = {:"" => {}}
|
227
|
-
end
|
96
|
+
data = []
|
97
|
+
json_body['data'].each do |group|
|
98
|
+
data.push({
|
99
|
+
id: group['id'],
|
100
|
+
name: group['name'],
|
101
|
+
description: group['description']
|
102
|
+
})
|
228
103
|
end
|
229
|
-
end
|
230
|
-
|
231
|
-
def add_ip_to_cache(ip, mac, cidr, section_name)
|
232
|
-
logger.debug("Adding IP #{ip} to cache for subnet #{cidr} in section #{section_name}")
|
233
|
-
@@m.synchronize do
|
234
|
-
# Clear cache data which has the same mac and ip with the new one
|
235
|
-
|
236
|
-
mac_addr = (mac.nil? || mac.empty?) ? SecureRandom.uuid : mac
|
237
|
-
section_hash = @@ip_cache[section_name.to_sym]
|
238
104
|
|
239
|
-
|
240
|
-
if values.keys.include? mac_addr.to_sym
|
241
|
-
@@ip_cache[section_name.to_sym][key].delete(mac_addr.to_sym)
|
242
|
-
end
|
243
|
-
@@ip_cache[section_name.to_sym].delete(key) if @@ip_cache[section_name.to_sym][key].nil? or @@ip_cache[section_name.to_sym][key].empty?
|
244
|
-
end
|
245
|
-
|
246
|
-
if section_hash.key?(cidr.to_sym)
|
247
|
-
@@ip_cache[section_name.to_sym][cidr.to_sym][mac_addr.to_sym] = {:ip => ip.to_s, :timestamp => Time.now.to_s}
|
248
|
-
else
|
249
|
-
@@ip_cache = @@ip_cache.merge({section_name.to_sym => {cidr.to_sym => {mac_addr.to_sym => {:ip => ip.to_s, :timestamp => Time.now.to_s}}}})
|
250
|
-
end
|
251
|
-
end
|
105
|
+
return data if json_body['data']
|
252
106
|
end
|
253
107
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
retry_count = 0
|
261
|
-
|
262
|
-
loop do
|
263
|
-
new_ip = increment_ip(temp_ip)
|
264
|
-
verify_ip = JSON.parse(ip_exists(new_ip, subnet_id))
|
108
|
+
def get_ipam_subnets(group_name)
|
109
|
+
group = get_ipam_group(group_name)
|
110
|
+
raise ERRORS[:no_group] if group.nil?
|
111
|
+
subnets = @api_resource.get("sections/#{group[:id]}/subnets/")
|
112
|
+
json_body = JSON.parse(subnets.body)
|
113
|
+
return nil if json_body['data'].nil?
|
265
114
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
retry_count += 1
|
275
|
-
break if retry_count >= MAX_RETRIES
|
115
|
+
data = []
|
116
|
+
json_body['data'].each do |subnet|
|
117
|
+
data.push({
|
118
|
+
id: subnet['id'],
|
119
|
+
subnet: subnet['subnet'],
|
120
|
+
mask: subnet['mask'],
|
121
|
+
description: subnet['description']
|
122
|
+
})
|
276
123
|
end
|
277
124
|
|
278
|
-
|
279
|
-
return ip if found_ip.nil?
|
280
|
-
|
281
|
-
found_ip
|
125
|
+
return data if json_body['data']
|
282
126
|
end
|
283
127
|
|
284
|
-
def
|
285
|
-
|
128
|
+
def ip_exists?(ip, subnet_id, _group_name)
|
129
|
+
ip = @api_resource.get("subnets/#{subnet_id}/addresses/#{ip}/")
|
130
|
+
json_body = JSON.parse(ip.body)
|
131
|
+
json_body['success']
|
286
132
|
end
|
287
133
|
|
288
|
-
def
|
289
|
-
|
134
|
+
def add_ip_to_subnet(ip, params)
|
135
|
+
data = { subnetId: params[:subnet_id], ip: ip, description: 'Address auto added by Foreman' }
|
136
|
+
subnet = @api_resource.post('addresses/', data.to_json)
|
137
|
+
json_body = JSON.parse(subnet.body)
|
138
|
+
return nil if json_body['code'] == 201
|
139
|
+
{ error: 'Unable to add IP to External IPAM' }
|
290
140
|
end
|
291
141
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
142
|
+
def delete_ip_from_subnet(ip, params)
|
143
|
+
subnet = @api_resource.delete("addresses/#{ip}/#{params[:subnet_id]}/")
|
144
|
+
json_body = JSON.parse(subnet.body)
|
145
|
+
return nil if json_body['success']
|
146
|
+
{ error: 'Unable to delete IP from External IPAM' }
|
296
147
|
end
|
297
148
|
|
298
|
-
def
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
149
|
+
def get_next_ip(mac, cidr, group_name)
|
150
|
+
subnet = get_ipam_subnet(cidr, group_name)
|
151
|
+
raise ERRORS[:no_subnet] if subnet.nil?
|
152
|
+
response = @api_resource.get("subnets/#{subnet[:id]}/first_free/")
|
153
|
+
json_body = JSON.parse(response.body)
|
154
|
+
return { error: json_body['message'] } if json_body['message']
|
155
|
+
ip = json_body['data']
|
156
|
+
next_ip = cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
|
157
|
+
{ data: next_ip }
|
306
158
|
end
|
307
159
|
|
308
|
-
def
|
309
|
-
|
310
|
-
uri.query = URI.encode_www_form(body) if body
|
311
|
-
request = Net::HTTP::Delete.new(uri)
|
312
|
-
request['token'] = @token
|
313
|
-
|
314
|
-
Net::HTTP.start(uri.hostname, uri.port) {|http|
|
315
|
-
http.request(request)
|
316
|
-
}
|
160
|
+
def groups_supported?
|
161
|
+
true
|
317
162
|
end
|
318
163
|
|
319
|
-
def
|
320
|
-
|
321
|
-
uri.query = URI.encode_www_form(body) if body
|
322
|
-
request = Net::HTTP::Post.new(uri)
|
323
|
-
request['token'] = @token
|
324
|
-
|
325
|
-
Net::HTTP.start(uri.hostname, uri.port) {|http|
|
326
|
-
http.request(request)
|
327
|
-
}
|
164
|
+
def authenticated?
|
165
|
+
!@token.nil?
|
328
166
|
end
|
329
167
|
|
168
|
+
private
|
169
|
+
|
330
170
|
def authenticate
|
331
|
-
auth_uri = URI(@api_base
|
171
|
+
auth_uri = URI("#{@api_base}/user/")
|
332
172
|
request = Net::HTTP::Post.new(auth_uri)
|
333
173
|
request.basic_auth @conf[:user], @conf[:password]
|
334
174
|
|
335
|
-
response = Net::HTTP.start(auth_uri.hostname, auth_uri.port)
|
175
|
+
response = Net::HTTP.start(auth_uri.hostname, auth_uri.port, use_ssl: auth_uri.scheme == 'https') do |http|
|
336
176
|
http.request(request)
|
337
|
-
|
177
|
+
end
|
338
178
|
|
339
179
|
response = JSON.parse(response.body)
|
340
180
|
logger.warn(response['message']) if response['message']
|
341
|
-
|
342
|
-
end
|
181
|
+
response.dig('data', 'token')
|
182
|
+
end
|
343
183
|
end
|
344
184
|
end
|