smart_proxy_ipam 0.0.20 → 0.1.2
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 +198 -0
- data/lib/smart_proxy_ipam/netbox/netbox_plugin.rb +17 -0
- data/lib/smart_proxy_ipam/phpipam/phpipam_client.rb +120 -277
- 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 -377
- data/lib/smart_proxy_ipam/phpipam/phpipam_helper.rb +0 -62
@@ -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(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,198 @@
|
|
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('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
|
+
params = URI.encode_www_form({ name: group_name })
|
74
|
+
response = @api_resource.get("ipam/vrfs/?#{params}")
|
75
|
+
json_body = JSON.parse(response.body)
|
76
|
+
return nil if json_body['count'].zero?
|
77
|
+
|
78
|
+
group = {
|
79
|
+
id: json_body['results'][0]['id'],
|
80
|
+
name: json_body['results'][0]['name'],
|
81
|
+
description: json_body['results'][0]['description']
|
82
|
+
}
|
83
|
+
|
84
|
+
return group if json_body['results']
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_group_id(group_name)
|
88
|
+
return nil if group_name.nil? || group_name.empty?
|
89
|
+
group = get_ipam_group(group_name)
|
90
|
+
raise ERRORS[:no_group] if group.nil?
|
91
|
+
group[:id]
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_ipam_subnets(group_name)
|
95
|
+
if group_name.nil?
|
96
|
+
params = URI.encode_www_form({ status: 'active' })
|
97
|
+
else
|
98
|
+
group_id = get_group_id(group_name)
|
99
|
+
params = URI.encode_www_form({ status: 'active', vrf_id: group_id })
|
100
|
+
end
|
101
|
+
|
102
|
+
response = @api_resource.get("ipam/prefixes/?#{params}")
|
103
|
+
json_body = JSON.parse(response.body)
|
104
|
+
return nil if json_body['count'].zero?
|
105
|
+
subnets = []
|
106
|
+
|
107
|
+
json_body['results'].each do |subnet|
|
108
|
+
subnets.push({
|
109
|
+
subnet: subnet['prefix'].split('/').first,
|
110
|
+
mask: subnet['prefix'].split('/').last,
|
111
|
+
description: subnet['description'],
|
112
|
+
id: subnet['id']
|
113
|
+
})
|
114
|
+
end
|
115
|
+
|
116
|
+
return subnets if json_body['results']
|
117
|
+
end
|
118
|
+
|
119
|
+
def ip_exists?(ip, subnet_id, group_name)
|
120
|
+
group_id = get_group_id(group_name)
|
121
|
+
url = "ipam/ip-addresses/?#{URI.encode_www_form({ address: ip })}"
|
122
|
+
url += "&#{URI.encode_www_form({ prefix_id: subnet_id })}" unless subnet_id.nil?
|
123
|
+
url += "&#{URI.encode_www_form({ vrf_id: group_id })}" unless group_id.nil?
|
124
|
+
response = @api_resource.get(url)
|
125
|
+
json_body = JSON.parse(response.body)
|
126
|
+
return false if json_body['count'].zero?
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_ip_to_subnet(ip, params)
|
131
|
+
desc = 'Address auto added by Foreman'
|
132
|
+
address = "#{ip}/#{params[:cidr].split('/').last}"
|
133
|
+
group_name = params[:group_name]
|
134
|
+
|
135
|
+
if group_name.nil? || group_name.empty?
|
136
|
+
data = { address: address, nat_outside: 0, description: desc }
|
137
|
+
else
|
138
|
+
group_id = get_group_id(group_name)
|
139
|
+
data = { vrf: group_id, address: address, nat_outside: 0, description: desc }
|
140
|
+
end
|
141
|
+
|
142
|
+
response = @api_resource.post('ipam/ip-addresses/', data.to_json)
|
143
|
+
return nil if response.code == '201'
|
144
|
+
{ error: "Unable to add #{address} in External IPAM server" }
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete_ip_from_subnet(ip, params)
|
148
|
+
group_name = params[:group_name]
|
149
|
+
|
150
|
+
if group_name.nil? || group_name.empty?
|
151
|
+
params = URI.encode_www_form({ address: ip })
|
152
|
+
else
|
153
|
+
group_id = get_group_id(group_name)
|
154
|
+
params = URI.encode_www_form({ address: ip, vrf_id: group_id })
|
155
|
+
end
|
156
|
+
|
157
|
+
response = @api_resource.get("ipam/ip-addresses/?#{params}")
|
158
|
+
json_body = JSON.parse(response.body)
|
159
|
+
|
160
|
+
return { error: ERRORS[:no_ip] } if json_body['count'].zero?
|
161
|
+
|
162
|
+
address_id = json_body['results'][0]['id']
|
163
|
+
response = @api_resource.delete("ipam/ip-addresses/#{address_id}/")
|
164
|
+
return nil if response.code == '204'
|
165
|
+
{ error: "Unable to delete #{ip} in External IPAM server" }
|
166
|
+
end
|
167
|
+
|
168
|
+
def get_next_ip(mac, cidr, group_name)
|
169
|
+
subnet = get_ipam_subnet(cidr, group_name)
|
170
|
+
raise ERRORS[:no_subnet] if subnet.nil?
|
171
|
+
response = @api_resource.get("ipam/prefixes/#{subnet[:id]}/available-ips/?limit=1")
|
172
|
+
json_body = JSON.parse(response.body)
|
173
|
+
return nil if json_body.empty?
|
174
|
+
ip = json_body[0]['address'].split('/').first
|
175
|
+
next_ip = cache_next_ip(@ip_cache, ip, mac, cidr, subnet[:id], group_name)
|
176
|
+
{ data: next_ip }
|
177
|
+
end
|
178
|
+
|
179
|
+
def groups_supported?
|
180
|
+
true
|
181
|
+
end
|
182
|
+
|
183
|
+
def authenticated?
|
184
|
+
!@token.nil?
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def subnet_from_result(result)
|
190
|
+
{
|
191
|
+
subnet: result['prefix'].split('/').first,
|
192
|
+
mask: result['prefix'].split('/').last,
|
193
|
+
description: result['description'],
|
194
|
+
id: result['id']
|
195
|
+
}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
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,341 +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
|
-
|
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('phpipam')
|
29
26
|
end
|
30
27
|
|
31
|
-
def
|
32
|
-
if
|
33
|
-
|
28
|
+
def get_ipam_subnet(cidr, group_name = nil)
|
29
|
+
if group_name.nil? || group_name.empty?
|
30
|
+
get_ipam_subnet_by_cidr(cidr)
|
34
31
|
else
|
35
|
-
|
32
|
+
group = get_ipam_group(group_name)
|
33
|
+
get_ipam_subnet_by_group(cidr, group[:id])
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
37
|
+
def get_ipam_subnet_by_group(cidr, group_id)
|
38
|
+
subnets = get_ipam_subnets(group_id)
|
39
|
+
return nil if subnets.nil?
|
42
40
|
subnet_id = nil
|
43
41
|
|
44
|
-
subnets
|
45
|
-
subnet_cidr = subnet[
|
46
|
-
subnet_id = subnet[
|
42
|
+
subnets.each do |subnet|
|
43
|
+
subnet_cidr = "#{subnet[:subnet]}/#{subnet[:mask]}"
|
44
|
+
subnet_id = subnet[:id] if subnet_cidr == cidr
|
47
45
|
end
|
48
|
-
|
49
|
-
return {:code => 404, :error => "No subnet #{cidr} found in section #{URI.unescape(section_name)}"}.to_json if subnet_id.nil?
|
50
|
-
|
51
|
-
response = get("subnets/#{subnet_id.to_s}/")
|
52
|
-
json_body = JSON.parse(response.body)
|
53
|
-
json_body['data'] = filter_hash(json_body['data'], [:id, :subnet, :mask, :description]) if json_body['data']
|
54
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
55
|
-
response.body = json_body.to_json
|
56
|
-
response.header['Content-Length'] = json_body.to_s.length
|
57
|
-
response.body
|
58
|
-
end
|
59
46
|
|
60
|
-
|
61
|
-
response = get("subnets
|
47
|
+
return nil if subnet_id.nil?
|
48
|
+
response = @api_resource.get("subnets/#{subnet_id}/")
|
62
49
|
json_body = JSON.parse(response.body)
|
63
|
-
json_body['data'] = filter_fields(json_body, [:id, :subnet, :description, :mask])[0]
|
64
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
65
|
-
response.body = json_body.to_json
|
66
|
-
response.header['Content-Length'] = json_body.to_s.length
|
67
|
-
response.body
|
68
|
-
end
|
69
|
-
|
70
|
-
def get_section(section_name)
|
71
|
-
response = get("sections/#{section_name}/")
|
72
|
-
json_body = JSON.parse(response.body)
|
73
|
-
json_body['data'] = filter_hash(json_body['data'], [:id, :name, :description]) if json_body['data']
|
74
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
75
|
-
response.body = json_body.to_json
|
76
|
-
response.header['Content-Length'] = json_body.to_s.length
|
77
|
-
response.body
|
78
|
-
end
|
79
50
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
response.header['Content-Length'] = json_body.to_s.length
|
87
|
-
response.body
|
88
|
-
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
|
+
}
|
89
57
|
|
90
|
-
|
91
|
-
response = get("sections/#{section_id}/subnets/")
|
92
|
-
fields = [:subnet, :mask, :description]
|
93
|
-
fields.push(:id) if include_id
|
94
|
-
json_body = JSON.parse(response.body)
|
95
|
-
json_body['data'] = filter_fields(json_body, fields) if json_body['data']
|
96
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
97
|
-
response.body = json_body.to_json
|
98
|
-
response.header['Content-Length'] = json_body.to_s.length
|
99
|
-
response.body
|
58
|
+
return data if json_body['data']
|
100
59
|
end
|
101
60
|
|
102
|
-
def
|
103
|
-
|
104
|
-
json_body = JSON.parse(
|
105
|
-
|
106
|
-
json_body = filter_hash(json_body, [:code, :data, :error, :message])
|
107
|
-
response.body = json_body.to_json
|
108
|
-
response.header['Content-Length'] = json_body.to_s.length
|
109
|
-
response.body
|
110
|
-
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?
|
111
65
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
response.header['Content-Length'] = json_body.to_s.length
|
119
|
-
response.body
|
120
|
-
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
|
+
}
|
121
72
|
|
122
|
-
|
123
|
-
response = delete("addresses/#{ip}/#{subnet_id.to_s}/")
|
124
|
-
json_body = JSON.parse(response.body)
|
125
|
-
json_body = filter_hash(json_body, [:code, :error, :message])
|
126
|
-
response.body = json_body.to_json
|
127
|
-
response.header['Content-Length'] = json_body.to_s.length
|
128
|
-
response.body
|
73
|
+
return data if json_body['data']
|
129
74
|
end
|
130
75
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
subnet_hash = @@ip_cache[section.to_sym][cidr.to_sym]
|
137
|
-
|
138
|
-
return {:code => json_body['code'], :error => json_body['message']}.to_json if json_body['message']
|
139
|
-
|
140
|
-
if subnet_hash && subnet_hash.key?(mac.to_sym)
|
141
|
-
json_body['data'] = @@ip_cache[section_name.to_sym][cidr.to_sym][mac.to_sym][:ip]
|
142
|
-
else
|
143
|
-
next_ip = nil
|
144
|
-
new_ip = json_body['data']
|
145
|
-
ip_not_in_cache = subnet_hash.nil? ? true : !subnet_hash.to_s.include?(new_ip.to_s)
|
146
|
-
|
147
|
-
if ip_not_in_cache
|
148
|
-
next_ip = new_ip.to_s
|
149
|
-
add_ip_to_cache(new_ip, mac, cidr, section)
|
150
|
-
else
|
151
|
-
next_ip = find_new_ip(subnet_id, new_ip, mac, cidr, section)
|
152
|
-
end
|
153
|
-
|
154
|
-
return {:code => 404, :error => "Unable to find another available IP address in subnet #{cidr}"}.to_json if next_ip.nil?
|
155
|
-
return {:code => 404, :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)
|
156
|
-
|
157
|
-
json_body['data'] = next_ip
|
158
|
-
end
|
159
|
-
|
160
|
-
json_body = {:code => json_body['code'], :data => json_body['data']}
|
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?
|
161
81
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
82
|
+
data = {
|
83
|
+
id: json_body['data']['id'],
|
84
|
+
name: json_body['data']['name'],
|
85
|
+
description: json_body['data']['description']
|
86
|
+
}
|
166
87
|
|
167
|
-
|
168
|
-
logger.info("Starting allocated ip address maintenance (used by get_next_ip call).")
|
169
|
-
@@timer_task = Concurrent::TimerTask.new(:execution_interval => DEFAULT_CLEANUP_INTERVAL) { init_cache }
|
170
|
-
@@timer_task.execute
|
88
|
+
return data if json_body['data']
|
171
89
|
end
|
172
90
|
|
173
|
-
|
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?
|
174
95
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
# 2). UUID (Used when Mac Address not specified)
|
183
|
-
#
|
184
|
-
# {
|
185
|
-
# "": {
|
186
|
-
# "100.55.55.0/24":{
|
187
|
-
# "00:0a:95:9d:68:10": {"ip": "100.55.55.1", "timestamp": "2019-09-17 12:03:43 -D400"},
|
188
|
-
# "906d8bdc-dcc0-4b59-92cb-665935e21662": {"ip": "100.55.55.2", "timestamp": "2019-09-17 11:43:22 -D400"}
|
189
|
-
# },
|
190
|
-
# },
|
191
|
-
# "IPAM Group Name": {
|
192
|
-
# "123.11.33.0/24":{
|
193
|
-
# "00:0a:95:9d:68:33": {"ip": "123.11.33.1", "timestamp": "2019-09-17 12:04:43 -0400"},
|
194
|
-
# "00:0a:95:9d:68:34": {"ip": "123.11.33.2", "timestamp": "2019-09-17 12:05:48 -0400"},
|
195
|
-
# "00:0a:95:9d:68:35": {"ip": "123.11.33.3", "timestamp:: "2019-09-17 12:06:50 -0400"}
|
196
|
-
# }
|
197
|
-
# },
|
198
|
-
# "Another IPAM Group": {
|
199
|
-
# "185.45.39.0/24":{
|
200
|
-
# "00:0a:95:9d:68:55": {"ip": "185.45.39.1", "timestamp": "2019-09-17 12:04:43 -0400"},
|
201
|
-
# "00:0a:95:9d:68:56": {"ip": "185.45.39.2", "timestamp": "2019-09-17 12:05:48 -0400"}
|
202
|
-
# }
|
203
|
-
# }
|
204
|
-
# }
|
205
|
-
def init_cache
|
206
|
-
@@m.synchronize do
|
207
|
-
if @@ip_cache and not @@ip_cache.empty?
|
208
|
-
logger.debug("Processing ip cache.")
|
209
|
-
@@ip_cache.each do |section, subnets|
|
210
|
-
subnets.each do |cidr, macs|
|
211
|
-
macs.each do |mac, ip|
|
212
|
-
if Time.now - Time.parse(ip[:timestamp]) > DEFAULT_CLEANUP_INTERVAL
|
213
|
-
@@ip_cache[section][cidr].delete(mac)
|
214
|
-
end
|
215
|
-
end
|
216
|
-
@@ip_cache[section].delete(cidr) if @@ip_cache[section][cidr].nil? or @@ip_cache[section][cidr].empty?
|
217
|
-
end
|
218
|
-
end
|
219
|
-
else
|
220
|
-
logger.debug("Clearing ip cache.")
|
221
|
-
@@ip_cache = {:"" => {}}
|
222
|
-
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
|
+
})
|
223
103
|
end
|
224
|
-
end
|
225
104
|
|
226
|
-
|
227
|
-
logger.debug("Adding IP #{ip} to cache for subnet #{cidr} in section #{section_name}")
|
228
|
-
@@m.synchronize do
|
229
|
-
# Clear cache data which has the same mac and ip with the new one
|
230
|
-
|
231
|
-
mac_addr = (mac.nil? || mac.empty?) ? SecureRandom.uuid : mac
|
232
|
-
section_hash = @@ip_cache[section_name.to_sym]
|
233
|
-
|
234
|
-
section_hash.each do |key, values|
|
235
|
-
if values.keys.include? mac_addr.to_sym
|
236
|
-
@@ip_cache[section_name.to_sym][key].delete(mac_addr.to_sym)
|
237
|
-
end
|
238
|
-
@@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?
|
239
|
-
end
|
240
|
-
|
241
|
-
if section_hash.key?(cidr.to_sym)
|
242
|
-
@@ip_cache[section_name.to_sym][cidr.to_sym][mac_addr.to_sym] = {:ip => ip.to_s, :timestamp => Time.now.to_s}
|
243
|
-
else
|
244
|
-
@@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}}}})
|
245
|
-
end
|
246
|
-
end
|
105
|
+
return data if json_body['data']
|
247
106
|
end
|
248
107
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
retry_count = 0
|
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?
|
256
114
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
break
|
266
|
-
end
|
267
|
-
|
268
|
-
temp_ip = new_ip
|
269
|
-
retry_count += 1
|
270
|
-
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
|
+
})
|
271
123
|
end
|
272
124
|
|
273
|
-
|
274
|
-
return ip if found_ip.nil?
|
275
|
-
|
276
|
-
found_ip
|
125
|
+
return data if json_body['data']
|
277
126
|
end
|
278
127
|
|
279
|
-
def
|
280
|
-
|
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']
|
281
132
|
end
|
282
133
|
|
283
|
-
def
|
284
|
-
|
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' }
|
285
140
|
end
|
286
141
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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' }
|
291
147
|
end
|
292
148
|
|
293
|
-
def
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
}
|
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 }
|
302
158
|
end
|
303
159
|
|
304
|
-
def
|
305
|
-
|
306
|
-
uri = URI(@api_base + path)
|
307
|
-
uri.query = URI.encode_www_form(body) if body
|
308
|
-
request = Net::HTTP::Delete.new(uri)
|
309
|
-
request['token'] = @token
|
310
|
-
|
311
|
-
Net::HTTP.start(uri.hostname, uri.port) {|http|
|
312
|
-
http.request(request)
|
313
|
-
}
|
160
|
+
def groups_supported?
|
161
|
+
true
|
314
162
|
end
|
315
163
|
|
316
|
-
def
|
317
|
-
|
318
|
-
uri = URI(@api_base + path)
|
319
|
-
uri.query = URI.encode_www_form(body) if body
|
320
|
-
request = Net::HTTP::Post.new(uri)
|
321
|
-
request['token'] = @token
|
322
|
-
|
323
|
-
Net::HTTP.start(uri.hostname, uri.port) {|http|
|
324
|
-
http.request(request)
|
325
|
-
}
|
164
|
+
def authenticated?
|
165
|
+
!@token.nil?
|
326
166
|
end
|
327
167
|
|
168
|
+
private
|
169
|
+
|
328
170
|
def authenticate
|
329
|
-
auth_uri = URI(@api_base
|
171
|
+
auth_uri = URI("#{@api_base}/user/")
|
330
172
|
request = Net::HTTP::Post.new(auth_uri)
|
331
173
|
request.basic_auth @conf[:user], @conf[:password]
|
332
174
|
|
333
|
-
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|
|
334
176
|
http.request(request)
|
335
|
-
|
177
|
+
end
|
336
178
|
|
337
179
|
response = JSON.parse(response.body)
|
338
|
-
|
339
|
-
|
180
|
+
logger.warn(response['message']) if response['message']
|
181
|
+
response.dig('data', 'token')
|
182
|
+
end
|
340
183
|
end
|
341
184
|
end
|