aws-eni 0.1.0 → 0.1.1

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: b7f9bdc9bb5d6c7427d50f1354b77317a5a4f1bc
4
- data.tar.gz: fe7568417322ab7bfa41be38041efaf1e0aeeae7
3
+ metadata.gz: 34f9ba0cdfc6a898ddf6b4ec24f72b04187d4b49
4
+ data.tar.gz: a0de204e867336cb1d3293a4d13fab1ab572b45e
5
5
  SHA512:
6
- metadata.gz: f86aba30d2b1b0e564810648c701f144c098388d8e349ae1143817b4ee58a07c3d86008be3b79b8e9cca66bc5a92ca801b68907711914a32eeece675894f30f4
7
- data.tar.gz: bd60223a0bc09962fb82db2cd7c413da1a08f37ff603a85b716723eaa3fecbf983de317155e8c451badf50063fa59df417d3f7c3e7b6df181651026e2be6696f
6
+ metadata.gz: 80b6c15ac5abdd73f50b9811a20d6494ac5c4df28afc14ebedcadb9398424d470d81eba1de018da964b0926aad7bfa98903a1440d6cc5b29a4b470d040e18bcd
7
+ data.tar.gz: 3bc10d67d60cd4afaf30146e293f7acfcc0de280f09e863796de7e03297c5cd5712ba8b987cad90c640af8c7c136343b3cb77f780f3eddaadd99888985a38810
@@ -8,7 +8,7 @@ include GLI::App
8
8
 
9
9
  program_desc 'Manage and sync local network config with AWS Elastic Network Interfaces'
10
10
 
11
- version Aws::ENI::VERSION
11
+ @version = Aws::ENI::VERSION
12
12
 
13
13
  autocomplete_commands true
14
14
  subcommand_option_handling :normal
@@ -17,8 +17,11 @@ sort_help :manually
17
17
 
18
18
  # global options
19
19
 
20
+ desc 'Display the program version'
21
+ switch [:v,:version], negatable: false
22
+
20
23
  desc 'Display all system commands and warnings'
21
- switch [:v,:verbose], negatable: false
24
+ switch [:V,:verbose], negatable: false
22
25
 
23
26
  pre do |opt|
24
27
  Aws::ENI::IFconfig.verbose = opt[:verbose]
@@ -207,6 +210,7 @@ arg 'security-groups', :optional
207
210
  arg 'ip-address', :optional
208
211
  command [:create] do |c|
209
212
  c.action do |global,opts,args|
213
+ args.delete('new')
210
214
  params = parse_args args, :subnet_id, :security_groups, :primary_ip
211
215
  interface = Aws::ENI.create_interface(params)
212
216
  puts "interface #{interface[:id]} created on #{interface[:subnet_id]}"
@@ -231,20 +235,26 @@ arg 'subnet-id', :optional
231
235
  arg 'security-groups', :optional
232
236
  arg 'ip-address', :optional
233
237
  command [:attach] do |c|
234
- c.desc 'Refresh the interface configuration after attachment'
235
- c.switch [:r,:c,:config], negatable: false
238
+ c.desc 'Do not configure or enable the device after attachment (implies block)'
239
+ c.switch [:n,'no-config'], negatable: false
240
+
241
+ c.desc 'Do not return until attachment is complete'
242
+ c.switch [:b,:block], negatable: false
236
243
 
237
244
  c.action do |global,opts,args|
245
+ config = !opts['no_config']
238
246
  if args.first =~ /^eni-/
239
247
  help_now! 'Too many arguments' if args.count > 1
240
248
  id = args.first
241
249
  else
250
+ args.delete('new')
242
251
  params = parse_args args, :subnet_id, :security_groups, :primary_ip
252
+ Aws::ENI.assert_ifconfig_access if config
243
253
  interface = Aws::ENI.create_interface(params)
244
254
  puts "interface #{interface[:id]} created on #{interface[:subnet_id]}"
245
255
  id = interface[:id]
246
256
  end
247
- device = Aws::ENI.attach_interface(id, enable: opts[:config], configure: opts[:config])
257
+ device = Aws::ENI.attach_interface(id, enable: config, configure: config, block: options[:block])
248
258
  puts "interface #{device[:id]} attached to #{device[:name]}"
249
259
  puts "device #{device[:name]} enabled and configured" if opts[:config]
250
260
  end
@@ -262,18 +272,22 @@ long_desc %{
262
272
  }
263
273
  arg 'interface-id OR device-name'
264
274
  command [:detach] do |c|
