aws-eni 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 14eb7ab252066d43cf0f289c7d5de6e796fe6b95
4
- data.tar.gz: b96f9a46cc748d54bbef973a5af74fca467a7b22
3
+ metadata.gz: 9e3020c6ae20a0d6a7027f065560e00c5764dbfb
4
+ data.tar.gz: f4827152a5efad857d3a8818e7f882a5a25e15ca
5
5
  SHA512:
6
- metadata.gz: 48d1acfaaac7ca2ea0777305388fa1cac6f48c1b0a368dc9b08e493ff4a52274fc1813a34027b0c0858c27c4e3c91f3396df2090f1c9152f520133f0b0abbc75
7
- data.tar.gz: fb0c4f4a9b9309b8236c729c7ac438f62579a057accf41488c5e3822cb0cb509d52f30aa6dc393ea2f6253cf4c720f568a0ef45e3628ad368c2b2e659210dc7d
6
+ metadata.gz: bd1bb10082ba681b5452411d8f2e75a39cc9b6d7227e3e8b996de89b5d619072c3295a40c63db98e0e11bb2c938e8c01c7b736fd5122f6bd4da45d896a65b435
7
+ data.tar.gz: 9c0e454b7797af337b50829ee4231ee9ee2f3f558343980023f4bb5078f6fb0c17fe0d81e5954473b90e67669e25adbb710ffc808143bb9e7b9eeb2a47e52ed4
@@ -24,7 +24,7 @@ desc 'Display all system commands and warnings'
24
24
  switch [:V,:verbose], negatable: false
25
25
 
26
26
  pre do |opt|
27
- Aws::ENI.timeout 90
27
+ Aws::ENI.timeout 120
28
28
  Aws::ENI.verbose opt[:verbose]
29
29
  true
30
30
  end
@@ -398,11 +398,14 @@ command [:unassign] do |c|
398
398
  c.desc 'Release any associated public IP address'
399
399
  c.switch [:r,:release], negatable: false
400
400
 
401
+ c.desc 'Do not wait for new interface state to propogate'
402
+ c.switch [:n,:noblock], negatable: false
403
+
401
404
  c.action do |global,opts,args|
402
405
  params = parse_args args, :interface_id, :device_name, :private_ip
403
406
  help_now! "Missing argument" unless params[:private_ip]
404
- params[:release] = opts[:release]
405
407
 
408
+ params.merge! release: opts[:release], block: !opts[:noblock]
406
409
  unassign = Aws::ENI.unassign_secondary_ip(params[:private_ip], params)
407
410
  if unassign[:released]
408
411
  puts "EIP #{unassign[:public_ip]} (#{unassign[:allocation_id]}) dissociated from #{unassign[:private_ip]} and released"
@@ -463,11 +466,15 @@ command [:dissociate] do |c|
463
466
  c.desc 'Release the associated public IP address'
464
467
  c.switch [:r,:release], negatable: false
465
468
 
469
+ c.desc 'Do not wait for new interface state to propogate'
470
+ c.switch [:noblock], negatable: false
471
+
466
472
  c.action do |global,opts,args|
467
473
  params = parse_args args, :private_ip, :public_ip, :allocation_id, :association_id, :interface_id, :device_name
468
- params[:release] = opts[:release]
469
474
  address = params[:private_ip] || params[:public_ip] || params[:association_id] || params[:allocation_id]
470
475
  help_now! "Missing argument" unless address
476
+
477
+ params.merge! release: opts[:release], block: !opts[:noblock]
471
478
  dissoc = Aws::ENI.dissociate_elastic_ip(address, params)
472
479
  if dissoc[:released]
473
480
  puts "EIP #{dissoc[:public_ip]} (#{dissoc[:allocation_id]}) dissociated from #{dissoc[:private_ip]} on #{dissoc[:device_name]} (#{dissoc[:interface_id]}) and released"
@@ -9,16 +9,21 @@ module Aws
9
9
  module ENI
10
10
  extend self
11
11
 
12
+ # class level thread-safe lock
13
+ @lock = Mutex.new
14
+
12
15
  def environment
