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 +4 -4
- data/bin/aws-eni +53 -7
- data/lib/aws-eni.rb +100 -22
- data/lib/aws-eni/errors.rb +3 -0
- data/lib/aws-eni/ifconfig.rb +9 -1
- data/lib/aws-eni/meta.rb +1 -4
- data/lib/aws-eni/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34f9ba0cdfc6a898ddf6b4ec24f72b04187d4b49
|
4
|
+
data.tar.gz: a0de204e867336cb1d3293a4d13fab1ab572b45e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80b6c15ac5abdd73f50b9811a20d6494ac5c4df28afc14ebedcadb9398424d470d81eba1de018da964b0926aad7bfa98903a1440d6cc5b29a4b470d040e18bcd
|
7
|
+
data.tar.gz: 3bc10d67d60cd4afaf30146e293f7acfcc0de280f09e863796de7e03297c5cd5712ba8b987cad90c640af8c7c136343b3cb77f780f3eddaadd99888985a38810
|
data/bin/aws-eni
CHANGED
@@ -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 [:
|
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 '
|
235
|
-
c.switch [:
|
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:
|
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
|
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|
|
data/lib/aws-eni.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
131
|
-
|
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:
|
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(
|
271
|
-
|
272
|
-
|
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
|
-
|
353
|
+
raise TimeoutError, "Timed out waiting for #{task}" unless timeout > 0
|
276
354
|
end
|
277
355
|
end
|
278
356
|
end
|
data/lib/aws-eni/errors.rb
CHANGED
data/lib/aws-eni/ifconfig.rb
CHANGED
@@ -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
|
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"),
|
data/lib/aws-eni/meta.rb
CHANGED
@@ -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.
|
data/lib/aws-eni/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2015-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gli
|