265
- c.desc 'Delete (or preserve) the unused ENI resource after dataching'
275
+ c.desc 'Delete (or preserve) the unused ENI resource after dataching (implies block)'
266
276
  c.switch [:d,:delete], default_value: :not_provided # GLI behavior workaround
267
277
 
278
+ c.desc 'Do not return until detachment is complete'
279
+ c.switch [:b,:block], negatable: false
280
+
268
281
  c.action do |global,opts,args|
269
282
  help_now! "Missing argument" if args.empty?
270
283
  params = parse_args args, :interface_id, :device_name
284
+ params[:block] = opts[:block]
271
285
  params[:delete] = opts[:delete] unless opts[:delete] == :not_provided
272
286
  id = params[:interface_id] || params[:device_name]
273
287
 
274
288
  device = Aws::ENI.detach_interface(id, params)
275
289
  if device[:deleted]
276
- puts "interface #{device[:id]} detached from #{device[:name]} and destroyed"
290
+ puts "interface #{device[:id]} detached from #{device[:name]} and deleted"
277
291
  else
278
292
  puts "interface #{device[:id]} detached from #{device[:name]}"
279
293
  end
@@ -368,6 +382,7 @@ arg 'device-name', :optional
368
382
  command [:associate] do |c|
369
383
  c.action do |global,opts,args|
370
384
  help_now! "Missing argument" if args.empty?
385
+ args.delete('new')
371
386
  params = parse_args args, :private_ip, :public_ip, :allocation_id, :interface_id, :device_name
372
387
  assoc = Aws::ENI.associate_elastic_ip(params[:private_ip], params)
373
388
  puts "EIP #{assoc[:public_ip]} (#{assoc[:allocation_id]}) associated with #{assoc[:private_ip]} on #{assoc[:device_name]} (#{assoc[:interface_id]})"
@@ -405,6 +420,8 @@ long_desc %{
405
420
  }
406
421
  command [:allocate] do |c|
407
422
  c.action do |global,opts,args|
423
+ args.delete('new')
424
+ help_now! "Invalid argument: #{args.first}" unless args.empty?
408
425
  alloc = Aws::ENI.allocate_elastic_ip
409
426
  puts "EIP #{alloc[:public_ip]} allocated as #{alloc[:allocation_id]}"
410
427
  end
@@ -427,6 +444,35 @@ command [:release] do |c|
427
444
  end
428
445
  end
429
446
 
447
+ desc 'Test access to AWS EC2 and our machine\'s network config'
448
+ long_desc %{
449
+ Check for sufficient privileges to alter the local machine's network interface
450
+ configuration and verify that the AWS access credentials include permissions
451
+ necessary to perform all network related functions.
452
+ }
453
+ command [:test] do |c|
454
+ c.action do |global,opts,args|
455
+ help_now! "Too many arguments" if args.count > 1
456
+
457
+ print 'IFconfig permissions test... '
458
+ if Aws::ENI.can_modify_ifconfig?
459
+ puts 'success!'
460
+ else
461
+ puts 'failed'
462
+ puts "- unable to modify network configuration with /sbin/ip (try sudo)"
463
+ end
464
+
465
+ print 'AWS EC2 permissions test... '
466
+ if Aws::ENI.can_access_ec2?
467
+ puts 'success!'
468
+ else
469
+ puts 'failed'
470
+ puts "- insufficient EC2 access. Ensure you have granted access to the"
471
+ puts " appropriate EC2 methods in your IAM policy (see documentation)"
472
+ end
473
+ end
474
+ end
475
+
430
476
  # error handling
431
477
 
432
478
  on_error do |exception|
@@ -23,13 +23,18 @@ module Aws
23
23
  raise EnvironmentError, "Unable to detect VPC settings, library incompatible with EC2-Classic"
24
24
  end
25
25
  end.freeze
26
- rescue Meta::ConnectionFailed
26
+ rescue ConnectionFailed
27
27
  raise EnvironmentError, "Unable to load EC2 meta-data"
28
28
  end
29
29
 
30
30
  def owner_tag(new_owner = nil)
31
31
  @owner_tag = new_owner.to_s if new_owner
32
- @owner_tag || 'aws-eni script'
32
+ @owner_tag ||= 'aws-eni script'
33
+ end
34
+
35
+ def timeout(new_default = nil)
36
+ @timeout = new_default.to_i if new_default
37
+ @timeout ||= 30
33
38
  end
34
39
 
35
40
  def client
@@ -63,11 +68,15 @@ module Aws
63
68
  params[:description] = "generated by #{owner_tag} from #{environment[:instance_id]} on #{timestamp}"
