aws-eni 0.1.2 → 0.1.3

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: 3dd624009ce2d87a86fa1e33188a38f0be254464
4
- data.tar.gz: b1b958d4b5b71a7431cd19cff6b8e595ba0db1ef
3
+ metadata.gz: 613d8b521f5ea3db65f0420e90dfe188c6b42e53
4
+ data.tar.gz: 0a1eebe7c2b86a128a282f9edd8d61d5c0c39d2c
5
5
  SHA512:
6
- metadata.gz: 209b4e24a7b5934a67beab03852f11565ed997296ce9063af45d87803c7cb0e169590dfb1a5d71b6e19207281a04306aa6f09ca38436f90012ee302fc59724ea
7
- data.tar.gz: bf9452d5c381ff04bab980f0c1fa5b1738294a336330c090c58b759919f52ef839d46c90a8af0307db1051c4f96a92ff4bc3ac9d5c42b9ebdaefc0cbac3c50f3
6
+ metadata.gz: 092fd301932d4521a4c7b1fda6c1d04f21160d9585bd5e0b1c11e5385a87b97b21bb33bd3a55001fafa3da4e2b39d67b12b5cd759c7f00aa2064ce9dfe733edd
7
+ data.tar.gz: c10cb8481dc1f9a544e3ebedb674bd434d4e30f1a9cdc1e02d022d2195016f5f16813919a387a82e27b4aa206d29b092cbbb51bc568e1a4f3ba7e135007f976c
@@ -213,7 +213,7 @@ command [:create] do |c|
213
213
  args.delete('new')
214
214
  params = parse_args args, :subnet_id, :security_groups, :primary_ip
215
215
  interface = Aws::ENI.create_interface(params)
216
- puts "interface #{interface[:id]} created on #{interface[:subnet_id]}"
216
+ puts "interface #{interface[:interface_id]} created on #{interface[:subnet_id]}"
217
217
  end
218
218
  end
219
219
 
@@ -251,12 +251,12 @@ command [:attach] do |c|
251
251
  params = parse_args args, :subnet_id, :security_groups, :primary_ip
252
252
  Aws::ENI.assert_ifconfig_access if config
253
253
  interface = Aws::ENI.create_interface(params)
254
- puts "interface #{interface[:id]} created on #{interface[:subnet_id]}"
255
- id = interface[:id]
254
+ puts "interface #{interface[:interface_id]} created on #{interface[:subnet_id]}"
255
+ id = interface[:interface_id]
256
256
  end
257
257
  device = Aws::ENI.attach_interface(id, enable: config, configure: config, block: opts[:block])
258
- puts "interface #{device[:id]} attached to #{device[:name]}"
259
- puts "device #{device[:name]} enabled and configured" if opts[:config]
258
+ puts "interface #{device[:interface_id]} attached to #{device[:device_name]}"
259
+ puts "device #{device[:device_name]} enabled and configured" if config
260
260
  end
261
261
  end
262
262
 
@@ -283,13 +283,13 @@ command [:detach] do |c|
283
283
  params = parse_args args, :interface_id, :device_name
284
284
  params[:block] = opts[:block]
285
285
  params[:delete] = opts[:delete] unless opts[:delete] == :not_provided
286
- id = params[:interface_id] || params[:device_name]
286
+ id = params[:device_name] || params[:interface_id]
287
287
 
288
288
  device = Aws::ENI.detach_interface(id, params)
289
289
  if device[:deleted]
290
- puts "interface #{device[:id]} detached from #{device[:name]} and deleted"
290
+ puts "interface #{device[:interface_id]} detached from #{device[:device_name]} and deleted"
291
291
  else
292
- puts "interface #{device[:id]} detached from #{device[:name]}"
292
+ puts "interface #{device[:interface_id]} detached from #{device[:device_name]}"
293
293
  end
294
294
  end
295
295
  end
