smart_proxy_ipam 0.0.9 → 0.0.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 903c2fe4707574d5898ba16c5974484b6969b30d93bb7111068fd4f612e44432
4
- data.tar.gz: 7c274fbb32851d7dea75a900a5f76fcaad0d317ef3ea1422274f5db265ca2be6
3
+ metadata.gz: f3812263cf984e604cf0b7272bf9519cf503ea883b46a353a6f1edbcaefe7eab
4
+ data.tar.gz: 11a128c3d83dc3f6c2b4c9157846076a351c4dc0ab82c4c7b332ddcb1a23349f
5
5
  SHA512:
6
- metadata.gz: 2377fc0504d1ca888c3d3710d00a1ab4f993c16f122e8d8570c5c8d60d89bcc9b9cf0dd7c8b3c7af182b2b02b8f2318c4addf54c270c682b13e76cdfdc6316c7
7
- data.tar.gz: 63a7b1dec614cd796c68542a63b2263c5bcfd08d4043e1095535154d4f67d1b5c1d00dd90bc06b6ae657e0306c31c08a73a35fff10ed04f9305022bfd0fb7bbf
6
+ metadata.gz: 517e692d368b9febe9d8c34eba7b9e9e7e919714cf7d560d6df4483f28e0e8bb3f8ef485030cd68f08458a7c87dacb358e8a4aac1bd4882b95df0c696415587e
7
+ data.tar.gz: 5bb0c0a9bf0ce71d58d2fc234429568882d210436b21589acbc74d96a4bddcca6b6ccb4f4ae371f805368db9fa71bda6bc21c946a2ed00b52090ce7f2e5a7179
@@ -10,11 +10,6 @@ module Proxy::Phpipam
10
10
  include ::Proxy::Log
11
11
  helpers ::Proxy::Helpers
12
12
 
13
- get '/providers' do
14
- content_type :json
15
- {:ipam_providers => ['phpIPAM']}.to_json
16
- end
17
-
18
13
  # Gets the next available IP address based on a given subnet
19
14
  #
20
15
  # Input: cidr(string): CIDR address in the format: "100.20.20.0/24"
@@ -29,26 +24,30 @@ module Proxy::Phpipam
29
24
 
30
25
  begin
31
26
  cidr = params[:cidr]
27
+ mac = params[:mac]
32
28
 
33
29
  if not cidr
34
30
  return {:error => "A 'cidr' parameter for the subnet must be provided(e.g. 100.10.10.0/24)"}.to_json
35
31
  end
32
+ if not mac
33
+ return {:error => "A 'mac' address must be provided(e.g. 00:0a:95:9d:68:10)"}.to_json
34
+ end
36
35
 
37
36
  phpipam_client = PhpipamClient.new
38
- response = phpipam_client.get_subnet(cidr)
37
+ subnet = JSON.parse(phpipam_client.get_subnet(cidr))
39
38
 
40
- if response['message'] && response['message'].downcase == "no subnets found"
39
+ if !subnet.kind_of?(Array) && subnet['message'] && subnet['message'].downcase == "no subnets found"
41
40
  return {:error => "The specified subnet does not exist in External IPAM."}.to_json
42
41
  end
43
-
44
- subnet_id = JSON.parse(response)[0]['id']
45
- response = phpipam_client.get_next_ip(subnet_id)
42
+
43
+ subnet_id = subnet[0]['id']
44
+ ip = phpipam_client.get_next_ip(subnet_id, mac, cidr)
46
45
 
47
- if response['message'] && response['message'].downcase == "no free addresses found"
46
+ if !ip.kind_of?(Array) && ip['message'] && ip['message'].downcase == "no free addresses found"
48
47
  return {:error => "There are no more free addresses in subnet #{cidr}"}.to_json
49
48
  end
50
49
 
51
- {:cidr => cidr, :next_ip => response['data']}.to_json
50
+ {:cidr => cidr, :next_ip => ip['next_ip']}.to_json
52
51
  rescue Errno::ECONNREFUSED
