smart_proxy_ipam 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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