aws-eni 0.1.0 → 0.1.1

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
  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