53
52
  return {:error => "Unable to connect to External IPAM server"}.to_json
54
53
  end
@@ -115,9 +114,9 @@ module Proxy::Phpipam
115
114
  end
116
115
  end
117
116
 
118
- # Get a list of subnets for given external ipam section
117
+ # Get a list of subnets for given external ipam section/group
119
118
  #
120
- # Input: section_id(integer). The id of the external ipam section
119
+ # Input: section_name(string). The name of the external ipam section/group
121
120
  # Returns: Array of subnets(as json) in "data" key on success, hash with error otherwise
122
121
  # Examples:
123
122
  # Response if success:
@@ -169,19 +168,19 @@ module Proxy::Phpipam
169
168
  # "time":0.012
170
169
  # }
171
170
  # Response if :error =>
172
- # {"error":"Unable to connect to phpIPAM server"}
173
- get '/sections/:section_id/subnets' do
171
+ # {"error":"Unable to connect to External IPAM server"}
172
+ get '/sections/:section_name/subnets' do
174
173
  content_type :json
175
174
 
176
175
  begin
177
- section_id = params[:section_id]
176
+ section_name = URI.decode(params[:section_name])
178
177
 
179
- if not section_id
180
- return {:error => "A 'section_id' must be provided"}.to_json
178
+ if not section_name
179
+ return {:error => "A 'section_name' must be provided"}.to_json
181
180
  end
182
181
 
183
182
  phpipam_client = PhpipamClient.new
184
- subnets = phpipam_client.get_subnets(section_id)
183
+ subnets = phpipam_client.get_subnets(section_name)
185
184
  subnets.to_json
186
185
  rescue Errno::ECONNREFUSED
187
186
  return {:error => "Unable to connect to External IPAM server"}.to_json
@@ -1,37 +1,37 @@
1
1
  require 'yaml'
2
- require 'logger'
3
2
  require 'json'
4
3
  require 'net/http'
4
+ require 'monitor'
5
+ require 'concurrent'
5
6
  require 'smart_proxy_ipam/ipam'
6
7
  require 'smart_proxy_ipam/ipam_main'
7
8
 
8
9
  module Proxy::Phpipam
9
10
  class PhpipamClient
11
+ include Proxy::Log
12
+
13
+ MAX_RETRIES = 5
14
+ DEFAULT_CLEANUP_INTERVAL = 120 # 2 mins
15
+ @@ip_cache = nil
16
+
10
17
  def initialize
11
- conf = Proxy::Ipam.get_config[:phpipam]
12
- @phpipam_config = {
13
- :url => conf[:url],
14
- :user => conf[:user],
15
- :password => conf[:password]
16
- }
17
- @api_base = "#{conf[:url]}/api/#{conf[:user]}/"
18
+ @conf = Proxy::Ipam.get_config[:phpipam]
19
+ @api_base = "#{@conf[:url]}/api/#{@conf[:user]}/"
18
20
  @token = nil
19
- end
20
-
21
- def init_reservations
22
- @@ipam_reservations = {}
21
+ @m = Monitor.new
22
+ init_cache if @@ip_cache.nil?
23
+ start_cleanup_task
23
24
  end
24
25
 
25
26
  def get_subnet(cidr)
26
- url = "subnets/cidr/#{cidr.to_s}"
27
- subnets = get(url)
27
+ subnets = get("subnets/cidr/#{cidr.to_s}")
28
28
  response = []
29
29
 
30
30
  if subnets
31
31
  if subnets['message'] && subnets['message'].downcase == 'no subnets found'
32
32
  return {:message => "no subnets found"}.to_json
33
33
  else
34
- # Only return the relevant fields to Foreman
34
+ # Only return the relevant fields
35
35
  subnets['data'].each do |subnet|
