smart_proxy_ipam 0.0.22 → 0.1.0

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