64
69
 
65
70
  response = client.create_network_interface(params)
66
- client.create_tags(resources: [response[:network_interface][:network_interface_id]], tags: [
67
- { key: 'created by', value: owner_tag },
68
- { key: 'created on', value: timestamp },
69
- { key: 'created from', value: environment[:instance_id] }
70
- ])
71
+ wait_for 'the interface to be created', rescue: Aws::EC2::Errors::ServiceError do
72
+ if interface_status(response[:network_interface][:network_interface_id]) == 'available'
73
+ client.create_tags(resources: [response[:network_interface][:network_interface_id]], tags: [
74
+ { key: 'created by', value: owner_tag },
75
+ { key: 'created on', value: timestamp },
76
+ { key: 'created from', value: environment[:instance_id] }
77
+ ])
78
+ end
79
+ end
71
80
  {
72
81
  id: response[:network_interface][:network_interface_id],
73
82
  subnet_id: response[:network_interface][:subnet_id],
@@ -77,6 +86,10 @@ module Aws
77
86
 
78
87
  # attach network interface
79
88
  def attach_interface(id, options = {})
89
+ do_enable = true unless options[:enable] == false
90
+ do_config = true unless options[:configure] == false
91
+ assert_ifconfig_access if do_config || do_enable
92
+
80
93
  interface = IFconfig[options[:device_number] || options[:name]]
81
94
  raise InvalidParameterError, "Interface #{interface.name} is already in use" if interface.exists?
82
95
 
@@ -86,10 +99,14 @@ module Aws
86
99
  params[:device_index] = interface.device_number
87
100
 
88
101
  response = client.attach_network_interface(params)
89
- attached = wait_for(10) { interface.exists? }
90
- raise TimeoutError, "Timed out waiting for the interface to attach" unless attached
91
- interface.configure if options[:configure]
92
- interface.enable if options[:enable]
102
+
103
+ if options[:block] || do_config || do_enable
104
+ wait_for 'the interface to attach', rescue: ConnectionFailed do
105
+ interface.exists? && interface_status(interface.interface_id) == 'in-use'
106
+ end
107
+ end
108
+ interface.configure if do_config
109
+ interface.enable if do_enable
93
110
  {
94
111
  id: interface.interface_id,
95
112
  name: interface.name,
@@ -125,22 +142,21 @@ module Aws
125
142
  attachment_id: description[:attachment][:attachment_id],
126
143
  force: true
127
144
  )
128
- deleted = false
129
145
  created_by_us = description.tag_set.any? { |tag| tag.key == 'created by' && tag.value == owner_tag }
130
- unless options[:delete] == false || options[:delete].nil? && !created_by_us
131
- detached = wait_for(10, 0.3) do
146
+ do_delete = options[:delete] || options[:delete].nil? && created_by_us
147
+
148
+ if options[:block] || do_delete
149
+ wait_for 'the interface to detach', interval: 0.3 do
132
150
  !interface.exists? && interface_status(description[:network_interface_id]) == 'available'
133
151
  end
134
- raise TimeoutError, "Timed out waiting for the interface to detach" unless detached
135
- client.delete_network_interface(network_interface_id: description[:network_interface_id])
136
- deleted = true
137
152
  end
153
+ client.delete_network_interface(network_interface_id: description[:network_interface_id]) if do_delete
138
154
  {
139
155
  id: description[:network_interface_id],
140
156
  name: "eth#{description[:attachment][:device_index]}",
141
157
  device_number: description[:attachment][:device_index],
142
158
  created_by_us: created_by_us,
143
- deleted: deleted,
159
+ deleted: do_delete,
144
160
  api_response: description
145
161
  }
146
162
  end
@@ -260,6 +276,59 @@ module Aws
260
276
  }
261
277
  end
262
278
 