36
36
  response.push({
37
37
  :id => subnet['id'],
@@ -68,12 +68,16 @@ module Proxy::Phpipam
68
68
  response
69
69
  end
70
70
 
71
- def get_subnets(section_id)
72
- subnets = get("sections/#{section_id.to_s}/subnets/")
73
- response = []
71
+ def get_subnets(section_name)
72
+ sections = get_sections
73
+ section = sections.select {|section| section[:name] == section_name}
74
+ return {:error => "Section '#{section_name}' not found"}.to_json if section.size == 0
75
+ section_id = section[0][:id].to_s
76
+ subnets = get("sections/#{section_id}/subnets/")
77
+ response = []
74
78
 
75
79
  if subnets && subnets['data']
76
- # Only return the relevant data
80
+ # Only return the relevant fields
77
81
  subnets['data'].each do |subnet|
78
82
  response.push({
79
83
  :id => subnet['id'],
@@ -92,7 +96,7 @@ module Proxy::Phpipam
92
96
  usage = get_subnet_usage(subnet_id)
93
97
 
94
98
  # We need to check subnet usage first in the case there are zero ips in the subnet. Checking
95
- # the ip existence on an empty subnet returns a malformed response from phpIPAM, containing
99
+ # the ip existence on an empty subnet returns a malformed response from phpIPAM(v1.3), containing
96
100
  # HTML in the JSON response.
97
101
  if usage['data']['used'] == "0"
98
102
  return {:ip => ip, :exists => false}.to_json
@@ -116,9 +120,120 @@ module Proxy::Phpipam
116
120
  delete("addresses/#{ip}/#{subnet_id.to_s}/")
117
121
  end
118
122
 
119
- def get_next_ip(subnet_id)
120
- url = "subnets/#{subnet_id.to_s}/first_free/"
121
- get(url)
123
+ def get_next_ip(subnet_id, mac, cidr)
124
+ response = get("subnets/#{subnet_id.to_s}/first_free/")
125
+ subnet_hash = @@ip_cache[cidr.to_sym]
126
+
127
+ return response if response['message']
128
+
129
+ if subnet_hash&.key?(mac.to_sym)
130
+ response['next_ip'] = @@ip_cache[cidr.to_sym][mac.to_sym]
131
+ else
132
+ new_ip = response['data']
133
+ ip_not_in_cache = subnet_hash&.key(new_ip).nil?
134
+
135
+ if ip_not_in_cache
136
+ next_ip = new_ip.to_s
137
+ add_ip_to_cache(new_ip, mac, cidr)
138
+ else
139
+ next_ip = find_new_ip(subnet_id, new_ip, mac, cidr)
140
+ end
141
+
142
+ if next_ip.nil?
143
+ response['error'] = "Unable to find another available IP address in subnet #{cidr}"
144
+ return response
145
+ end
146
+
147
+ unless usable_ip(next_ip, cidr)
148
+ response['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 #{CLEAR_CACHE_DELAY} seconds)."
149
+ return response
150
+ end
151
+
152
+ response['next_ip'] = next_ip
153
+ end
154
+
155
+ response
156
+ end
157
+
158
+ def start_cleanup_task
159
+ logger.info("Starting allocated ip address maintenance (used by get_next_ip call).")
160
+ @timer_task = Concurrent::TimerTask.new(:execution_interval => DEFAULT_CLEANUP_INTERVAL) { init_cache }
161
+ @timer_task.execute
162
+ end
163
+
164
+ private
165
+
166
+ # @@ip_cache structure
167
+ # {
168
+ # "100.55.55.0/24":{
169
+ # "00:0a:95:9d:68:10": "100.55.55.1"
170
+ # },
171
+ # "123.11.33.0/24":{
172
+ # "00:0a:95:9d:68:33": "123.11.33.1",
173
+ # "00:0a:95:9d:68:34": "123.11.33.2",
174
+ # "00:0a:95:9d:68:35": "123.11.33.3"
175
+ # }
176
+ # }
177
+ def init_cache
178
+ logger.debug("Clearing ip cache.")
179
+ @m.synchronize do
180
+ @@ip_cache = {}
181
+ end
182
+ end
183
+
184
+ def add_ip_to_cache(ip, mac, cidr)
185
+ logger.debug("Adding IP #{ip} to cache for subnet #{cidr}")
186
+ @m.synchronize do
187
+ if @@ip_cache.key?(cidr.to_sym)
188
+ @@ip_cache[cidr.to_sym][mac.to_sym] = ip.to_s
189
+ else
190
+ @@ip_cache = @@ip_cache.merge({cidr.to_sym => {mac.to_sym => ip.to_s}})
191
+ end
192
+ end
193
+ end
194
+
195
+ # Called when next available IP from external IPAM has been cached by another user/host, but
196
+ # not actually persisted in external IPAM. Try to increment the IP(MAX_RETRIES times), and
197
+ # see if it is available in external IPAM.
198
+ def find_new_ip(subnet_id, ip, mac, cidr)
199
+ found_ip = nil
200
+ temp_ip = ip
201
+ retry_count = 0
202
+
203
+ loop do
204
+ new_ip = increment_ip(temp_ip)
205
+ verify_ip = JSON.parse(ip_exists(new_ip, subnet_id))
206
+
207
+ # If new IP doesn't exist in IPAM and not in the cache
208
+ if verify_ip['exists'] == false && !ip_exists_in_cache(new_ip, cidr)
209
+ found_ip = new_ip.to_s
210
+ add_ip_to_cache(found_ip, mac, cidr)
211
+ break
212
+ end
213
+
214
+ temp_ip = new_ip
215
+ retry_count += 1
216
+ break if retry_count >= MAX_RETRIES
217
+ end
218
+
219
+ # Return the original IP found in external ipam if no new ones found after MAX_RETRIES
220
+ return ip if found_ip.nil?
221
+
222
+ found_ip
223
+ end
224
+
225
+ def increment_ip(ip)
226
+ IPAddr.new(ip.to_s).succ.to_s
227
+ end
228
+
229
+ def ip_exists_in_cache(ip, cidr)
230
+ @@ip_cache[cidr.to_sym] && !@@ip_cache[cidr.to_sym].key(ip).nil?
231
+ end
232
+
233
+ # Checks if given IP is within a subnet. Broadcast address is considered unusable
234
+ def usable_ip(ip, cidr)
235
+ network = IPAddr.new(cidr)
236
+ network.include?(IPAddr.new(ip)) && network.to_range.last != ip
122
237
  end
123
238
 
124
239
  def get(path, body=nil)
@@ -166,7 +281,7 @@ module Proxy::Phpipam
166
281
  def authenticate
167
282
  auth_uri = URI(@api_base + '/user/')
168
283
  request = Net::HTTP::Post.new(auth_uri)
169
- request.basic_auth @phpipam_config[:user], @phpipam_config[:password]
284
+ request.basic_auth @conf[:user], @conf[:password]
170
285
 
171
286
  response = Net::HTTP.start(auth_uri.hostname, auth_uri.port) {|http|
172
287
  http.request(request)
@@ -174,6 +289,6 @@ module Proxy::Phpipam
174
289
 
175
290
  response = JSON.parse(response.body)
176
291
  @token = response['data']['token']
177
- end
292
+ end
178
293
  end
179
294
  end
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Proxy
3
3
  module Ipam
4
- VERSION = '0.0.9'
4
+ VERSION = '0.0.10'
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_ipam
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-12 00:00:00.000000000 Z
11
+ date: 2019-08-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Smart proxy plugin for IPAM integration with various IPAM providers
14
14
  email: chrisjsmith001@gmail.com
@@ -49,8 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
49
  - !ruby/object:Gem::Version
50
50
  version: '0'
51
51
  requirements: []
52
- rubyforge_project:
53
- rubygems_version: 2.7.7
52
+ rubygems_version: 3.0.6
54
53
  signing_key:
55
54
  specification_version: 4
56
55
  summary: Smart proxy plugin for IPAM integration with various IPAM providers