aws-eni 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b7f9bdc9bb5d6c7427d50f1354b77317a5a4f1bc
4
+ data.tar.gz: fe7568417322ab7bfa41be38041efaf1e0aeeae7
5
+ SHA512:
6
+ metadata.gz: f86aba30d2b1b0e564810648c701f144c098388d8e349ae1143817b4ee58a07c3d86008be3b79b8e9cca66bc5a92ca801b68907711914a32eeece675894f30f4
7
+ data.tar.gz: bd60223a0bc09962fb82db2cd7c413da1a08f37ff603a85b716723eaa3fecbf983de317155e8c451badf50063fa59df417d3f7c3e7b6df181651026e2be6696f
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aws-eni.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Mike Greiling
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,63 @@
1
+ # aws-eni
2
+
3
+ A command line tool and client library to manage AWS Elastic Network Interfaces from within an EC2 instance.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'aws-eni'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install aws-eni
20
+
21
+ ## Usage
22
+
23
+ Synchronize your EC2 instance network interface config with AWS.
24
+
25
+ $ aws-eni sync
26
+
27
+ List all interface cards, their IPs, and their associations
28
+
29
+ $ aws-eni list
30
+ eth0:
31
+ 10.0.1.23
32
+ eth1:
33
+ 10.0.2.54 => 54.25.169.87 (EIP)
34
+ 10.0.2.72 => 52.82.17.251 (EIP)
35
+
36
+ Add a new private IP
37
+
38
+ $ aws-eni add eth1
39
+ added 10.0.2.81
40
+
41
+ Associate a new elastic IP
42
+
43
+ $ aws-eni associate 10.0.2.81
44
+ associated 10.0.2.81 => 52.171.254.36
45
+
46
+ Dissociate an elastic IP
47
+
48
+ $ aws-eni dissociate 10.0.2.81
49
+ dissociated 52.171.254.36 from 10.0.2.81
50
+
51
+ Remove a private IP
52
+
53
+ $ aws-eni remove 10.0.2.81
54
+ removed 10.0.2.81 from eth1
55
+
56
+
57
+ ## Contributing
58
+
59
+ 1. Fork it ( https://github.com/[my-github-username]/aws-eni/fork )
60
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
61
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
62
+ 4. Push to the branch (`git push origin my-new-feature`)
63
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'aws-eni/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "aws-eni"
8
+ spec.version = Aws::ENI::VERSION
9
+ spec.authors = ["Mike Greiling"]
10
+ spec.email = ["mike@pixelcog.com"]
11
+ spec.summary = "Manage and sync local network config with AWS Elastic Network Interfaces"
12
+ spec.description = "A command line tool and client library to manage AWS Elastic Network Interfaces from within an EC2 instance"
13
+ spec.homepage = "http://pixelcog.com/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "gli", "~> 2.5"
22
+ spec.add_dependency "aws-sdk", "~> 2.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ end
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env ruby
2
+ require 'gli'
3
+ require 'resolv'
4
+ require 'ipaddr'
5
+ require 'aws-eni'
6
+
7
+ include GLI::App
8
+
9
+ program_desc 'Manage and sync local network config with AWS Elastic Network Interfaces'
10
+
11
+ version Aws::ENI::VERSION
12
+
13
+ autocomplete_commands true
14
+ subcommand_option_handling :normal
15
+ arguments :loose
16
+ sort_help :manually
17
+
18
+ # global options
19
+
20
+ desc 'Display all system commands and warnings'
21
+ switch [:v,:verbose], negatable: false
22
+
23
+ pre do |opt|
24
+ Aws::ENI::IFconfig.verbose = opt[:verbose]
25
+ true
26
+ end
27
+
28
+ # arg parser methods
29
+
30
+ def local_ip?(ip)
31
+ IPAddr.new(Aws::ENI.environment[:vpc_cidr]) === IPAddr.new(ip)
32
+ end
33
+
34
+ def parse_args(args, *accept)
35
+ params = {}
36
+ args.each do |arg|
37
+ if arg =~ /^eth[0-9]+/ && accept.include?(:device_name)
38
+ help_now! "You may only specify one device name." if params[:device_name]
39
+ params[:device_name] = arg
40
+ elsif arg =~ /^eni-/ && accept.include?(:interface_id)
41
+ help_now! "You may only specify one interface ID." if params[:interface_id]
42
+ params[:interface_id] = arg
43
+ elsif arg =~ /^subnet-/ && accept.include?(:subnet_id)
44
+ help_now! "You may only specify one subnet." if params[:subnet_id]
45
+ params[:subnet_id] = arg
46
+ elsif arg =~ /^sg-/ && accept.include?(:security_groups)
47
+ params[:security_groups] ||= []
48
+ params[:security_groups] << arg
49
+ elsif arg =~ /^eipalloc-/ && accept.include?(:allocation_id)
50
+ help_now! "You may only specify one allocation ID" if params[:allocation_id]
51
+ params[:allocation_id] = arg
52
+ elsif arg =~ /^eipassoc-/ && accept.include?(:association_id)
53
+ help_now! "You may only specify one association ID" if params[:association_id]
54
+ params[:association_id] = arg
55
+ elsif arg =~ Resolv::IPv4::Regex
56
+ if local_ip? arg
57
+ if accept.include?(:primary_ip)
58
+ help_now! "You may only specify one primary IP address." if params[:primary_ip]
59
+ params[:primary_ip] = arg
60
+ elsif accept.include?(:private_ip)
61
+ help_now! "You may only specify one private IP address." if params[:private_ip]
62
+ params[:private_ip] = arg
63
+ else
64
+ help_now! "Invalid IP address: #{arg}"
65
+ end
66
+ elsif accept.include?(:public_ip)
67
+ help_now! "You may only specify one public IP address." if params[:public_ip]
68
+ params[:public_ip] = arg
69
+ else
70
+ help_now! "Invalid IP address: #{arg}"
71
+ end
72
+ else
73
+ help_now! "Invalid argument: #{arg}"
74
+ end
75
+ end
76
+ params
77
+ end
78
+
79
+ # commands
80
+
81
+ desc 'List current interface configuration'
82
+ long_desc %{
83
+ List information about a set of interfaces including interface ID, interface
84
+ name, MAC address, and a list of primary and secondary IP addresses along with
85
+ any public ips associated with them.
86
+
87
+ Use the optional filter argument to limit the listing to interfaces with a
88
+ matching name, interface ID, subnet ID, or MAC address (default 'all').
89
+ }
90
+ arg 'filter', :optional
91
+ command [:list,:ls] do |c|
92
+ c.action do |global,opts,args|
93
+ help_now! "Too many arguments" if args.count > 1
94
+ args.delete('all')
95
+ Aws::ENI.list(args.first).each do |interface|
96
+ print "#{interface[:name]}:"
97
+ print "\tID #{interface[:interface_id]}"
98
+ print " HWaddr #{interface[:hwaddr]}"
99
+ print " Status " << (interface[:enabled] ? "UP" : "DOWN") << "\n"
100
+ interface[:local_ips].each do |local_ip|
101
+ if interface[:public_ips][local_ip]
102
+ puts "\t#{local_ip} => #{interface[:public_ips][local_ip]}"
103
+ else
104
+ puts "\t#{local_ip}"
105
+ end
106
+ end
107
+ end
108
+ puts "\ninterface config is out of sync" if Aws::ENI.configure(nil, dry_run: true) > 0
109
+ end
110
+ end
111
+
112
+ desc 'Configure network interfaces'
113
+ long_desc %{
114
+ Syncronize configuration for a set of interfaces to match their configuration
115
+ on AWS by managing secondary ips, routes, and rules.
116
+
117
+ Use the optional filter argument to limit this action to interfaces with a
118
+ matching name, interface ID, subnet ID, or MAC address (default 'all').
119
+ }
120
+ arg 'filter', :optional
121
+ command [:config] do |c|
122
+ c.action do |global,opts,args|
123
+ help_now! "Too many arguments" if args.count > 1
124
+ args.delete('all')
125
+ if Aws::ENI.configure(args.first) != 0
126
+ puts 'synchronized interface config'
127
+ else
128
+ puts 'network interface config already in sync'
129
+ end
130
+ end
131
+ end
132
+
133
+ desc 'Remove custom configuration from network interfaces'
134
+ long_desc %{
135
+ Remove custom configuration for a set of interfaces removing any custom ips,
136
+ routes, and rules previously added (the 'eth0' primary IP is always left
137
+ untouched for safety).
138
+
139
+ Use the optional filter argument to limit this action to interfaces with a
140
+ matching name, interface ID, subnet ID, or MAC address (default 'all').
141
+ }
142
+ arg 'filter', :optional
143
+ command [:deconfig] do |c|
144
+ c.action do |global,opts,args|
145
+ help_now! "Too many arguments" if args.count > 1
146
+ args.delete('all')
147
+ Aws::ENI.deconfigure(args.first)
148
+ end
149
+ end
150
+
151
+ desc 'Enable network interface'
152
+ long_desc %{
153
+ Enable one or more network interfaces (similar to 'ifup').
154
+
155
+ Specify one name, interface ID, subnet ID, or MAC address to enable any
156
+ matching interfaces, or specify 'all' to enable all interfaces.
157
+ }
158
+ arg 'filter'
159
+ command [:enable,:up] do |c|
160
+ c.action do |global,opts,args|
161
+ help_now! "Incorrect number of arguments" unless args.count.between?(0,1)
162
+ args.delete('all')
163
+ Aws::ENI::IFconfig.filter(args.first).each(&:enable)
164
+ puts "interfaces enabled"
165
+ end
166
+ end
167
+
168
+ desc 'Disable network interface'
169
+ long_desc %{
170
+ Disable one or more network interfaces (similar to 'ifdown').
171
+
172
+ Specify one name, interface ID, subnet ID, or MAC address to disable any
173
+ matching interfaces, or specify 'all' to disable all interfaces.
174
+
175
+ eth0 cannot be disabled.
176
+ }
177
+ arg 'filter'
178
+ command [:disable,:down] do |c|
179
+ c.action do |global,opts,args|
180
+ help_now! "Incorrect number of arguments" unless args.count.between?(0,1)
181
+ args.delete('all')
182
+ Aws::ENI::IFconfig.filter(args.first).each do |dev|
183
+ if dev.name == 'eth0'
184
+ warn 'skipping eth0'
185
+ else
186
+ dev.disable
187
+ puts "interface #{dev.name} disabled"
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ desc 'Create new network interface'
194
+ long_desc %{
195
+ Create a new Elastic Network Interface with a given set of parameters.
196
+
197
+ Optional arguments include subnet ID, security group IDs, and a primary ip
198
+ address.
199
+
200
+ If no subnet ID is provided (e.g. subnet-1a2b3c4d) the subnet for eth0 will
201
+ be used. If no security group is provided (e.g. sg-1a2b3c4d) the VPC default
202
+ security group will be used. If a private IP is provided, it must fall within
203
+ the subnet's CIDR block. Arguments can be provided in any order.
204
+ }
205
+ arg 'subnet', :optional
206
+ arg 'security-groups', :optional
207
+ arg 'ip-address', :optional
208
+ command [:create] do |c|
209
+ c.action do |global,opts,args|
210
+ params = parse_args args, :subnet_id, :security_groups, :primary_ip
211
+ interface = Aws::ENI.create_interface(params)
212
+ puts "interface #{interface[:id]} created on #{interface[:subnet_id]}"
213
+ end
214
+ end
215
+
216
+ desc 'Attach network interface'
217
+ long_desc %{
218
+ Attach an Elastic Network Interface to our instance.
219
+
220
+ If interface ID provided (e.g. eni-1a2b3c4d), that interface will be attached,
221
+ otherwise a new interface will be created with the provided parameters which
222
+ may include a subnet ID, security group IDs, and a primary IP address.
223
+
224
+ If no subnet ID is provided (e.g. subnet-1a2b3c4d) the subnet for eth0 will
225
+ be used. If no security group is provided (e.g. sg-1a2b3c4d) the VPC default
226
+ security group will be used. If a private IP is provided, it must fall within
227
+ the subnet's CIDR block. Arguments can be provided in any order.
228
+ }
229
+ arg 'interface-id', :optional
230
+ arg 'subnet-id', :optional
231
+ arg 'security-groups', :optional
232
+ arg 'ip-address', :optional
233
+ command [:attach] do |c|
234
+ c.desc 'Refresh the interface configuration after attachment'
235
+ c.switch [:r,:c,:config], negatable: false
236
+
237
+ c.action do |global,opts,args|
238
+ if args.first =~ /^eni-/
239
+ help_now! 'Too many arguments' if args.count > 1
240
+ id = args.first
241
+ else
242
+ params = parse_args args, :subnet_id, :security_groups, :primary_ip
243
+ interface = Aws::ENI.create_interface(params)
244
+ puts "interface #{interface[:id]} created on #{interface[:subnet_id]}"
245
+ id = interface[:id]
246
+ end
247
+ device = Aws::ENI.attach_interface(id, enable: opts[:config], configure: opts[:config])
248
+ puts "interface #{device[:id]} attached to #{device[:name]}"
249
+ puts "device #{device[:name]} enabled and configured" if opts[:config]
250
+ end
251
+ end
252
+
253
+ desc 'Detach network interface'
254
+ long_desc %{
255
+ Detach an Elastic Network Interface from our instance.
256
+
257
+ You must provide the interface ID (e.g. eni-1a2b3c4d) or the device name
258
+ (e.g. eth1) or both.
259
+
260
+ If no flag is present, the default action is to destroy the network interface
261
+ after detachment only if it was originally created by aws-eni.
262
+ }
263
+ arg 'interface-id OR device-name'
264
+ command [:detach] do |c|
265
+ c.desc 'Delete (or preserve) the unused ENI resource after dataching'
266
+ c.switch [:d,:delete], default_value: :not_provided # GLI behavior workaround
267
+
268
+ c.action do |global,opts,args|
269
+ help_now! "Missing argument" if args.empty?
270
+ params = parse_args args, :interface_id, :device_name
271
+ params[:delete] = opts[:delete] unless opts[:delete] == :not_provided
272
+ id = params[:interface_id] || params[:device_name]
273
+
274
+ device = Aws::ENI.detach_interface(id, params)
275
+ if device[:deleted]
276
+ puts "interface #{device[:id]} detached from #{device[:name]} and destroyed"
277
+ else
278
+ puts "interface #{device[:id]} detached from #{device[:name]}"
279
+ end
280
+ end
281
+ end
282
+
283
+ desc 'Clean unattached network interfaces'
284
+ long_desc %{
285
+ Delete unused Elastic Network Interfaces based on provided criteria.
286
+
287
+ You may provide a specific interface ID (e.g. eni-1a2b3c4d), a subnet ID
288
+ (e.g. subnet-1a2b3c4d), or an availability zone from this region (e.g.
289
+ us-east-1a) to act as search criteria.
290
+
291
+ By default, this will only delete ENIs which were originally created with this
292
+ script.
293
+ }
294
+ arg 'filter', :optional
295
+ command [:clean] do |c|
296
+ c.desc 'Force deletion of all unattached interfaces which meet our criteria'
297
+ c.switch [:f,:force], negatable: false
298
+
299
+ c.action do |global,opts,args|
300
+ help_now! "Too many arguments" if args.count > 1
301
+ deleted = Aws::ENI.clean_interfaces(args.first, safe_mode: !opts[:force])
302
+ puts "#{deleted[:count]} interfaces deleted"
303
+ end
304
+ end
305
+
306
+ desc 'Assign a new secondary private IP address'
307
+ long_desc %{
308
+ Assign an additional private IP address to a given interface.
309
+
310
+ You may optionally specify a private IP in the interface's subnet CIDR range
311
+ to assign, otherwise one will be generated automatically.
312
+ }
313
+ arg 'ip-address', :optional
314
+ arg 'interface-id OR device-name'
315
+ command [:assign] do |c|
316
+ c.action do |global,opts,args|
317
+ help_now! "Missing argument" if args.empty?
318
+ params = parse_args args, :interface_id, :device_name, :private_ip
319
+ device = params[:interface_id] || params[:device_name]
320
+ assignment = Aws::ENI.assign_secondary_ip(device, params)
321
+ puts "IP #{assignment[:private_ip]} assigned to #{assignment[:device_name]} (#{assignment[:interface_id]})"
322
+ end
323
+ end
324
+
325
+ desc 'Unassign a secondary private IP address'
326
+ long_desc %{
327
+ Remove a private IP address from a given interface. Any associated public ip
328
+ address will be dissociated first and optionally released.
329
+
330
+ If no interface is specified, it will be inferred by the IP address.
331
+ }
332
+ arg 'ip-address'
333
+ arg 'interface-id', :optional
334
+ arg 'device-name', :optional
335
+ command [:unassign] do |c|
336
+ c.desc 'Release any associated public IP address'
337
+ c.switch [:r,:release], negatable: false
338
+
339
+ c.action do |global,opts,args|
340
+ help_now! "Missing argument" if args.empty?
341
+ params = parse_args args, :interface_id, :device_name, :private_ip
342
+ params[:release] = opts[:release]
343
+ unassign = Aws::ENI.unassign_secondary_ip(params[:private_ip], params)
344
+ if unassign[:released]
345
+ puts "EIP #{unassign[:public_ip]} (#{unassign[:allocation_id]}) dissociated from #{unassign[:private_ip]} and released"
346
+ elsif unassign[:public_ip]
347
+ puts "EIP #{unassign[:public_ip]} (#{unassign[:allocation_id]}) dissociated from #{unassign[:private_ip]}"
348
+ end
349
+ puts "IP #{unassign[:private_ip]} removed from #{unassign[:device_name]} (#{unassign[:interface_id]})"
350
+ end
351
+ end
352
+
353
+ desc 'Associate a public IP address with a private IP address'
354
+ long_desc %{
355
+ Associate a private IP address with a new or existing public IP address.
356
+
357
+ If no public IP or allocation ID is provided, a new Elastic IP Address will be
358
+ allocated and used.
359
+
360
+ If no interface ID or device name provided, it will be inferred from the ip
361
+ address.
362
+ }
363
+ arg 'private-ip'
364
+ arg 'public-ip', :optional
365
+ arg 'allocation-id', :optional
366
+ arg 'interface-id', :optional
367
+ arg 'device-name', :optional
368
+ command [:associate] do |c|
369
+ c.action do |global,opts,args|
370
+ help_now! "Missing argument" if args.empty?
371
+ params = parse_args args, :private_ip, :public_ip, :allocation_id, :interface_id, :device_name
372
+ assoc = Aws::ENI.associate_elastic_ip(params[:private_ip], params)
373
+ puts "EIP #{assoc[:public_ip]} (#{assoc[:allocation_id]}) associated with #{assoc[:private_ip]} on #{assoc[:device_name]} (#{assoc[:interface_id]})"
374
+ end
375
+ end
376
+
377
+ desc 'Dissociate a public IP address from a private IP address'
378
+ long_desc %{
379
+ Remove an association between a given public IP address or private IP address
380
+ and their counterpart. The public IP address is then optionally released.
381
+ }
382
+ arg 'private-ip OR public-ip OR allocation-id'
383
+ arg 'interface-id', :optional
384
+ arg 'device-name', :optional
385
+ command [:dissociate] do |c|
386
+ c.desc 'Release the associated public IP address'
387
+ c.switch [:r,:release], negatable: false
388
+
389
+ c.action do |global,opts,args|
390
+ help_now! "Missing argument" if args.empty?
391
+ params = parse_args args, :private_ip, :public_ip, :allocation_id, :association_id, :interface_id, :device_name
392
+ ip = params[:private_ip] || params[:public_ip] || params[:association_id] || params[:allocation_id]
393
+ dissoc = Aws::ENI.dissociate_elastic_ip(ip, params)
394
+ if dissoc[:released]
395
+ puts "EIP #{dissoc[:public_ip]} (#{dissoc[:allocation_id]}) dissociated from #{dissoc[:private_ip]} on #{dissoc[:device_name]} (#{dissoc[:interface_id]}) and released"
396
+ else
397
+ puts "EIP #{dissoc[:public_ip]} (#{dissoc[:allocation_id]}) dissociated from #{dissoc[:private_ip]} on #{dissoc[:device_name]} (#{dissoc[:interface_id]})"
398
+ end
399
+ end
400
+ end
401
+
402
+ desc 'Allocate a new Elastic IP address'
403
+ long_desc %{
404
+ Allocate a new Elastic IP address for use
405
+ }
406
+ command [:allocate] do |c|
407
+ c.action do |global,opts,args|
408
+ alloc = Aws::ENI.allocate_elastic_ip
409
+ puts "EIP #{alloc[:public_ip]} allocated as #{alloc[:allocation_id]}"
410
+ end
411
+ end
412
+
413
+ desc 'Release one or more unassigned Elastic IP addresses'
414
+ long_desc %{
415
+ Release one or more unassigned Elastic IP addresses. If IP address or
416
+ allication, will release only that EIP, otherwise will clean all unassigned
417
+ EIPs.
418
+ }
419
+ arg 'ip-address OR allocation-id'
420
+ command [:release] do |c|
421
+ c.action do |global,opts,args|
422
+ help_now! "Missing argument" if args.empty?
423
+ params = parse_args args, :public_ip, :allocation_id
424
+ eip = params[:public_ip] || params[:allocation_id]
425
+ release = Aws::ENI.release_elastic_ip(eip, params)
426
+ puts "EIP #{release[:public_ip]} (#{release[:allocation_id]}) released"
427
+ end
428
+ end
429
+
430
+ # error handling
431
+
432
+ on_error do |exception|
433
+ if Aws::ENI::PermissionError === exception
434
+ warn 'error: This action requires super-user privileges (try sudo)'
435
+ false
436
+ else
437
+ true
438
+ end
439
+ end
440
+
441
+ ARGV << 'ls' if ARGV.empty?
442
+ exit run(ARGV)