smart_proxy_ipam 0.0.22 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +391 -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 +47 -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 -283
- 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 -396
- 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 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
|
@@ -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/
|
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
|
-
|
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
|
49
|
-
subnet_cidr = subnet[
|
50
|
-
subnet_id = subnet[
|
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
|
-
|
65
|
-
response = get("subnets
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
108
|
-
|
109
|
-
json_body = JSON.parse(
|
110
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
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
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
82
|
+
data = {
|
83
|
+
id: json_body['data']['id'],
|
84
|
+
name: json_body['data']['name'],
|
85
|
+
description: json_body['data']['description']
|
86
|
+
}
|
177
87
|
|
178
|
-
|
179
|
-
!@token.nil?
|
88
|
+
return data if json_body['data']
|
180
89
|
end
|
181
90
|
|
182
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
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
|
289
|
-
|
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
|
293
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
313
|
-
|
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
|
324
|
-
|
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
|
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)
|
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
|
-
|
346
|
-
end
|
181
|
+
response.dig('data', 'token')
|
182
|
+
end
|
347
183
|
end
|
348
184
|
end
|