13
- @environment ||= Meta.connection do
14
- hwaddr = Meta.instance('network/interfaces/macs/').lines.first.strip.chomp('/')
15
- {
16
- instance_id: Meta.instance('instance-id'),
17
- availability_zone: Meta.instance('placement/availability-zone'),
18
- region: Meta.instance('placement/availability-zone').sub(/^(.*)[a-z]$/,'\1'),
19
- vpc_id: Meta.interface(hwaddr, 'vpc-id'),
20
- vpc_cidr: Meta.interface(hwaddr, 'vpc-ipv4-cidr-block')
21
- }.freeze
16
+ @lock.synchronize do
17
+ @environment ||= Meta.connection do
18
+ hwaddr = Meta.instance('network/interfaces/macs/').lines.first.strip.chomp('/')
19
+ {
20
+ instance_id: Meta.instance('instance-id'),
21
+ availability_zone: Meta.instance('placement/availability-zone'),
22
+ region: Meta.instance('placement/availability-zone').sub(/^(.*)[a-z]$/,'\1'),
23
+ vpc_id: Meta.interface(hwaddr, 'vpc-id'),
24
+ vpc_cidr: Meta.interface(hwaddr, 'vpc-ipv4-cidr-block')
25
+ }.freeze
26
+ end
22
27
  end
23
28
  rescue Errors::MetaNotFound
24
29
  raise Errors::EnvironmentError, 'Unable to detect VPC, library incompatible with EC2-Classic'
@@ -33,7 +38,7 @@ module Aws
33
38
 
34
39
  def timeout(new_default = nil)
35
40
  @timeout = new_default.to_i if new_default
36
- @timeout ||= 30
41
+ @timeout ||= 120
37
42
  end
38
43
 
39
44
  def verbose(set_verbose = nil)
@@ -117,7 +122,7 @@ module Aws
117
122
  end
118
123
 
119
124
  if do_block || do_config || do_enable
120
- wait_for 'the interface to attach', rescue: Errors::MetaNotFound do
125
+ wait_for 'the interface to attach', rescue: Errors::InvalidInterface do
121
126
  device.exists? && Client.interface_attached(device.interface_id)
122
127
  end
123
128
  end
@@ -261,7 +266,7 @@ module Aws
261
266
  new_ip = options[:private_ip]
262
267
 
263
268
  if do_block && !device.enabled?
264
- raise Errors::InvalidParameter, "Interface #{device.name} is not enabled (cannot block)"
269
+ raise Errors::InvalidParameter, "Interface #{device.name} is not enabled (cannot test connection)"
265
270
  end
266
271
 
267
272
  if new_ip
@@ -273,29 +278,34 @@ module Aws
273
278
  private_ip_addresses: [new_ip],
274
279
  allow_reassignment: false
275
280
  )
276
- wait_for 'private ip address to be assigned' do
277
- Client.interface_private_ips(interface_id).include?(new_ip) ||
278
- device.local_ips.include?(new_ip)
279
- end
280
281
  else
281
282
  Client.assign_private_ip_addresses(
282
283
  network_interface_id: interface_id,
283
284
  secondary_private_ip_address_count: 1,
284
285
  allow_reassignment: false
285
286
  )
286
- wait_for 'new private ip address to be assigned' do
287
- new_ips = Client.interface_private_ips(interface_id) - current_ips
288
- new_ips = device.local_ips - current_ips if new_ips.empty?
289
- new_ip = new_ips.first if new_ips
287
+ wait_for 'new private IP address to be assigned' do
288
+ client_ips = Client.interface_private_ips(interface_id)
289
+ new_ip = (client_ips - current_ips).first || (device.meta_ips - current_ips).first
290
290
  end
291
291
  end
292
292
 
293
293
  if do_config
294
294
  device.add_alias(new_ip)
295
- if do_block && !Interface.test(new_ip, target: device.gateway)
295
+ if do_block && !Interface.test(new_ip, target: device.gateway, timeout: timeout)
296
296
  raise Errors::TimeoutError, 'Timed out waiting for IP address to become active'