279
+ # test whether we have permission to modify our local configuration
280
+ def can_modify_ifconfig?
281
+ IFconfig.mutable?
282
+ end
283
+
284
+ def assert_ifconfig_access
285
+ raise PermissionError, 'Insufficient user priveleges (try sudo)' unless can_modify_ifconfig?
286
+ end
287
+
288
+ # test whether we have the appropriate permissions within our AWS access
289
+ # credentials to perform all possible API calls
290
+ def can_access_ec2?
291
+ client = self.client
292
+ test_methods = {
293
+ describe_network_interfaces: nil,
294
+ create_network_interface: {
295
+ subnet_id: 'subnet-abcd1234'
296
+ },
297
+ attach_network_interface: {
298
+ network_interface_id: 'eni-abcd1234',
299
+ instance_id: 'i-abcd1234',
300
+ device_index: 0
301
+ },
302
+ detach_network_interface: {
303
+ attachment_id: 'eni-attach-abcd1234'
304
+ },
305
+ delete_network_interface: {
306
+ network_interface_id: 'eni-abcd1234'
307
+ },
308
+ create_tags: {
309
+ resources: ['eni-abcd1234'],
310
+ tags: []
311
+ }
312
+ }
313
+ test_methods.each do |method, params|
314
+ begin
315
+ params ||= {}
316
+ params[:dry_run] = true
317
+ client.public_send(method, params)
318
+ raise Error, "Unexpected behavior while testing AWS API access"
319
+ rescue Aws::EC2::Errors::DryRunOperation
320
+ # success
321
+ rescue Aws::EC2::Errors::UnauthorizedOperation
322
+ return false
323
+ end
324
+ end
325
+ true
326
+ end
327
+
328
+ def assert_ec2_access
329
+ raise AWSPermissionError, 'Insufficient AWS API access' unless can_access_ec2?
330
+ end
331
+
263
332
  private
264
333
 
265
334
  def interface_status(id)
@@ -267,12 +336,21 @@ module Aws
267
336
  resp[:network_interfaces].first[:status] unless resp[:network_interfaces].empty?
268
337
  end
269
338
 
270
- def wait_for(timer = 5, interval = 0.1, &block)
271
- until timer < 0 or block.call
272
- timer -= interval
339
+ def wait_for(task, options = {}, &block)
340
+ errors = [*options[:rescue]]
341
+ timeout = options[:timeout] || self.timeout
342
+ interval = options[:interval] || 0.1
343
+
344
+ until timeout < 0
345
+ begin
346
+ break if block.call
347
+ rescue Exception => e
348
+ raise unless errors.any? { |error| error === e }
349
+ end
273
350
  sleep interval
351
+ timeout -= interval
274
352
  end
275
- timer > 0
353
+ raise TimeoutError, "Timed out waiting for #{task}" unless timeout > 0
276
354
  end
277
355
  end
278
356
  end
@@ -9,5 +9,8 @@ module Aws
9
9
  class EnvironmentError < Error; end
10
10
  class CommandError < Error; end
11
11
  class PermissionError < CommandError; end
12
+ class AWSPermissionError < Error; end
13
+ class BadResponse < Error; end
14
+ class ConnectionFailed < Error; end
12
15
  end
13
16
  end
@@ -70,6 +70,14 @@ module Aws
70
70
  end
71
71
  end
72
72
 
73
+ # Test whether we have permission to run RTNETLINK commands
74
+ def mutable?
75
+ exec 'link set dev eth0' # random innocuous command
76
+ true
77
+ rescue PermissionError
78
+ false
79
+ end
80
+
73
81
  # Execute a command
74
82
  def exec(command, options = {})
75
83
  output = nil
@@ -127,7 +135,7 @@ module Aws
127
135
  unless @meta_cache && hwaddr == @meta_cache[:hwaddr]
128
136
  dev_path = "network/interfaces/macs/#{hwaddr}"
129
137
  Meta.open_connection do |conn|
130
- raise Meta::BadResponse unless Meta.http_get(conn, "#{dev_path}/")
138
+ raise BadResponse unless Meta.http_get(conn, "#{dev_path}/")
131
139
  @meta_cache = {
132
140
  hwaddr: hwaddr,
133
141
  interface_id: Meta.http_get(conn, "#{dev_path}/interface-id"),
@@ -1,5 +1,6 @@
1
1
  require 'time'
2
2
  require 'net/http'
3
+ require 'aws-eni/errors'
3
4
 
4
5
  module Aws
5
6
  module ENI
@@ -10,10 +11,6 @@ module Aws
10
11
  PORT = '80'
11
12
  BASE = '/latest/meta-data/'
12
13
 
13
- # Custom exception classes
14
- class BadResponse < RuntimeError; end
15
- class ConnectionFailed < RuntimeError; end
16
-
17
14
  # These are the errors we trap when attempting to talk to the instance
18
15
  # metadata service. Any of these imply the service is not present, no
19
16
  # responding or some other non-recoverable error.
@@ -1,5 +1,5 @@
1
1
  module Aws
2
2
  module ENI
3
- VERSION = "0.1.0"
3
+ VERSION = "0.1.1"
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.0
4
+ version: 0.1.1
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-27 00:00:00.000000000 Z
11
+ date: 2015-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gli