@@ -327,10 +327,20 @@ long_desc %{
327
327
  arg 'ip-address', :optional
328
328
  arg 'interface-id OR device-name'
329
329
  command [:assign] do |c|
330
+ c.desc 'Do not configure the interface after assignment'
331
+ c.switch [:n,'no-config'], negatable: false
332
+
333
+ c.desc 'Do not return until connection is verified'
334
+ c.switch [:b,:block], negatable: false
335
+
330
336
  c.action do |global,opts,args|
331
- help_now! "Missing argument" if args.empty?
332
337
  params = parse_args args, :interface_id, :device_name, :private_ip
333
- device = params[:interface_id] || params[:device_name]
338
+ device = params[:device_name] || params[:interface_id]
339
+ help_now! "Missing argument" if device.nil?
340
+
341
+ params.merge! configure: !opts['no-config'], block: opts[:block]
342
+ Aws::ENI.assert_ifconfig_access if params[:configure]
343
+
334
344
  assignment = Aws::ENI.assign_secondary_ip(device, params)
335
345
  puts "IP #{assignment[:private_ip]} assigned to #{assignment[:device_name]} (#{assignment[:interface_id]})"
336
346
  end
@@ -380,10 +390,14 @@ arg 'allocation-id', :optional
380
390
  arg 'interface-id', :optional
381
391
  arg 'device-name', :optional
382
392
  command [:associate] do |c|
393
+ c.desc 'Do not return until connection is verified'
394
+ c.switch [:b,:block], negatable: false
395
+
383
396
  c.action do |global,opts,args|
384
397
  help_now! "Missing argument" if args.empty?
385
398
  args.delete('new')
386
399
  params = parse_args args, :private_ip, :public_ip, :allocation_id, :interface_id, :device_name
400
+ params[:block] = opts[:block]
387
401
  assoc = Aws::ENI.associate_elastic_ip(params[:private_ip], params)
388
402
  puts "EIP #{assoc[:public_ip]} (#{assoc[:allocation_id]}) associated with #{assoc[:private_ip]} on #{assoc[:device_name]} (#{assoc[:interface_id]})"
389
403
  end
@@ -427,19 +441,19 @@ command [:allocate] do |c|
427
441
  end
428
442
  end
429
443
 
430
- desc 'Release one or more unassigned Elastic IP addresses'
444
+ desc 'Release unassigned Elastic IP addresses'
431
445
  long_desc %{
432
- Release one or more unassigned Elastic IP addresses. If IP address or
433
- allication, will release only that EIP, otherwise will clean all unassigned
434
- EIPs.
446
+ Release one unassigned Elastic IP addresses identified by its public IP or its
447
+ allocation ID
435
448
  }
436
449
  arg 'ip-address OR allocation-id'
437
450
  command [:release] do |c|
438
451
  c.action do |global,opts,args|
439
452
  help_now! "Missing argument" if args.empty?
453
+ help_now! "Too many arguments" if args.count > 1
440
454
  params = parse_args args, :public_ip, :allocation_id
441
455
  eip = params[:public_ip] || params[:allocation_id]
442
- release = Aws::ENI.release_elastic_ip(eip, params)
456
+ release = Aws::ENI.release_elastic_ip(eip)
443
457
  puts "EIP #{release[:public_ip]} (#{release[:allocation_id]}) released"
444
458
  end
445
459
  end
@@ -1,4 +1,5 @@
1
1
  require 'time'
2
+ require 'resolv'
2
3
  require 'aws-sdk'
3
4
  require 'aws-eni/version'
4
5
  require 'aws-eni/errors'
@@ -78,7 +79,7 @@ module Aws
78
79
  end
79
80
  end
80
81
  {
81
- id: response[:network_interface][:network_interface_id],
82
+ interface_id: response[:network_interface][:network_interface_id],
82
83
  subnet_id: response[:network_interface][:subnet_id],
83
84
  api_response: response[:network_interface]
84
85
  }
@@ -90,74 +91,73 @@ module Aws
90
91
  do_config = true unless options[:configure] == false
91
92
  assert_ifconfig_access if do_config || do_enable
92
93
 
93
- interface = IFconfig[options[:device_number] || options[:name]]
94
- raise InvalidParameterError, "Interface #{interface.name} is already in use" if interface.exists?
94
+ device = IFconfig[options[:device_number] || options[:name]].assert(exists: false)
95
95
 
96
- params = {}
97
- params[:network_interface_id] = id
98
- params[:instance_id] = environment[:instance_id]
99
- params[:device_index] = interface.device_number
100
-
101
- response = client.attach_network_interface(params)
96
+ response = client.attach_network_interface(
97
+ network_interface_id: id,
98
+ instance_id: environment[:instance_id],
99
+ device_index: device.device_number
100
+ )
102
101
 
103
102
  if options[:block] || do_config || do_enable
104
103
  wait_for 'the interface to attach', rescue: ConnectionFailed do
105
- interface.exists? && interface_status(interface.interface_id) == 'in-use'
104
+ device.exists? && interface_status(device.interface_id) == 'in-use'
106
105
  end
107
106
  end
108
- interface.configure if do_config
109
- interface.enable if do_enable
107
+ device.configure if do_config
108
+ device.enable if do_enable
110
109
  {
111
- id: interface.interface_id,
112
- name: interface.name,
113
- configured: options[:configure],
114
- api_response: response
110
+ interface_id: device.interface_id,
111
+ device_name: device.name,
112
+ device_number: device.device_number,
113
+ enabled: do_enable,
114
+ configured: do_config,
115
+ api_response: response
115
116
  }
116
117
  end
117
118
 
118
119
  # detach network interface
119
120
  def detach_interface(id, options = {})
120
- interface = IFconfig.filter(id).first
121
- raise InvalidParameterError, "Interface #{interface.name} does not exist" unless interface && interface.exists?
122
- if options[:name] && interface.name != options[:name]
123
- raise InvalidParameterError, "Interface #{interface.interface_id} not found on #{options[:name]}"
124
- end
125
- if options[:device_number] && interface.device_number != options[:device_number].to_i
126
- raise InvalidParameterError, "Interface #{interface.interface_id} not found at index #{options[:device_number]}"
127
- end
121
+ device = IFconfig[id].assert(
122
+ exists: true,
123
+ device_name: options[:device_name],
124
+ interface_id: options[:interface_id],
125
+ device_number: options[:device_number]
126
+ )
127
+ interface_id = device.interface_id
128
128
 
129
- description = client.describe_network_interfaces(filters: [{
129
+ response = client.describe_network_interfaces(filters: [{
130
130
  name: 'attachment.instance-id',
131
131
  values: [environment[:instance_id]]
132
132
  },{
133
133
  name: 'network-interface-id',
134
- values: [interface.interface_id]
134
+ values: [interface_id]
135
135
  }])
136
- description = description[:network_interfaces].first
137
- raise UnknownInterfaceError, "Interface attachment could not be located" unless description
136
+ interface = response[:network_interfaces].first
137
+ raise UnknownInterfaceError, "Interface attachment could not be located" unless interface
138
138
 
139
- interface.disable
140
- interface.deconfigure
139
+ device.disable
140
+ device.deconfigure
141
141
  client.detach_network_interface(
142
- attachment_id: description[:attachment][:attachment_id],
142
+ attachment_id: interface[:attachment][:attachment_id],
143
143
  force: true
144
144
  )
145
- created_by_us = description.tag_set.any? { |tag| tag.key == 'created by' && tag.value == owner_tag }
145
+ created_by_us = interface.tag_set.any? { |tag| tag.key == 'created by' && tag.value == owner_tag }
146
146
  do_delete = options[:delete] || options[:delete].nil? && created_by_us
147
147
 
148
148
  if options[:block] || do_delete
149
149
  wait_for 'the interface to detach', interval: 0.3 do
150
- !interface.exists? && interface_status(description[:network_interface_id]) == 'available'
150
+ !device.exists? && interface_status(interface[:network_interface_id]) == 'available'
151
151
  end
152
152
  end
153
- client.delete_network_interface(network_interface_id: description[:network_interface_id]) if do_delete
153
+ client.delete_network_interface(network_interface_id: interface[:network_interface_id]) if do_delete
154
154
  {
155
- id: description[:network_interface_id],
156
- name: "eth#{description[:attachment][:device_index]}",
157
- device_number: description[:attachment][:device_index],
155
+ interface_id: interface_id,
156
+ device_name: device.name,
157
+ device_number: device.device_number,
158
158
  created_by_us: created_by_us,
159
159
  deleted: do_delete,
160
- api_response: description
160
+ api_response: interface
161
161
  }
162
162
  end
163
163
 
@@ -187,14 +187,13 @@ module Aws
187
187
 
188
188
  descriptions = client.describe_network_interfaces(filters: filters)
189
189
  interfaces = descriptions[:network_interfaces].select do |interface|
190
- skip = safe_mode && interface.tag_set.any? do |tag|
190
+ unless safe_mode && interface.tag_set.any? do |tag|
191
191
  begin
192
192
  tag.key == 'created on' && Time.now - Time.parse(tag.value) < 60
193
193
  rescue ArgumentError
194
194
  false
195
195
  end
196
196
  end
197
- unless skip
198
197
  client.delete_network_interface(network_interface_id: interface[:network_interface_id])
199
198
  true
200
199
  end
@@ -207,12 +206,53 @@ module Aws
207
206
  end
208
207
 
209
208
  # add new private ip using the AWS api and add it to our local ip config
210
- def assign_secondary_ip(interface, options = {})
211
- raise NoMethodError, "assign_secondary_ip not yet implemented"
209
+ def assign_secondary_ip(id, options = {})
210
+ device = IFconfig[id].assert(
211
+ exists: true,
212
+ device_name: options[:device_name],
213
+ interface_id: options[:interface_id],
214
+ device_number: options[:device_number]
215
+ )
216
+ interface_id = device.interface_id
217
+ current_ips = interface_ips(interface_id)
218
+ new_ip = options[:private_ip]
219
+
220
+ if new_ip = options[:private_ip]
221
+ if current_ips.include?(new_ip)
222
+ raise InvalidParameterError, "IP #{new_ip} already assigned to #{device.name}"
223
+ end
224
+ client.assign_private_ip_addresses(
225
+ network_interface_id: interface_id,
226
+ private_ip_addresses: [new_ip],
227
+ allow_reassignment: false
228
+ )
229
+ wait_for 'private ip address to be assigned' do
230
+ interface_ips(interface_id).include?(new_ip)
231
+ end
232
+ else
233
+ client.assign_private_ip_addresses(
234
+ network_interface_id: interface_id,
235
+ secondary_private_ip_address_count: 1,
236
+ allow_reassignment: false
237
+ )
238
+ wait_for 'new private ip address to be assigned' do
239
+ new_ips = interface_ips(interface_id) - current_ips
240
+ new_ip = new_ips.first if new_ips
241
+ end
242
+ end
243
+
244
+ unless options[:configure] == false
245
+ device.add_alias(new_ip)
246
+ if options[:block] && !IFconfig.test(new_ip, target: device.gateway)
247
+ raise TimeoutError, "Timed out waiting for ip address to become active"
248
+ end
249
+ end
212
250
  {
213
- private_ip: '0.0.0.0',
214
- device_name: 'eth0',
215
- interface_id: 'eni-1a2b3c4d'
251
+ private_ip: new_ip,
252
+ interface_id: interface_id,
253
+ device_name: device.name,
254
+ device_number: device.device_number,
255
+ interface_ips: current_ips << new_ip
216
256
  }
217
257
  end
218
258
 
@@ -232,14 +272,46 @@ module Aws
232
272
 
233
273
  # associate a private ip with an elastic ip through the AWS api
234
274
  def associate_elastic_ip(private_ip, options = {})
235
- raise NoMethodError, "associate_elastic_ip not yet implemented"
275
+ find = options[:device_name] || options[:device_number] || options[:interface_id] || private_ip
276
+ device = IFconfig[find].assert(
277
+ exists: true,
278
+ private_ip: private_ip,
279
+ device_name: options[:device_name],
280
+ interface_id: options[:interface_id],
281
+ device_number: options[:device_number]
282
+ )
283
+ options[:public_ip] ||= options[:allocation_id]
284
+
285
+ if public_ip = device.public_ips[private_ip]
286
+ raise InvalidParameterError, "IP #{private_ip} already has an associated EIP (#{public_ip})"
287
+ end
288
+
289
+ if options[:public_ip]
290
+ eip = describe_address(options[:public_ip])
291
+ if options[:allocation_id] && eip[:allocation_id] != options[:allocation_id]
292
+ raise InvalidParameterError, "EIP #{eip[:public_ip]} (#{eip[:allocation_id]}) does not match #{options[:allocation_id]}"
293
+ end
294
+ else
295
+ eip = allocate_elastic_ip
296
+ end
297
+
298
+ resp = client.associate_address(
299
+ network_interface_id: device.interface_id,
300
+ allocation_id: eip[:allocation_id],
301
+ private_ip_address: private_ip,
302
+ allow_reassociation: false
303
+ )
304
+
305
+ if options[:block] && !IFconfig.test(private_ip)
306
+ raise TimeoutError, "Timed out waiting for ip address to become active"
307
+ end
236
308
  {
237
- private_ip: '0.0.0.0',
238
- device_name: 'eth0',
239
- interface_id: 'eni-1a2b3c4d',
240
- public_ip: '0.0.0.0',
241
- allocation_id: 'eipalloc-1a2b3c4d',
242
- association_id: 'eipassoc-1a2b3c4d'
309
+ private_ip: private_ip,
310
+ device_name: device.name,
311
+ interface_id: device.interface_id,
312
+ public_ip: eip[:public_ip],
313
+ allocation_id: eip[:allocation_id],
314
+ association_id: resp[:association_id]
243
315
  }
244
316
  end
245
317
 
@@ -260,19 +332,23 @@ module Aws
260
332
 
261
333
  # allocate a new elastic ip address
262
334
  def allocate_elastic_ip
263
- raise NoMethodError, "allocate_elastic_ip not yet implemented"
335
+ eip = client.allocate_address(domain: 'vpc')
264
336
  {
265
- public_ip: '0.0.0.0',
266
- allocation_id: 'eipalloc-1a2b3c4d'
337
+ public_ip: eip[:public_ip],
338
+ allocation_id: eip[:allocation_id]
267
339
  }
268
340
  end
269
341
 
270
342
  # release the specified elastic ip address
271
- def release_elastic_ip(ip, options = {})
272
- raise NoMethodError, "release_elastic_ip not yet implemented"
343
+ def release_elastic_ip(ip)
344
+ eip = describe_address(ip)
345
+ if eip[:association_id]
346
+ raise AWSPermissionError, "Elastic IP #{eip[:public_ip]} (#{eip[:allocation_id]}) is currently in use"
347
+ end
348
+ client.release_address(allocation_id: eip[:allocation_id])
273
349
  {
274
- public_ip: '0.0.0.0',
275
- allocation_id: 'eipalloc-1a2b3c4d'
350
+ public_ip: eip[:public_ip],
351
+ allocation_id: eip[:allocation_id]
276
352
  }
277
353
  end
278
354
 
@@ -308,7 +384,21 @@ module Aws
308
384
  create_tags: {
309
385
  resources: ['eni-abcd1234'],
310
386
  tags: []
387
+ },
388
+ describe_addresses: nil,
389
+ allocate_address: nil,
390
+ release_address: {
391
+ dry_run: true,
392
+ allocation_id: 'eipalloc-no_exist'
393
+ },
394
+ associate_address: {
395
+ allocation_id: 'eipalloc-no_exist',
396
+ network_interface_id: 'eni-abcd1234'
311
397
  }
398
+ # has no dry_run method
399
+ # assign_private_ip_addresses: {
400
+ # network_interface_id: 'eni-abcd1234'
401
+ # }
312
402
  }
313
403
  test_methods.each do |method, params|
314
404
  begin
@@ -318,6 +408,8 @@ module Aws
318
408
  raise Error, "Unexpected behavior while testing AWS API access"
319
409
  rescue Aws::EC2::Errors::DryRunOperation
320
410
  # success
411
+ rescue Aws::EC2::Errors::InvalidAllocationIDNotFound
412
+ # release_address does not properly support dry_run
321
413
  rescue Aws::EC2::Errors::UnauthorizedOperation
322
414
  return false
323
415
  end
@@ -331,6 +423,29 @@ module Aws
331
423
 
332
424
  private
333
425
 
426
+ # use either an ip address or allocation id
427
+ def describe_address(address)
428
+ filter_by = address =~ Resolv::IPv4::Regex ? 'public-ip' : 'allocation-id'
429
+ resp = client.describe_addresses(filters: [
430
+ { name: 'domain', values: ['vpc'] },
431
+ { name: filter_by, values: [address] }
432
+ ])
433
+ raise InvalidParameterError, "IP #{address} could not be located" if resp[:addresses].empty?
434
+ resp[:addresses].first
435
+ end
436
+
437
+ def interface_ips(id)
438
+ resp = client.describe_network_interfaces(network_interface_ids: [id])
439
+ interface = resp[:network_interfaces].first
440
+ if interface && interface[:private_ip_addresses]
441
+ primary = interface[:private_ip_addresses].find { |ip| ip[:primary] }
442
+ interface[:private_ip_addresses].map { |ip| ip[:private_ip_address] }.tap do |ips|
443
+ # ensure primary ip is first in the list
444
+ ips.unshift(*ips.delete(primary[:private_ip_address])) if primary
445
+ end
446
+ end
447
+ end
448
+
334
449
  def interface_status(id)
335
450
  resp = client.describe_network_interfaces(network_interface_ids: [id])
336
451
  resp[:network_interfaces].first[:status] unless resp[:network_interfaces].empty?
@@ -13,10 +13,23 @@ module Aws
13
13
 
14
14
  # Array-like accessor to automatically instantiate our class
15
15
  def [](index)
16
- index = $1.to_i if index.to_s =~ /^(?:eth)?([0-9]+)$/
17
- index ||= next_available_index
18
- @instance_cache ||= []
19
- @instance_cache[index] ||= new("eth#{index}", false)
16
+ case index
17
+ when Integer
18
+ @instance_cache ||= []
19
+ @instance_cache[index] ||= new("eth#{index}", false)
20
+ when nil
21
+ self[next_available_index]
22
+ when /^(?:eth)?([0-9]+)$/
23
+ self[$1.to_i]
24
+ when /^eni-/
25
+ find { |dev| dev.instance_id == index }
26
+ when /^[0-9a-f:]+$/i
27
+ find { |dev| dev.hwaddr.casecmp(index) == 0 }
28
+ when /^[0-9\.]+$/
29
+ find { |dev| dev.has_ip?(index) }
30
+ end.tap do |dev|
31
+ raise UnknownInterfaceError, "No interface found matching #{index}" unless dev
32
+ end
20
33
  end
21
34
 
22
35
  # Purge and deconfigure non-existent interfaces from the cache
@@ -25,11 +38,6 @@ module Aws
25
38
  @instance_cache.map!{ |dev| dev if dev.exists? }
26
39
  end
27
40
 
28
- # Return array of available ethernet interfaces
29
- def existing
30
- Dir.entries("/sys/class/net/").grep(/^eth[0-9]+$/){ |name| self[name] }
31
- end
32
-
33
41
  # Return the next unused device index
34
42
  def next_available_index
35
43
  for index in 0..32 do
@@ -39,7 +47,7 @@ module Aws
39
47
 
40
48
  # Iterate over available ethernet interfaces (required for Enumerable)
41
49
  def each(&block)
42
- existing.each(&block)
50
+ Dir.entries("/sys/class/net/").grep(/^eth[0-9]+$/){ |name| self[name] }.each(&block)
43
51
  end
44
52
 
45
53
  # Return array of enabled interfaces
@@ -63,38 +71,65 @@ module Aws
63
71
 
64
72
  # Return an array of available interfaces identified by name, id,
65
73
  # hwaddr, or subnet id.
66
- def filter(match = nil)
67
- return existing unless match
68
- select{ |dev| dev.is?(match) }.tap do |result|
69
- raise UnknownInterfaceError, "No interface found matching \"#{match}\"" if result.empty?
74
+ def filter(filter = nil)
75
+ case filter
76
+ when nil
77
+ to_a
78
+ when /^eni-/, /^eth[0-9]+$/, /^[0-9a-f:]+$/i, /^[0-9\.]+$/
79
+ self[filter].to_a
80
+ when /^subnet-/
81
+ select { |dev| dev.subnet_id == filter }
82
+ end.tap do |devs|
83
+ raise UnknownInterfaceError, "No interface found matching #{filter}" if devs.nil? || devs.empty?
70
84
  end
71
85
  end
72
86
 
73
87
  # Test whether we have permission to run RTNETLINK commands
74
88
  def mutable?
75
- exec 'link set dev eth0' # random innocuous command
89
+ cmd('link set dev eth0') # innocuous command
76
90
  true
77
91
  rescue PermissionError
78
92
  false
79
93
  end
80
94
 
81
- # Execute a command
95
+ # Execute an 'ip' command
96
+ def cmd(command, options = {})
97
+ errors = options[:errors]
98
+ options[:errors] = true
99
+ begin
100
+ exec("/sbin/ip #{command}", options)
101
+ rescue CommandError => e
102
+ case e.message
103
+ when /operation not permitted/i
104
+ raise PermissionError, "Operation not permitted"
105
+ else
106
+ raise if errors
107
+ end
108
+ end
109
+ end
110
+
111
+ # Test connectivity from a given ip address
112
+ def test(ip, options = {})
113
+ timeout = Integer(options[:timeout] || 30)
114
+ target = options[:target] || '8.8.8.8'
115
+ !!exec("ping -w #{timeout} -c 1 -I #{ip} #{target}")
116
+ end
117
+
118
+ # Execute a command, returns output as string or nil on error
82
119
  def exec(command, options = {})
83
120
  output = nil
121
+ errors = options[:errors]
84
122
  verbose = self.verbose || options[:verbose]
85
- raise_errors = options[:raise_errors]
86
- puts "ip #{command}" if verbose
87
-
88
- Open3.popen3("/sbin/ip #{command}") do |i,o,e,t|
89
- unless t.value.success?
90
- error_msg = e.read
91
- if error_msg =~ /operation not permitted/i
92
- raise PermissionError, "Operation not permitted"
93
- end
94
- warn "Warning: #{error_msg}" if verbose
95
- raise CommandError, error_msg if raise_errors
123
+
124
+ puts command if verbose
125
+ Open3.popen3(command) do |i,o,e,t|
126
+ if t.value.success?
127
+ output = o.read
128
+ else
129
+ error = e.read
130
+ warn "Warning: #{error}" if verbose
131
+ raise CommandError, error if errors
96
132
  end
97
- output = o.read
98
133
  end
99
134
  output
100
135
  end
@@ -159,10 +194,18 @@ module Aws
159
194
  info[:subnet_cidr]
160
195
  end
161
196
 
197
+ def gateway
198
+ IPAddr.new(subnet_cidr).succ.to_s
199
+ end
200
+
201
+ def prefix
202
+ subnet_cidr.split('/').last.to_i
203
+ end
204
+
162
205
  # Return an array of configured ip addresses (primary + secondary)
163
206
  def local_ips
164
- list = exec("addr show dev #{name} primary") +
165
- exec("addr show dev #{name} secondary")
207
+ list = cmd("addr show dev #{name} primary") +
208
+ cmd("addr show dev #{name} secondary")
166
209
  list.lines.grep(/inet ([0-9\.]+)\/.* #{name}/i){ $1 }
167
210
  end
168
211
 
@@ -180,29 +223,29 @@ module Aws
180
223
  ip_assoc
181
224
  end
182
225
 
183
- # Enable our interface
226
+ # Enable our interface and create necessary routes
184
227
  def enable
185
- exec("link set dev #{name} up")
228
+ cmd("link set dev #{name} up")
229
+ cmd("route add default via #{gateway} dev #{name} table #{route_table}")
230
+ cmd("route flush cache")
186
231
  end
187
232
 
188
233
  # Disable our interface
189
234
  def disable
190
- exec("link set dev #{name} down")
235
+ cmd("link set dev #{name} down")
191
236
  end
192
237
 
193
238
  # Check whether our interface is enabled
194
239
  def enabled?
195
- exists? && exec("link show up").include?(name)
240
+ exists? && cmd("link show up").include?(name)
196
241
  end
197
242
 
198
243
  # Initialize a new interface config
199
244
  def configure(dry_run = false)
200
245
  changes = 0
201
- info = self.info
202
- prefix = info[:subnet_cidr].split('/').last.to_i
203
- gateway = IPAddr.new(info[:subnet_cidr]).succ.to_s
246
+ prefix = self.prefix # prevent exists? check on each use
204
247
 
205
- meta_ips = Meta.get("network/interfaces/macs/#{info[:hwaddr]}/local-ipv4s").lines.map(&:strip)
248
+ meta_ips = Meta.get("network/interfaces/macs/#{hwaddr}/local-ipv4s").lines.map(&:strip)
206
249
  local_primary, *local_aliases = local_ips
207
250
  meta_primary, *meta_aliases = meta_ips
208
251
 
@@ -210,35 +253,34 @@ module Aws
210
253
  if name != 'eth0' && local_primary != meta_primary
211
254
  unless dry_run
212
255
  deconfigure
213
- exec("addr add #{meta_primary}/#{prefix} brd + dev #{name}")
214
- exec("route add default via #{gateway} dev #{name} table #{route_table}")
215
- exec("route flush cache")
256
+ cmd("addr add #{meta_primary}/#{prefix} brd + dev #{name}")
216
257
  end
217
258
  changes += 1
218
259
  end
219
260
 
220
261
  # add missing secondary ips
221
262
  (meta_aliases - local_aliases).each do |ip|
222
- exec("addr add #{ip}/#{prefix} brd + dev #{name}") unless dry_run
263
+ cmd("addr add #{ip}/#{prefix} brd + dev #{name}") unless dry_run
223
264
  changes += 1
224
265
  end
225
266
 
226
267
  # remove extra secondary ips
227
268
  (local_aliases - meta_aliases).each do |ip|
228
- exec("addr del #{ip}/#{prefix} dev #{name}") unless dry_run
269
+ cmd("addr del #{ip}/#{prefix} dev #{name}") unless dry_run
229
270
  changes += 1
230
271
  end
231
272
 
273
+ # add and remove source-ip based rules
232
274
  unless name == 'eth0'
233
275
  rules_to_add = meta_ips || []
234
- exec("rule list").lines.grep(/^([0-9]+):.*\s([0-9\.]+)\s+lookup #{route_table}/) do
276
+ cmd("rule list").lines.grep(/^([0-9]+):.*\s([0-9\.]+)\s+lookup #{route_table}/) do
235
277
  unless rules_to_add.delete($2)
236
- exec("rule delete pref #{$1}") unless dry_run
278
+ cmd("rule delete pref #{$1}") unless dry_run
237
279
  changes += 1
238
280
  end
239
281
  end
240
282
  rules_to_add.each do |ip|
241
- exec("rule add from #{ip} lookup #{route_table}") unless dry_run
283
+ cmd("rule add from #{ip} lookup #{route_table}") unless dry_run
242
284
  changes += 1
243
285
  end
244
286
  end
@@ -251,67 +293,105 @@ module Aws
251
293
  def deconfigure
252
294
  # assume eth0 primary ip is managed by dhcp
253
295
  if name == 'eth0'
254
- exec("addr flush dev eth0 secondary")
296
+ cmd("addr flush dev eth0 secondary")
255
297
  else
256
- exec("rule list").lines.grep(/^([0-9]+):.*lookup #{route_table}/) do
257
- exec("rule delete pref #{$1}")
298
+ cmd("rule list").lines.grep(/^([0-9]+):.*lookup #{route_table}/) do
299
+ cmd("rule delete pref #{$1}")
258
300
  end
259
- exec("addr flush dev #{name}")
260
- exec("route flush table #{route_table}")
261
- exec("route flush cache")
301
+ cmd("addr flush dev #{name}")
302
+ cmd("route flush table #{route_table}")
303
+ cmd("route flush cache")
262
304
  end
263
305
  @clean = true
264
306
  end
265
307
 
266
308
  # Add a secondary ip to this interface
267
309
  def add_alias(ip)
268
- prefix = info[:subnet_cidr].split('/').last.to_i
269
- exec("addr add #{ip}/#{prefix} brd + dev #{name}")
270
-
271
- unless name == 'eth0' || exec("rule list") =~ /from #{ip} lookup #{route_table}/
272
- exec("rule add from #{ip} lookup #{route_table}")
310
+ cmd("addr add #{ip}/#{prefix} brd + dev #{name}")
311
+ unless name == 'eth0' || cmd("rule list").include?("from #{ip} lookup #{route_table}")
312
+ cmd("rule add from #{ip} lookup #{route_table}")
273
313
  end
274
314
  end
275
315
 
276
316
  # Remove a secondary ip from this interface
277
317
  def remove_alias
278
- prefix = info[:subnet_cidr].split('/').last.to_i
279
- exec("addr del #{ip}/#{prefix} dev #{name}")
280
-
281
- if name != 'eth0' && exec("rule list") =~ /([0-9]+):\s+from #{ip} lookup #{route_table}/
282
- exec("rule delete pref #{$1}")
318
+ cmd("addr del #{ip}/#{prefix} dev #{name}")
319
+ unless name == 'eth0' || !cmd("rule list").match(/([0-9]+):\s+from #{ip} lookup #{route_table}/)
320
+ cmd("rule delete pref #{$1}")
283
321
  end
284
322
  end
285
323
 
286
- # Identify this interface by one of its attributes
287
- def is?(match)
288
- if match == name
289
- true
324
+ # Return true if the ip address is associated with this interface
325
+ def has_ip?(ip_addr)
326
+ if IPAddr.new(subnet_cidr) === IPAddr.new(ip_addr)
327
+ # ip within subnet
328
+ local_ips.include? ip_addr
290
329
  else
291
- info = self.info
292
- match == info[:interface_id] || match == info[:hwaddr] || match == info[:subnet_id]
330
+ # ip outside subnet
331
+ public_ips.has_value? ip_addr
332
+ end
333
+ end
334
+
335
+ # Throw exception unless this interface matches the provided attributes
336
+ # else returns self
337
+ def assert(attr)
338
+ error = nil
339
+ attr.find do |attr,val|
340
+ next if val.nil?
341
+ error = case attr
342
+ when :exists
343
+ if val
344
+ "The specified interface does not exist." unless exists?
345
+ else
346
+ "Interface #{name} exists." if exists?
347
+ end
348
+ when :enabled
349
+ if val
350
+ "Interface #{name} is not enabled." unless enabled?
351
+ else
352
+ "Interface #{name} is not disabled." if enabled?
353
+ end
354
+ when :name, :device_name
355
+ "The specified interface does not match" unless name == val
356
+ when :index, :device_index, :device_number
357
+ "Interface #{name} is device number #{val}" unless device_number == val.to_i
358
+ when :hwaddr
359
+ "Interface #{name} does not match hwaddr #{val}" unless hwaddr == val
360
+ when :interface_id
361
+ "Interface #{name} does not have interface id #{val}" unless interface_id == val
362
+ when :subnet_id
363
+ "Interface #{name} does not have subnet id #{val}" unless subnet_id == val
364
+ when :ip, :has_ip
365
+ "Interface #{name} does not have IP #{val}" unless has_ip? val
366
+ when :public_ip
367
+ "Interface #{name} does not have private IP #{val}" unless public_ips.include? val
368
+ when :local_ip, :private_ip
369
+ "Interface #{name} does not have private IP #{val}" unless local_ips.include? val
370
+ else
371
+ "Unknown attribute: #{attr}"
372
+ end
293
373
  end
374
+ raise UnknownInterfaceError, error if error
375
+ self
294
376
  end
295
377
 
296
378
  # Return an array representation of our interface config, including public
297
379
  # ip associations and enabled status
298
380
  def to_h
299
- info.merge({
381
+ info.merge(
300
382
  name: name,
301
383
  device_number: device_number,
302
384
  route_table: route_table,
303
385
  local_ips: local_ips,
304
386
  public_ips: public_ips,
305
387
  enabled: enabled?
306
- })
388
+ )
307
389
  end
308
390
 
309
391
  private
310
392
 
311
393
  # Alias for static method
312
- def exec(command, options = {})
313
- self.class.exec(command, options)
314
- end
394
+ def cmd(*args) self.class.cmd(*args) end
315
395
  end
316
396
  end
317
397
  end
@@ -1,5 +1,5 @@
1
1
  module Aws
2
2
  module ENI
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
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.1.2
4
+ version: 0.1.3
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-04-28 00:00:00.000000000 Z
11
+ date: 2015-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gli