297
297
  end
298
298
  end
299
+
300
+ if do_block
301
+ # ensure new state has propagated to avoid race conditions
302
+ wait_for 'new private IP address to appear in metadata' do
303
+ device.meta_ips.include?(new_ip)
304
+ end
305
+ wait_for 'new private IP address to appear in EC2 resource' do
306
+ Client.interface_private_ips(interface_id).include?(new_ip)
307
+ end
308
+ end
299
309
  {
300
310
  private_ip: new_ip,
301
311
  interface_id: interface_id,
@@ -308,6 +318,7 @@ module Aws
308
318
  # remove a private ip using the AWS api and remove it from local config
309
319
  def unassign_secondary_ip(private_ip, options = {})
310
320
  do_release = !!options[:release]
321
+ do_block = options[:block] != false
311
322
 
312
323
  find = options[:device_name] || options[:device_number] || options[:interface_id] || private_ip
313
324
  device = Interface[find].assert(
@@ -335,6 +346,16 @@ module Aws
335
346
  network_interface_id: interface[:network_interface_id],
336
347
  private_ip_addresses: [private_ip]
337
348
  )
349
+
350
+ if do_block
351
+ # ensure new state has propagated to avoid race conditions
352
+ wait_for 'private IP address to be removed from metadata' do
353
+ !device.meta_ips.include?(private_ip)
354
+ end
355
+ wait_for 'private IP address to be removed from EC2 resource' do
356
+ !Client.interface_private_ips(device.interface_id).include?(private_ip)
357
+ end
358
+ end
338
359
  {
339
360
  private_ip: private_ip,
340
361
  device_name: device.name,
@@ -402,8 +423,14 @@ module Aws
402
423
  allow_reassociation: false
403
424
  )
404
425
 
405
- if do_block && !Interface.test(private_ip)
406
- raise Errors::TimeoutError, 'Timed out waiting for ip address to become active'
426
+ if do_block
427
+ # ensure new state has propagated to avoid race conditions
428
+ wait_for 'public IP address to appear in metadata' do
429
+ device.public_ips.has_value?(eip[:public_ip])
430
+ end
431
+ if !Interface.test(private_ip, timeout: timeout)
432
+ raise Errors::TimeoutError, 'Timed out waiting for IP address to become active'
433
+ end
407
434
  end
408
435
  {
409
436
  private_ip: private_ip,
@@ -420,6 +447,7 @@ module Aws
420
447
  # optionally release the public ip
421
448
  def dissociate_elastic_ip(address, options = {})
422
449
  do_release = !!options[:release]
450
+ do_block = options[:block] != false
423
451
 
424
452
  # assert device attributes if we've specified a device
425
453
  if find = options[:device_name] || options[:device_number]
@@ -463,6 +491,13 @@ module Aws
463
491
 
464
492
  Client.disassociate_address(association_id: eip[:association_id])
465
493
  Client.release_address(allocation_id: eip[:allocation_id]) if do_release
494
+
495
+ if device && do_block
496
+ # ensure new state has propagated to avoid race conditions
497
+ wait_for 'public IP address to be removed from metadata' do
498
+ !device.public_ips.has_value?(eip[:public_ip])
499
+ end
500
+ end
466
501
  {
467
502
  private_ip: eip[:private_ip_address],
468
503
  device_name: device && device.name,
@@ -578,7 +613,7 @@ module Aws
578
613
  def wait_for(task, options = {}, &block)
579
614
  errors = [*options[:rescue]]
580
615
  timeout = options[:timeout] || self.timeout
581
- interval = options[:interval] || 0.1
616
+ interval = options[:interval] || 0.3
582
617
 
583
618
  until timeout < 0
584
619
  begin
@@ -6,6 +6,9 @@ module Aws
6
6
  module ENI
7
7
  class Interface
8
8
 
9
+ # Class level thread-safe lock
10
+ @lock = Mutex.new
11
+
9
12
  class << self
10
13
  include Enumerable
11
14
 
@@ -15,8 +18,10 @@ module Aws
15
18
  def [](index)
16
19
  case index
17
20
  when Integer
18
- @instance_cache ||= []
19
- @instance_cache[index] ||= new("eth#{index}", false)
21
+ @lock.synchronize do
22
+ @instance_cache ||= []
23
+ @instance_cache[index] ||= new("eth#{index}", false)
24
+ end
20
25
  when nil
21
26
  self[next_available_index]
22
27
  when /^(?:eth)?([0-9]+)$/
@@ -144,6 +149,7 @@ module Aws
144
149
  @name = name
145
150
  @device_number = $1.to_i
146
151
  @route_table = @device_number + 10000
152
+ @lock = Mutex.new
147
153
  configure if auto_config
148
154
  end
149
155
 
@@ -166,19 +172,21 @@ module Aws
166
172
 
167
173
  # Validate and return basic interface metadata
168
174
  def info
169
- hwaddr = self.hwaddr
170
- unless @meta_cache && @meta_cache[:hwaddr] == hwaddr
171
- @meta_cache = Meta.connection do
172
- raise Errors::MetaBadResponse unless Meta.interface(hwaddr, '', not_found: nil)
173
- {
174
- hwaddr: hwaddr,
175
- interface_id: Meta.interface(hwaddr, 'interface-id'),
176
- subnet_id: Meta.interface(hwaddr, 'subnet-id'),
177
- subnet_cidr: Meta.interface(hwaddr, 'subnet-ipv4-cidr-block')
178
- }.freeze
175
+ @lock.synchronize do
176
+ hwaddr = self.hwaddr
177
+ unless @meta_cache && @meta_cache[:hwaddr] == hwaddr
178
+ @meta_cache = Meta.connection do
179
+ raise Errors::MetaBadResponse unless Meta.interface(hwaddr, '', not_found: nil)
180
+ {
181
+ hwaddr: hwaddr,
182
+ interface_id: Meta.interface(hwaddr, 'interface-id'),
183
+ subnet_id: Meta.interface(hwaddr, 'subnet-id'),
184
+ subnet_cidr: Meta.interface(hwaddr, 'subnet-ipv4-cidr-block')
185
+ }.freeze
186
+ end
179
187
  end
188
+ @meta_cache
180
189
  end
181
- @meta_cache
182
190
  rescue Errors::MetaConnectionFailed
183
191
  raise Errors::InvalidInterface, "Interface #{name} could not be found in the EC2 instance meta-data"
184
192
  end
@@ -210,13 +218,25 @@ module Aws
210
218
  list.lines.grep(/inet ([0-9\.]+)\/.* #{name}/i){ $1 }
211
219
  end
212
220
 
221
+ # Return an array of ip addresses found in our instance metadata
222
+ def meta_ips
223
+ # hack to use cached hwaddr when available since this is often polled
224
+ # continuously for changes
225
+ hwaddr = (@meta_cache && @meta_cache[:hwaddr]) || hwaddr
226
+ Meta.interface(hwaddr, 'local-ipv4s', cache: false).lines.map(&:strip)
227
+ end
228
+
229
+ # Return a hash of local/public ip associations found in instance metadata
213
230
  def public_ips
214
231
  hwaddr = self.hwaddr
215
232
  Hash[
216
233
  Meta.connection do
217
234
  Meta.interface(hwaddr, 'ipv4-associations/', not_found: '', cache: false).lines.map do |public_ip|
218
235
  public_ip.strip!
219
- [ Meta.interface(hwaddr, "ipv4-associations/#{public_ip}", cache: false), public_ip ]
236
+ unless private_ip = Meta.interface(hwaddr, "ipv4-associations/#{public_ip}", not_found: nil, cache: false)
237
+ raise Errors::MetaBadResponse
238
+ end
239
+ [ private_ip, public_ip ]
220
240
  end
221
241
  end
222
242
  ]
@@ -244,7 +264,6 @@ module Aws
244
264
  changes = 0
245
265
  prefix = self.prefix # prevent exists? check on each use
246
266
 
247
- meta_ips = Meta.interface(hwaddr, 'local-ipv4s', cache: false).lines.map(&:strip)
248
267
  local_primary, *local_aliases = local_ips
249
268
  meta_primary, *meta_aliases = meta_ips
250
269
 
@@ -5,6 +5,10 @@ require 'aws-eni/errors'
5
5
  module Aws
6
6
  module ENI
7
7
  module Meta
8
+ extend self
9
+
10
+ # Per-thread open connection storage
11
+ @connections = {}
8
12
 
9
13
  # These are the errors we trap when attempting to talk to the instance
10
14
  # metadata service. Any of these imply the service is not present, not
@@ -21,7 +25,7 @@ module Aws
21
25
 
22
26
  # Perform a GET request on an open HTTP connection to the EC2 instance
23
27
  # meta-data and return the body of any 200 response.
24
- def self.get(path, options = {})
28
+ def get(path, options = {})
25
29
  @cache ||= {}
26
30
  if @cache[path] && options[:cache] != false
27
31
  @cache[path]
@@ -43,39 +47,46 @@ module Aws
43
47
 
44
48
  # Perform a GET request on the instance metadata and return the body of
45
49
  # any 200 response.
46
- def self.instance(path, options = {})
50
+ def instance(path, options = {})
47
51
  get("/latest/meta-data/#{path}", options)
48
52
  end
49
53
 
50
54
  # Perform a GET request on the interface metadata and return the body of
51
55
  # any 200 response.
52
- def self.interface(hwaddr, path, options = {})
56
+ def interface(hwaddr, path, options = {})
53
57
  instance("network/interfaces/macs/#{hwaddr}/#{path}", options)
54
58
  end
55
59
 
56
60
  # Open a connection and attempt to execute the block `retries` times.
57
61
  # Can specify open and read timeouts in addition to the number of retries.
58
- def self.connection(options = {})
59
- return yield(@open_connection) if @open_connection
62
+ def connection(options = {})
63
+ raise ArgumentError, 'Must be called with a block' unless block_given?
64
+ attempts = 0
60
65
  retries = options[:retries] || 5
61
- failed_attempts = 0
62
- begin
63
- http = Net::HTTP.new('169.254.169.254', '80', nil)
64
- http.open_timeout = options[:open_timeout] || 5
65
- http.read_timeout = options[:read_timeout] || 5
66
- @open_connection = http.start
67
- yield(http).tap { http.finish }
68
- rescue *FAILURES => e
69
- if failed_attempts < retries
70
- # retry after an ever increasing cooldown time with each failure
71
- Kernel.sleep(1.2 ** failed_attempts)
72
- failed_attempts += 1
73
- retry
74
- else
75
- raise Errors::MetaConnectionFailed, "EC2 Metadata request failed after #{retries} retries."
66
+
67
+ if (http = @connections[Thread.current]) && http.active?
68
+ yield(http)
69
+ else
70
+ begin
71
+ http = Net::HTTP.new('169.254.169.254', '80', nil)
72
+ http.open_timeout = options[:open_timeout] || 5
73
+ http.read_timeout = options[:read_timeout] || 5
74
+
75
+ @connections[Thread.current] = http.start
76
+ yield(http)
77
+ rescue *FAILURES => e
78
+ if attempts < retries
79
+ # increasing cooldown time on each attempt
80
+ Kernel.sleep(1.2 ** attempts)
81
+ attempts += 1
82
+ retry
83
+ else
84
+ raise Errors::MetaConnectionFailed, "EC2 Metadata request failed after #{retries} retries."
85
+ end
86
+ ensure
87
+ http = @connections.delete(Thread.current)
88
+ http.finish if http && http.active?
76
89
  end
77
- ensure
78
- @open_connection = nil
79
90
  end
80
91
  end
81
92
  end
@@ -1,5 +1,5 @@
1
1
  module Aws
2
2
  module ENI
3
- VERSION = "0.3.1"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-eni
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Greiling
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-07 00:00:00.000000000 Z
11
+ date: 2015-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gli