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