aws-eni 0.2.1 → 0.2.2
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 +4 -4
- data/bin/aws-eni +29 -22
- data/lib/aws-eni.rb +125 -202
- data/lib/aws-eni/client.rb +152 -0
- data/lib/aws-eni/errors.rb +25 -12
- data/lib/aws-eni/{ifconfig.rb → interface.rb} +29 -32
- data/lib/aws-eni/meta.rb +40 -30
- data/lib/aws-eni/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31f890a936163bc23d0c7bec943936be043fda58
|
4
|
+
data.tar.gz: 4e5d5ac127109e761af68f26ce33caff51a00121
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f71962db0b6c220aef90fbc6cf3c4c57d2100b90177e7a34f2a33fd78f5c67bd262c2b373269f70299c325d0a5aef1891f5a79cf0a222580ca720290957f69ea
|
7
|
+
data.tar.gz: 62990ff085b4609f33745d40571ec86ba3ef1b00cc568a84ba8eea2c0b5f3534d90a7960a96d673f6b77b73c0ab38a22aff9fbeb9e56cb1d58467833d0f46d2f
|
data/bin/aws-eni
CHANGED
@@ -25,7 +25,7 @@ switch [:V,:verbose], negatable: false
|
|
25
25
|
|
26
26
|
pre do |opt|
|
27
27
|
Aws::ENI.timeout 90
|
28
|
-
Aws::ENI
|
28
|
+
Aws::ENI.verbose opt[:verbose]
|
29
29
|
true
|
30
30
|
end
|
31
31
|
|
@@ -164,8 +164,8 @@ command [:enable,:up] do |c|
|
|
164
164
|
c.action do |global,opts,args|
|
165
165
|
help_now! "Incorrect number of arguments" unless args.count.between?(0,1)
|
166
166
|
args.delete('all')
|
167
|
-
Aws::ENI::
|
168
|
-
puts "interfaces enabled"
|
167
|
+
enabled = Aws::ENI::enable(args.first)
|
168
|
+
puts "#{enabled} interfaces enabled"
|
169
169
|
end
|
170
170
|
end
|
171
171
|
|
@@ -183,14 +183,8 @@ command [:disable,:down] do |c|
|
|
183
183
|
c.action do |global,opts,args|
|
184
184
|
help_now! "Incorrect number of arguments" unless args.count.between?(0,1)
|
185
185
|
args.delete('all')
|
186
|
-
Aws::ENI::
|
187
|
-
|
188
|
-
warn 'skipping eth0'
|
189
|
-
else
|
190
|
-
dev.disable
|
191
|
-
puts "interface #{dev.name} disabled"
|
192
|
-
end
|
193
|
-
end
|
186
|
+
disabled = Aws::ENI::disable(args.first)
|
187
|
+
puts "#{disabled} interfaces disabled"
|
194
188
|
end
|
195
189
|
end
|
196
190
|
|
@@ -250,12 +244,21 @@ command [:attach] do |c|
|
|
250
244
|
else
|
251
245
|
args.delete('new')
|
252
246
|
params = parse_args args, :subnet_id, :security_groups, :primary_ip
|
253
|
-
Aws::ENI.
|
247
|
+
Aws::ENI.assert_interface_access if config
|
254
248
|
interface = Aws::ENI.create_interface(params)
|
255
249
|
puts "interface #{interface[:interface_id]} created on #{interface[:subnet_id]}"
|
256
250
|
id = interface[:interface_id]
|
251
|
+
new_interface = true
|
252
|
+
end
|
253
|
+
begin
|
254
|
+
device = Aws::ENI.attach_interface(id, enable: config, configure: config, block: opts[:block])
|
255
|
+
rescue Aws::ENI::Errors::ClientOperationError => e
|
256
|
+
warn e.message
|
257
|
+
if new_interface && Aws::ENI.clean_interfaces(id, safe_mode: false)
|
258
|
+
puts "interface #{id} deleted"
|
259
|
+
end
|
260
|
+
exit_now! "attachment failed"
|
257
261
|
end
|
258
|
-
device = Aws::ENI.attach_interface(id, enable: config, configure: config, block: opts[:block])
|
259
262
|
puts "interface #{device[:interface_id]} attached to #{device[:device_name]}"
|
260
263
|
puts "device #{device[:device_name]} enabled and configured" if config
|
261
264
|
end
|
@@ -295,7 +298,7 @@ command [:detach] do |c|
|
|
295
298
|
device[:public_ips].each do |address|
|
296
299
|
if device[:released]
|
297
300
|
puts "EIP #{address[:public_ip]} (#{address[:allocation_id]}) dissociated and released"
|
298
|
-
|
301
|
+
elsif device[:deleted]
|
299
302
|
puts "EIP #{address[:public_ip]} (#{address[:allocation_id]}) dissociated"
|
300
303
|
end
|
301
304
|
end
|
@@ -366,7 +369,7 @@ command [:assign] do |c|
|
|
366
369
|
help_now! "Missing argument" if device.nil?
|
367
370
|
|
368
371
|
params.merge! configure: !opts[:noconfig], block: opts[:block]
|
369
|
-
Aws::ENI.
|
372
|
+
Aws::ENI.assert_interface_access if params[:configure]
|
370
373
|
|
371
374
|
assignment = Aws::ENI.assign_secondary_ip(device, params)
|
372
375
|
puts "IP #{assignment[:private_ip]} assigned to #{assignment[:device_name]} (#{assignment[:interface_id]})"
|
@@ -498,8 +501,8 @@ command [:test] do |c|
|
|
498
501
|
c.action do |global,opts,args|
|
499
502
|
help_now! "Too many arguments" if args.count > 1
|
500
503
|
|
501
|
-
print '
|
502
|
-
if Aws::ENI.
|
504
|
+
print 'Interface permissions test... '
|
505
|
+
if Aws::ENI.has_interface_access?
|
503
506
|
puts 'success!'
|
504
507
|
else
|
505
508
|
puts 'failed'
|
@@ -507,7 +510,7 @@ command [:test] do |c|
|
|
507
510
|
end
|
508
511
|
|
509
512
|
print 'AWS EC2 permissions test... '
|
510
|
-
if Aws::ENI.
|
513
|
+
if Aws::ENI.has_client_access?
|
511
514
|
puts 'success!'
|
512
515
|
else
|
513
516
|
puts 'failed'
|
@@ -519,10 +522,14 @@ end
|
|
519
522
|
|
520
523
|
# error handling
|
521
524
|
|
522
|
-
on_error do |
|
523
|
-
|
524
|
-
|
525
|
-
|
525
|
+
on_error do |error|
|
526
|
+
case error
|
527
|
+
when Aws::ENI::Errors::InterfacePermissionError
|
528
|
+
warn 'Error: This action requires super-user privileges (try sudo)'
|
529
|
+
when Aws::ENI::Errors::ClientPermissionError
|
530
|
+
warn 'Error: Insufficient EC2 operation privileges (check AWS IAM policy)'
|
531
|
+
when Aws::ENI::Errors::ServiceError
|
532
|
+
warn "Error: #{error.message}"
|
526
533
|
else
|
527
534
|
true
|
528
535
|
end
|
data/lib/aws-eni.rb
CHANGED
@@ -1,30 +1,29 @@
|
|
1
1
|
require 'time'
|
2
|
-
require 'aws-sdk'
|
3
2
|
require 'aws-eni/version'
|
4
3
|
require 'aws-eni/errors'
|
5
4
|
require 'aws-eni/meta'
|
6
|
-
require 'aws-eni/
|
5
|
+
require 'aws-eni/client'
|
6
|
+
require 'aws-eni/interface'
|
7
7
|
|
8
8
|
module Aws
|
9
9
|
module ENI
|
10
10
|
extend self
|
11
11
|
|
12
12
|
def environment
|
13
|
-
@environment ||=
|
14
|
-
hwaddr =
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
raise EnvironmentError, "Unable to load EC2 meta-data"
|
13
|
+
@environment ||= Meta.connection do
|
14
|
+
hwaddr = Meta.instance('network/interfaces/macs/').lines.first.strip.chomp('/')
|
15
|
+
{
|
16
|
+
instance_id: Meta.instance('instance-id'),
|
17
|
+
availability_zone: Meta.instance('placement/availability-zone'),
|
18
|
+
region: Meta.instance('placement/availability-zone').sub(/^(.*)[a-z]$/,'\1'),
|
19
|
+
vpc_id: Meta.interface(hwaddr, 'vpc-id'),
|
20
|
+
vpc_cidr: Meta.interface(hwaddr, 'vpc-ipv4-cidr-block')
|
21
|
+
}.freeze
|
22
|
+
end
|
23
|
+
rescue Errors::MetaNotFound
|
24
|
+
raise Errors::EnvironmentError, 'Unable to detect VPC, library incompatible with EC2-Classic'
|
25
|
+
rescue Errors::MetaConnectionFailed
|
26
|
+
raise Errors::EnvironmentError, 'Unable to load EC2 meta-data'
|
28
27
|
end
|
29
28
|
|
30
29
|
def owner_tag(new_owner = nil)
|
@@ -37,40 +36,54 @@ module Aws
|
|
37
36
|
@timeout ||= 30
|
38
37
|
end
|
39
38
|
|
40
|
-
def
|
41
|
-
|
39
|
+
def verbose(set_verbose = nil)
|
40
|
+
unless set_verbose.nil?
|
41
|
+
@verbose = !!set_verbose
|
42
|
+
Interface.verbose = @verbose
|
43
|
+
end
|
44
|
+
@verbose
|
42
45
|
end
|
43
46
|
|
44
47
|
# return our internal model of this instance's network configuration on AWS
|
45
48
|
def list(filter = nil)
|
46
|
-
|
49
|
+
Interface.filter(filter).map(&:to_h) if environment
|
47
50
|
end
|
48
51
|
|
49
52
|
# sync local machine's network interface config with the EC2 meta-data
|
50
53
|
# pass dry_run option to check whether configuration is out of sync without
|
51
54
|
# modifying it
|
52
55
|
def configure(filter = nil, options = {})
|
53
|
-
|
56
|
+
Interface.configure(filter, options) if environment
|
54
57
|
end
|
55
58
|
|
56
59
|
# clear local machine's network interface config
|
57
60
|
def deconfigure(filter = nil)
|
58
|
-
|
61
|
+
Interface.deconfigure(filter) if environment
|
62
|
+
end
|
63
|
+
|
64
|
+
# enable one or more network interfaces
|
65
|
+
def enable(filter = nil)
|
66
|
+
Interface.filter(filter).select{ |dev| !dev.enabled? }.each(&:enable).count
|
67
|
+
end
|
68
|
+
|
69
|
+
# disable one or more network interfaces
|
70
|
+
def disable(filter = nil)
|
71
|
+
Interface.filter(filter).select{ |dev| dev.enabled? && dev.name != 'eth0' }.each(&:disable).count
|
59
72
|
end
|
60
73
|
|
61
74
|
# create network interface
|
62
75
|
def create_interface(options = {})
|
63
76
|
timestamp = Time.now.xmlschema
|
64
77
|
params = {}
|
65
|
-
params[:subnet_id] = options[:subnet_id] ||
|
78
|
+
params[:subnet_id] = options[:subnet_id] || Interface.first.subnet_id
|
66
79
|
params[:private_ip_address] = options[:primary_ip] if options[:primary_ip]
|
67
80
|
params[:groups] = [*options[:security_groups]] if options[:security_groups]
|
68
81
|
params[:description] = "generated by #{owner_tag} from #{environment[:instance_id]} on #{timestamp}"
|
69
82
|
|
70
|
-
|
71
|
-
wait_for 'the interface to be created', rescue:
|
72
|
-
if
|
73
|
-
|
83
|
+
interface = Client.create_network_interface(params)[:network_interface]
|
84
|
+
wait_for 'the interface to be created', rescue: Errors::UnknownInterface do
|
85
|
+
if Client.describe_interface(interface[:network_interface_id])
|
86
|
+
Client.create_tags(resources: [interface[:network_interface_id]], tags: [
|
74
87
|
{ key: 'created by', value: owner_tag },
|
75
88
|
{ key: 'created on', value: timestamp },
|
76
89
|
{ key: 'created from', value: environment[:instance_id] }
|
@@ -78,9 +91,9 @@ module Aws
|
|
78
91
|
end
|
79
92
|
end
|
80
93
|
{
|
81
|
-
interface_id:
|
82
|
-
subnet_id:
|
83
|
-
api_response:
|
94
|
+
interface_id: interface[:network_interface_id],
|
95
|
+
subnet_id: interface[:subnet_id],
|
96
|
+
api_response: interface
|
84
97
|
}
|
85
98
|
end
|
86
99
|
|
@@ -88,19 +101,23 @@ module Aws
|
|
88
101
|
def attach_interface(id, options = {})
|
89
102
|
do_enable = true unless options[:enable] == false
|
90
103
|
do_config = true unless options[:configure] == false
|
91
|
-
|
104
|
+
assert_interface_access if do_config || do_enable
|
92
105
|
|
93
|
-
device =
|
106
|
+
device = Interface[options[:device_number] || options[:name]].assert(exists: false)
|
94
107
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
108
|
+
begin
|
109
|
+
response = Client.attach_network_interface(
|
110
|
+
network_interface_id: id,
|
111
|
+
instance_id: environment[:instance_id],
|
112
|
+
device_index: device.device_number
|
113
|
+
)
|
114
|
+
rescue EC2::Errors::AttachmentLimitExceeded
|
115
|
+
raise Errors::ClientOperationError, "Unable to attach #{id} to #{device.name} (attachment limit exceeded)"
|
116
|
+
end
|
100
117
|
|
101
118
|
if options[:block] || do_config || do_enable
|
102
|
-
wait_for 'the interface to attach', rescue:
|
103
|
-
device.exists? &&
|
119
|
+
wait_for 'the interface to attach', rescue: Errors::MetaNotFound do
|
120
|
+
device.exists? && Client.interface_attached(device.interface_id)
|
104
121
|
end
|
105
122
|
end
|
106
123
|
device.configure if do_config
|
@@ -117,30 +134,25 @@ module Aws
|
|
117
134
|
|
118
135
|
# detach network interface
|
119
136
|
def detach_interface(id, options = {})
|
120
|
-
device =
|
137
|
+
device = Interface[id].assert(
|
121
138
|
exists: true,
|
122
139
|
device_name: options[:device_name],
|
123
140
|
interface_id: options[:interface_id],
|
124
141
|
device_number: options[:device_number]
|
125
142
|
)
|
143
|
+
interface = Client.describe_interface(device.interface_id)
|
144
|
+
|
126
145
|
if device.name == 'eth0'
|
127
|
-
raise
|
146
|
+
raise Errors::InvalidInterface, 'For safety, interface eth0 cannot be detached.'
|
128
147
|
end
|
129
|
-
interface_id = device.interface_id
|
130
148
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
},{
|
135
|
-
name: 'network-interface-id',
|
136
|
-
values: [interface_id]
|
137
|
-
}])
|
138
|
-
interface = response[:network_interfaces].first
|
139
|
-
raise UnknownInterfaceError, "Interface attachment could not be located" unless interface
|
149
|
+
unless interface[:attachment] && interface[:attachment][:instance_id] == environment[:instance_id]
|
150
|
+
raise Errors::UnknownInterface, "Interface #{interface_id} is not attached to this machine"
|
151
|
+
end
|
140
152
|
|
141
153
|
device.disable
|
142
154
|
device.deconfigure
|
143
|
-
|
155
|
+
Client.detach_network_interface(
|
144
156
|
attachment_id: interface[:attachment][:attachment_id],
|
145
157
|
force: true
|
146
158
|
)
|
@@ -161,12 +173,12 @@ module Aws
|
|
161
173
|
|
162
174
|
if options[:block] || do_delete
|
163
175
|
wait_for 'the interface to detach', interval: 0.3 do
|
164
|
-
!device.exists? &&
|
176
|
+
!device.exists? && !Client.interface_attached(interface[:network_interface_id])
|
165
177
|
end
|
166
178
|
end
|
167
|
-
|
179
|
+
Client.delete_network_interface(network_interface_id: interface[:network_interface_id]) if do_delete
|
168
180
|
{
|
169
|
-
interface_id:
|
181
|
+
interface_id: interface[:network_interface_id],
|
170
182
|
device_name: device.name,
|
171
183
|
device_number: device.device_number,
|
172
184
|
created_by_us: created_by_us,
|
@@ -196,14 +208,14 @@ module Aws
|
|
196
208
|
when /^#{environment[:region]}[a-z]$/
|
197
209
|
filters << { name: 'availability-zone', values: [filter] }
|
198
210
|
else
|
199
|
-
raise
|
211
|
+
raise Errors::InvalidInput, "Unknown interface attribute: #{filter}"
|
200
212
|
end
|
201
213
|
end
|
202
214
|
if safe_mode
|
203
215
|
filters << { name: 'tag:created by', values: [owner_tag] }
|
204
216
|
end
|
205
217
|
|
206
|
-
descriptions =
|
218
|
+
descriptions = Client.describe_network_interfaces(filters: filters)
|
207
219
|
interfaces = descriptions[:network_interfaces].select do |interface|
|
208
220
|
created_recently = interface.tag_set.any? do |tag|
|
209
221
|
begin
|
@@ -221,7 +233,7 @@ module Aws
|
|
221
233
|
dissociate_elastic_ip(assoc[:allocation_id], release: true) if do_release
|
222
234
|
end
|
223
235
|
end
|
224
|
-
|
236
|
+
Client.delete_network_interface(network_interface_id: interface[:network_interface_id])
|
225
237
|
true
|
226
238
|
end
|
227
239
|
end
|
@@ -235,37 +247,37 @@ module Aws
|
|
235
247
|
|
236
248
|
# add new private ip using the AWS api and add it to our local ip config
|
237
249
|
def assign_secondary_ip(id, options = {})
|
238
|
-
device =
|
250
|
+
device = Interface[id].assert(
|
239
251
|
exists: true,
|
240
252
|
device_name: options[:device_name],
|
241
253
|
interface_id: options[:interface_id],
|
242
254
|
device_number: options[:device_number]
|
243
255
|
)
|
244
256
|
interface_id = device.interface_id
|
245
|
-
current_ips =
|
257
|
+
current_ips = Client.interface_private_ips(interface_id)
|
246
258
|
new_ip = options[:private_ip]
|
247
259
|
|
248
260
|
if new_ip
|
249
261
|
if current_ips.include?(new_ip)
|
250
|
-
raise
|
262
|
+
raise Errors::ClientOperationError, "IP #{new_ip} already assigned to #{device.name}"
|
251
263
|
end
|
252
|
-
|
264
|
+
Client.assign_private_ip_addresses(
|
253
265
|
network_interface_id: interface_id,
|
254
266
|
private_ip_addresses: [new_ip],
|
255
267
|
allow_reassignment: false
|
256
268
|
)
|
257
269
|
wait_for 'private ip address to be assigned' do
|
258
|
-
|
270
|
+
Client.interface_private_ips(interface_id).include?(new_ip) ||
|
259
271
|
device.local_ips.include?(new_ip)
|
260
272
|
end
|
261
273
|
else
|
262
|
-
|
274
|
+
Client.assign_private_ip_addresses(
|
263
275
|
network_interface_id: interface_id,
|
264
276
|
secondary_private_ip_address_count: 1,
|
265
277
|
allow_reassignment: false
|
266
278
|
)
|
267
279
|
wait_for 'new private ip address to be assigned' do
|
268
|
-
new_ips =
|
280
|
+
new_ips = Client.interface_private_ips(interface_id) - current_ips
|
269
281
|
new_ips = device.local_ips - current_ips if new_ips.empty?
|
270
282
|
new_ip = new_ips.first if new_ips
|
271
283
|
end
|
@@ -273,8 +285,8 @@ module Aws
|
|
273
285
|
|
274
286
|
unless options[:configure] == false
|
275
287
|
device.add_alias(new_ip)
|
276
|
-
if options[:block] && !
|
277
|
-
raise TimeoutError,
|
288
|
+
if options[:block] && !Interface.test(new_ip, target: device.gateway)
|
289
|
+
raise Errors::TimeoutError, 'Timed out waiting for IP address to become active'
|
278
290
|
end
|
279
291
|
end
|
280
292
|
{
|
@@ -291,22 +303,20 @@ module Aws
|
|
291
303
|
do_release = !!options[:release]
|
292
304
|
|
293
305
|
find = options[:device_name] || options[:device_number] || options[:interface_id] || private_ip
|
294
|
-
device =
|
306
|
+
device = Interface[find].assert(
|
295
307
|
exists: true,
|
296
308
|
device_name: options[:device_name],
|
297
309
|
interface_id: options[:interface_id],
|
298
310
|
device_number: options[:device_number]
|
299
311
|
)
|
300
312
|
|
301
|
-
|
302
|
-
interface = resp[:network_interfaces].first
|
303
|
-
raise UnknownInterfaceError, "Interface attachment could not be located" unless interface
|
313
|
+
interface = Client.describe_interface(device.interface_id)
|
304
314
|
|
305
315
|
unless addr_info = interface[:private_ip_addresses].find { |addr| addr[:private_ip_address] == private_ip }
|
306
|
-
raise
|
316
|
+
raise Errors::ClientOperationError, "IP #{private_ip} not found on #{device.name}"
|
307
317
|
end
|
308
318
|
if addr_info[:primary]
|
309
|
-
raise
|
319
|
+
raise Errors::ClientOperationError, 'The primary IP address of an interface cannot be unassigned'
|
310
320
|
end
|
311
321
|
|
312
322
|
if assoc = addr_info[:association]
|
@@ -314,7 +324,7 @@ module Aws
|
|
314
324
|
end
|
315
325
|
|
316
326
|
device.remove_alias(private_ip)
|
317
|
-
|
327
|
+
Client.unassign_private_ip_addresses(
|
318
328
|
network_interface_id: interface[:network_interface_id],
|
319
329
|
private_ip_addresses: [private_ip]
|
320
330
|
)
|
@@ -331,10 +341,10 @@ module Aws
|
|
331
341
|
|
332
342
|
# associate a private ip with an elastic ip through the AWS api
|
333
343
|
def associate_elastic_ip(private_ip, options = {})
|
334
|
-
raise
|
344
|
+
raise Errors::MissingInput, 'You must specify a private IP address' unless private_ip
|
335
345
|
|
336
346
|
find = options[:device_name] || options[:device_number] || options[:interface_id] || private_ip
|
337
|
-
device =
|
347
|
+
device = Interface[find].assert(
|
338
348
|
exists: true,
|
339
349
|
private_ip: private_ip,
|
340
350
|
device_name: options[:device_name],
|
@@ -344,27 +354,27 @@ module Aws
|
|
344
354
|
options[:public_ip] ||= options[:allocation_id]
|
345
355
|
|
346
356
|
if public_ip = device.public_ips[private_ip]
|
347
|
-
raise
|
357
|
+
raise Errors::ClientOperationError, "IP #{private_ip} already has an associated EIP (#{public_ip})"
|
348
358
|
end
|
349
359
|
|
350
360
|
if options[:public_ip]
|
351
|
-
eip = describe_address(options[:public_ip])
|
361
|
+
eip = Client.describe_address(options[:public_ip])
|
352
362
|
if options[:allocation_id] && eip[:allocation_id] != options[:allocation_id]
|
353
|
-
raise
|
363
|
+
raise Errors::InvalidAddress, "EIP #{eip[:public_ip]} (#{eip[:allocation_id]}) does not match #{options[:allocation_id]}"
|
354
364
|
end
|
355
365
|
else
|
356
366
|
eip = allocate_elastic_ip
|
357
367
|
end
|
358
368
|
|
359
|
-
resp =
|
369
|
+
resp = Client.associate_address(
|
360
370
|
network_interface_id: device.interface_id,
|
361
371
|
allocation_id: eip[:allocation_id],
|
362
372
|
private_ip_address: private_ip,
|
363
373
|
allow_reassociation: false
|
364
374
|
)
|
365
375
|
|
366
|
-
if options[:block] && !
|
367
|
-
raise TimeoutError,
|
376
|
+
if options[:block] && !Interface.test(private_ip)
|
377
|
+
raise Errors::TimeoutError, 'Timed out waiting for ip address to become active'
|
368
378
|
end
|
369
379
|
{
|
370
380
|
private_ip: private_ip,
|
@@ -383,7 +393,7 @@ module Aws
|
|
383
393
|
|
384
394
|
# assert device attributes if we've specified a device
|
385
395
|
if find = options[:device_name] || options[:device_number]
|
386
|
-
device =
|
396
|
+
device = Interface[find].assert(
|
387
397
|
device_name: options[:device_name],
|
388
398
|
device_number: options[:device_number],
|
389
399
|
interface_id: options[:interface_id],
|
@@ -393,36 +403,36 @@ module Aws
|
|
393
403
|
end
|
394
404
|
|
395
405
|
# get our address info
|
396
|
-
eip = describe_address(address)
|
397
|
-
device ||=
|
406
|
+
eip = Client.describe_address(address)
|
407
|
+
device ||= Interface.find { |dev| dev.interface_id == eip[:network_interface_id] }
|
398
408
|
|
399
409
|
# assert eip attributes if options provided
|
400
410
|
if options[:private_ip] && eip[:private_ip_address] != options[:private_ip]
|
401
|
-
raise
|
411
|
+
raise Errors::InvalidAddress, "#{address} is not associated with IP #{options[:private_ip]}"
|
402
412
|
end
|
403
413
|
if options[:public_ip] && eip[:public_ip] != options[:public_ip]
|
404
|
-
raise
|
414
|
+
raise Errors::InvalidAddress, "#{address} is not associated with public IP #{options[:public_ip]}"
|
405
415
|
end
|
406
416
|
if options[:allocation_id] && eip[:allocation_id] != options[:allocation_id]
|
407
|
-
raise
|
417
|
+
raise Errors::InvalidAddress, "#{address} is not associated with allocation ID #{options[:allocation_id]}"
|
408
418
|
end
|
409
419
|
if options[:association_id] && eip[:association_id] != options[:association_id]
|
410
|
-
raise
|
420
|
+
raise Errors::InvalidAddress, "#{address} is not associated with association ID #{options[:association_id]}"
|
411
421
|
end
|
412
422
|
if options[:interface_id] && eip[:network_interface_id] != options[:interface_id]
|
413
|
-
raise
|
423
|
+
raise Errors::InvalidAddress, "#{address} is not associated with interface ID #{options[:interface_id]}"
|
414
424
|
end
|
415
425
|
|
416
426
|
if device
|
417
427
|
if device.name == 'eth0' && device.local_ips.first == eip[:private_ip_address]
|
418
|
-
raise
|
428
|
+
raise Errors::ClientOperationError, 'For safety, a public address cannot be dissociated from the primary IP on eth0'
|
419
429
|
end
|
420
|
-
elsif
|
421
|
-
raise
|
430
|
+
elsif Client.interface_attached(eip[:network_interface_id])
|
431
|
+
raise Errors::ClientOperationError, "#{address} is associated with an interface attached to another machine"
|
422
432
|
end
|
423
433
|
|
424
|
-
|
425
|
-
|
434
|
+
Client.disassociate_address(association_id: eip[:association_id])
|
435
|
+
Client.release_address(allocation_id: eip[:allocation_id]) if do_release
|
426
436
|
{
|
427
437
|
private_ip: eip[:private_ip_address],
|
428
438
|
device_name: device && device.name,
|
@@ -436,7 +446,7 @@ module Aws
|
|
436
446
|
|
437
447
|
# allocate a new elastic ip address
|
438
448
|
def allocate_elastic_ip
|
439
|
-
eip =
|
449
|
+
eip = Client.allocate_address(domain: 'vpc')
|
440
450
|
{
|
441
451
|
public_ip: eip[:public_ip],
|
442
452
|
allocation_id: eip[:allocation_id]
|
@@ -445,127 +455,41 @@ module Aws
|
|
445
455
|
|
446
456
|
# release the specified elastic ip address
|
447
457
|
def release_elastic_ip(ip)
|
448
|
-
eip = describe_address(ip)
|
458
|
+
eip = Client.describe_address(ip)
|
449
459
|
if eip[:association_id]
|
450
|
-
raise
|
460
|
+
raise Errors::ClientOperationError, "Elastic IP #{eip[:public_ip]} (#{eip[:allocation_id]}) is currently in use"
|
451
461
|
end
|
452
|
-
|
462
|
+
Client.release_address(allocation_id: eip[:allocation_id])
|
453
463
|
{
|
454
464
|
public_ip: eip[:public_ip],
|
455
465
|
allocation_id: eip[:allocation_id]
|
456
466
|
}
|
457
467
|
end
|
458
468
|
|
459
|
-
# test whether we have permission to modify our
|
460
|
-
def
|
461
|
-
|
469
|
+
# test whether we have permission to modify our machine's interface configuration
|
470
|
+
def has_interface_access?
|
471
|
+
Interface.mutable?
|
462
472
|
end
|
463
473
|
|
464
|
-
|
465
|
-
|
474
|
+
# throw exception if we cannot modify our machine's interface configuration
|
475
|
+
def assert_interface_access
|
476
|
+
raise Errors::InterfacePermissionError, 'Insufficient user priveleges to configure network interfaces' unless has_interface_access?
|
466
477
|
end
|
467
478
|
|
468
|
-
# test whether we have
|
469
|
-
#
|
470
|
-
def
|
471
|
-
|
472
|
-
test_methods = {
|
473
|
-
describe_network_interfaces: nil,
|
474
|
-
create_network_interface: {
|
475
|
-
subnet_id: 'subnet-abcd1234'
|
476
|
-
},
|
477
|
-
attach_network_interface: {
|
478
|
-
network_interface_id: 'eni-abcd1234',
|
479
|
-
instance_id: 'i-abcd1234',
|
480
|
-
device_index: 0
|
481
|
-
},
|
482
|
-
detach_network_interface: {
|
483
|
-
attachment_id: 'eni-attach-abcd1234'
|
484
|
-
},
|
485
|
-
delete_network_interface: {
|
486
|
-
network_interface_id: 'eni-abcd1234'
|
487
|
-
},
|
488
|
-
create_tags: {
|
489
|
-
resources: ['eni-abcd1234'],
|
490
|
-
tags: []
|
491
|
-
},
|
492
|
-
describe_addresses: nil,
|
493
|
-
allocate_address: nil,
|
494
|
-
release_address: {
|
495
|
-
dry_run: true,
|
496
|
-
allocation_id: 'eipalloc-no_exist'
|
497
|
-
},
|
498
|
-
associate_address: {
|
499
|
-
allocation_id: 'eipalloc-no_exist',
|
500
|
-
network_interface_id: 'eni-abcd1234'
|
501
|
-
}
|
502
|
-
# has no dry_run method
|
503
|
-
# assign_private_ip_addresses: {
|
504
|
-
# network_interface_id: 'eni-abcd1234'
|
505
|
-
# }
|
506
|
-
}
|
507
|
-
test_methods.each do |method, params|
|
508
|
-
begin
|
509
|
-
params ||= {}
|
510
|
-
params[:dry_run] = true
|
511
|
-
client.public_send(method, params)
|
512
|
-
raise Error, "Unexpected behavior while testing AWS API access"
|
513
|
-
rescue Aws::EC2::Errors::DryRunOperation
|
514
|
-
# success
|
515
|
-
rescue Aws::EC2::Errors::InvalidAllocationIDNotFound
|
516
|
-
# release_address does not properly support dry_run
|
517
|
-
rescue Aws::EC2::Errors::UnauthorizedOperation
|
518
|
-
return false
|
519
|
-
end
|
520
|
-
end
|
521
|
-
true
|
479
|
+
# test whether we have permission to perform all necessary EC2 operations
|
480
|
+
# within our given AWS access credentials
|
481
|
+
def has_client_access?
|
482
|
+
Client.has_access?
|
522
483
|
end
|
523
484
|
|
524
|
-
|
525
|
-
|
485
|
+
# throw exception if we do not have permissions to perform all needed EC2
|
486
|
+
# operations with our given AWS credentials
|
487
|
+
def assert_client_access
|
488
|
+
raise Errors::ClientPermissionError, 'Insufficient access to EC2 operations for network interface modification' unless has_client_access?
|
526
489
|
end
|
527
490
|
|
528
491
|
private
|
529
492
|
|
530
|
-
# use either an ip address or allocation id
|
531
|
-
def describe_address(address)
|
532
|
-
filter_by = case address
|
533
|
-
when /^eipalloc-/
|
534
|
-
'allocation-id'
|
535
|
-
when /^eipassoc-/
|
536
|
-
'association-id'
|
537
|
-
else
|
538
|
-
if IPAddr.new(environment[:vpc_cidr]) === IPAddr.new(address)
|
539
|
-
'private-ip-address'
|
540
|
-
else
|
541
|
-
'public-ip'
|
542
|
-
end
|
543
|
-
end
|
544
|
-
resp = client.describe_addresses(filters: [
|
545
|
-
{ name: 'domain', values: ['vpc'] },
|
546
|
-
{ name: filter_by, values: [address] }
|
547
|
-
])
|
548
|
-
raise InvalidParameterError, "IP #{address} could not be located" if resp[:addresses].empty?
|
549
|
-
resp[:addresses].first
|
550
|
-
end
|
551
|
-
|
552
|
-
def interface_ips(id)
|
553
|
-
resp = client.describe_network_interfaces(network_interface_ids: [id])
|
554
|
-
interface = resp[:network_interfaces].first
|
555
|
-
if interface && interface[:private_ip_addresses]
|
556
|
-
primary = interface[:private_ip_addresses].find { |ip| ip[:primary] }
|
557
|
-
interface[:private_ip_addresses].map { |ip| ip[:private_ip_address] }.tap do |ips|
|
558
|
-
# ensure primary ip is first in the list
|
559
|
-
ips.unshift(*ips.delete(primary[:private_ip_address])) if primary
|
560
|
-
end
|
561
|
-
end
|
562
|
-
end
|
563
|
-
|
564
|
-
def interface_status(id)
|
565
|
-
resp = client.describe_network_interfaces(network_interface_ids: [id])
|
566
|
-
resp[:network_interfaces].first[:status] unless resp[:network_interfaces].empty?
|
567
|
-
end
|
568
|
-
|
569
493
|
def wait_for(task, options = {}, &block)
|
570
494
|
errors = [*options[:rescue]]
|
571
495
|
timeout = options[:timeout] || self.timeout
|
@@ -574,13 +498,12 @@ module Aws
|
|
574
498
|
until timeout < 0
|
575
499
|
begin
|
576
500
|
break if block.call
|
577
|
-
rescue
|
578
|
-
raise unless errors.any? { |error| error === e }
|
501
|
+
rescue *errors => e
|
579
502
|
end
|
580
503
|
sleep interval
|
581
504
|
timeout -= interval
|
582
505
|
end
|
583
|
-
raise TimeoutError, "Timed out waiting for #{task}" unless timeout > 0
|
506
|
+
raise Errors::TimeoutError, "Timed out waiting for #{task}" unless timeout > 0
|
584
507
|
end
|
585
508
|
end
|
586
509
|
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'aws-eni/errors'
|
3
|
+
require 'aws-eni/meta'
|
4
|
+
|
5
|
+
module Aws
|
6
|
+
module ENI
|
7
|
+
module Client
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# determine the region from instance metadata
|
11
|
+
def region
|
12
|
+
Meta.instance('placement/availability-zone').sub(/^(.*)[a-z]$/,'\1')
|
13
|
+
rescue Errors::MetaConnectionFailed
|
14
|
+
raise Errors::EnvironmentError, 'Unable to load EC2 meta-data'
|
15
|
+
end
|
16
|
+
|
17
|
+
# determine the vpc cidr block from instance metadata
|
18
|
+
def vpc_cidr
|
19
|
+
hwaddr = Meta.instance('network/interfaces/macs/').lines.first.strip.chomp('/')
|
20
|
+
Meta.interface(hwaddr, 'vpc-ipv4-cidr-block')
|
21
|
+
rescue Errors::MetaConnectionFailed, Errors::MetaNotFound
|
22
|
+
raise Errors::EnvironmentError, 'Unable to load EC2 meta-data'
|
23
|
+
end
|
24
|
+
|
25
|
+
# lazy-load our ec2 client
|
26
|
+
def client
|
27
|
+
@client ||= EC2::Client.new(region: region)
|
28
|
+
rescue StandardError => e
|
29
|
+
raise Errors::EnvironmentError, 'Unable to initialize EC2 client'
|
30
|
+
end
|
31
|
+
|
32
|
+
# pass along method calls to our lazy-loaded api client
|
33
|
+
def method_missing(method, *args)
|
34
|
+
client.public_send(method, *args)
|
35
|
+
rescue EC2::Errors::UnauthorizedOperation => e
|
36
|
+
raise Errors::ClientPermissionError, "Operation not permitted: #{e.message}"
|
37
|
+
rescue EC2::Errors::ServiceError => e
|
38
|
+
error = e.class.to_s.gsub(/^.*::/, '')
|
39
|
+
raise Errors::ClientOperationError, "EC2 service error (#{error}: #{e.message})"
|
40
|
+
end
|
41
|
+
|
42
|
+
# retrieve a single interface resource
|
43
|
+
def describe_interface(id)
|
44
|
+
resp = describe_network_interfaces(network_interface_ids: [id])
|
45
|
+
raise Errors::UnknownInterface, "Interface #{id} could not be located" if resp[:network_interfaces].empty?
|
46
|
+
resp[:network_interfaces].first
|
47
|
+
end
|
48
|
+
|
49
|
+
# retrieve a single address resource by public ip, associated private ip,
|
50
|
+
# allocation id, or association id
|
51
|
+
def describe_address(address)
|
52
|
+
filter_by = case address
|
53
|
+
when /^eipalloc-/
|
54
|
+
'allocation-id'
|
55
|
+
when /^eipassoc-/
|
56
|
+
'association-id'
|
57
|
+
else
|
58
|
+
if IPAddr.new(vpc_cidr) === IPAddr.new(address)
|
59
|
+
'private-ip-address'
|
60
|
+
else
|
61
|
+
'public-ip'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
resp = describe_addresses(filters: [
|
65
|
+
{ name: 'domain', values: ['vpc'] },
|
66
|
+
{ name: filter_by, values: [address] }
|
67
|
+
])
|
68
|
+
raise Errors::UnknownAddress, "IP #{address} could not be located" if resp[:addresses].empty?
|
69
|
+
resp[:addresses].first
|
70
|
+
end
|
71
|
+
|
72
|
+
# retrieve an array of private ips associated with the given interface
|
73
|
+
def interface_private_ips(id)
|
74
|
+
interface = describe_interface(id)
|
75
|
+
if interface[:private_ip_addresses]
|
76
|
+
primary = interface[:private_ip_addresses].find { |ip| ip[:primary] }
|
77
|
+
interface[:private_ip_addresses].map { |ip| ip[:private_ip_address] }.tap do |ips|
|
78
|
+
# ensure primary ip is first in the list
|
79
|
+
ips.unshift(*ips.delete(primary[:private_ip_address])) if primary
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# determine whether a given interface is attached or free
|
85
|
+
def interface_attached(id)
|
86
|
+
describe_interface(id)[:status] == 'in-use'
|
87
|
+
end
|
88
|
+
|
89
|
+
# test whether we have the appropriate permissions within our AWS access
|
90
|
+
# credentials to perform all possible API calls
|
91
|
+
def has_access?
|
92
|
+
test_methods = {
|
93
|
+
describe_network_interfaces: {},
|
94
|
+
create_network_interface: {
|
95
|
+
subnet_id: 'subnet-abcd1234'
|
96
|
+
},
|
97
|
+
attach_network_interface: {
|
98
|
+
network_interface_id: 'eni-abcd1234',
|
99
|
+
instance_id: 'i-abcd1234',
|
100
|
+
device_index: 0
|
101
|
+
},
|
102
|
+
detach_network_interface: {
|
103
|
+
attachment_id: 'eni-attach-abcd1234'
|
104
|
+
},
|
105
|
+
delete_network_interface: {
|
106
|
+
network_interface_id: 'eni-abcd1234'
|
107
|
+
},
|
108
|
+
create_tags: {
|
109
|
+
resources: ['eni-abcd1234'],
|
110
|
+
tags: []
|
111
|
+
},
|
112
|
+
describe_addresses: {},
|
113
|
+
allocate_address: {},
|
114
|
+
release_address: {
|
115
|
+
allocation_id: 'eipalloc-abcd1234'
|
116
|
+
},
|
117
|
+
associate_address: {
|
118
|
+
allocation_id: 'eipalloc-abcd1234',
|
119
|
+
network_interface_id: 'eni-abcd1234'
|
120
|
+
},
|
121
|
+
disassociate_address: {
|
122
|
+
association_id: 'eipassoc-abcd1234'
|
123
|
+
}
|
124
|
+
# these have no dry_run method
|
125
|
+
# assign_private_ip_addresses: {
|
126
|
+
# network_interface_id: 'eni-abcd1234'
|
127
|
+
# }
|
128
|
+
# unassign_private_ip_addresses: {
|
129
|
+
# network_interface_id: 'eni-abcd1234',
|
130
|
+
# private_ip_addresses: ['0.0.0.0']
|
131
|
+
# }
|
132
|
+
}
|
133
|
+
test_methods.all? do |method, params|
|
134
|
+
begin
|
135
|
+
client.public_send(method, params.merge(dry_run: true))
|
136
|
+
rescue EC2::Errors::DryRunOperation
|
137
|
+
true
|
138
|
+
rescue EC2::Errors::InvalidAllocationIDNotFound
|
139
|
+
# release_address does not properly support dry_run
|
140
|
+
true
|
141
|
+
rescue EC2::Errors::UnauthorizedOperation
|
142
|
+
false
|
143
|
+
rescue
|
144
|
+
raise Errors::ClientOperationError, 'Unexpected behavior while testing EC2 client permissions'
|
145
|
+
else
|
146
|
+
raise Errors::ClientOperationError, 'Unexpected behavior while testing EC2 client permissions'
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/lib/aws-eni/errors.rb
CHANGED
@@ -1,16 +1,29 @@
|
|
1
|
-
|
2
1
|
module Aws
|
3
2
|
module ENI
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
module Errors
|
4
|
+
class ServiceError < RuntimeError; end
|
5
|
+
|
6
|
+
class EnvironmentError < ServiceError; end
|
7
|
+
|
8
|
+
class MetaNotFound < ServiceError; end
|
9
|
+
class MetaBadResponse < ServiceError; end
|
10
|
+
class MetaConnectionFailed < ServiceError; end
|
11
|
+
|
12
|
+
class InterfaceOperationError < ServiceError; end
|
13
|
+
class InterfacePermissionError < InterfaceOperationError; end
|
14
|
+
|
15
|
+
class ClientOperationError < ServiceError; end
|
16
|
+
class ClientPermissionError < ClientOperationError; end
|
17
|
+
|
18
|
+
class MissingInput < ServiceError; end
|
19
|
+
class InvalidInput < ServiceError; end
|
20
|
+
class TimeoutError < ServiceError; end
|
21
|
+
|
22
|
+
class UnknownInterface < ServiceError; end
|
23
|
+
class InvalidInterface < ServiceError; end
|
24
|
+
|
25
|
+
class UnknownAddress < ServiceError; end
|
26
|
+
class InvalidAddress < ServiceError; end
|
27
|
+
end
|
15
28
|
end
|
16
29
|
end
|
@@ -4,7 +4,7 @@ require 'aws-eni/errors'
|
|
4
4
|
|
5
5
|
module Aws
|
6
6
|
module ENI
|
7
|
-
class
|
7
|
+
class Interface
|
8
8
|
|
9
9
|
class << self
|
10
10
|
include Enumerable
|
@@ -28,7 +28,7 @@ module Aws
|
|
28
28
|
when /^[0-9\.]+$/
|
29
29
|
find { |dev| dev.has_ip?(index) }
|
30
30
|
end.tap do |dev|
|
31
|
-
raise
|
31
|
+
raise Errors::UnknownInterface, "No interface found matching #{index}" unless dev
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -76,11 +76,11 @@ module Aws
|
|
76
76
|
when nil
|
77
77
|
to_a
|
78
78
|
when /^eni-/, /^eth[0-9]+$/, /^[0-9a-f:]+$/i, /^[0-9\.]+$/
|
79
|
-
self[filter]
|
79
|
+
[*self[filter]]
|
80
80
|
when /^subnet-/
|
81
81
|
select { |dev| dev.subnet_id == filter }
|
82
82
|
end.tap do |devs|
|
83
|
-
raise
|
83
|
+
raise Errors::UnknownInterface, "No interface found matching #{filter}" if devs.nil? || devs.empty?
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
@@ -88,7 +88,7 @@ module Aws
|
|
88
88
|
def mutable?
|
89
89
|
cmd('link set dev eth0') # innocuous command
|
90
90
|
true
|
91
|
-
rescue
|
91
|
+
rescue Errors::InterfacePermissionError
|
92
92
|
false
|
93
93
|
end
|
94
94
|
|
@@ -98,10 +98,10 @@ module Aws
|
|
98
98
|
options[:errors] = true
|
99
99
|
begin
|
100
100
|
exec("/sbin/ip #{command}", options)
|
101
|
-
rescue
|
101
|
+
rescue Errors::InterfaceOperationError => e
|
102
102
|
case e.message
|
103
103
|
when /operation not permitted/i
|
104
|
-
raise
|
104
|
+
raise Errors::InterfacePermissionError, "Operation not permitted"
|
105
105
|
else
|
106
106
|
raise if errors
|
107
107
|
end
|
@@ -128,7 +128,7 @@ module Aws
|
|
128
128
|
else
|
129
129
|
error = e.read
|
130
130
|
warn "Warning: #{error}" if verbose
|
131
|
-
raise
|
131
|
+
raise Errors::InterfaceOperationError, error if errors
|
132
132
|
end
|
133
133
|
end
|
134
134
|
output
|
@@ -139,7 +139,7 @@ module Aws
|
|
139
139
|
|
140
140
|
def initialize(name, auto_config = true)
|
141
141
|
unless name =~ /^eth([0-9]+)$/
|
142
|
-
raise
|
142
|
+
raise Errors::InvalidInterface, "Invalid interface: #{name}"
|
143
143
|
end
|
144
144
|
@name = name
|
145
145
|
@device_number = $1.to_i
|
@@ -153,7 +153,7 @@ module Aws
|
|
153
153
|
exists? && IO.read("/sys/class/net/#{name}/address").strip
|
154
154
|
rescue Errno::ENOENT
|
155
155
|
end.tap do |address|
|
156
|
-
raise
|
156
|
+
raise Errors::UnknownInterface, "Interface #{name} not found on this machine" unless address
|
157
157
|
end
|
158
158
|
end
|
159
159
|
|
@@ -167,15 +167,14 @@ module Aws
|
|
167
167
|
# Validate and return basic interface metadata
|
168
168
|
def info
|
169
169
|
hwaddr = self.hwaddr
|
170
|
-
unless @meta_cache &&
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
subnet_cidr: Meta.http_get(conn, "#{dev_path}/subnet-ipv4-cidr-block")
|
170
|
+
unless @meta_cache && @meta_cache[:hwaddr] == hwaddr
|
171
|
+
@meta_cache = Meta.connection do
|
172
|
+
raise MetaBadResponse unless Meta.interface(hwaddr, '')
|
173
|
+
{
|
174
|
+
hwaddr: hwaddr,
|
175
|
+
interface_id: Meta.interface(hwaddr, 'interface-id'),
|
176
|
+
subnet_id: Meta.interface(hwaddr, 'subnet-id'),
|
177
|
+
subnet_cidr: Meta.interface(hwaddr, 'subnet-ipv4-cidr-block')
|
179
178
|
}.freeze
|
180
179
|
end
|
181
180
|
end
|
@@ -210,17 +209,15 @@ module Aws
|
|
210
209
|
end
|
211
210
|
|
212
211
|
def public_ips
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
ip_assoc[local_ip] = public_ip
|
212
|
+
hwaddr = self.hwaddr
|
213
|
+
Hash[
|
214
|
+
Meta.connection do
|
215
|
+
Meta.interface(hwaddr, 'ipv4-associations/', not_found: '', cache: false).lines.map do |public_ip|
|
216
|
+
public_ip.strip!
|
217
|
+
[ Meta.interface(hwaddr, "ipv4-associations/#{public_ip}", cache: false), public_ip ]
|
218
|
+
end
|
221
219
|
end
|
222
|
-
|
223
|
-
ip_assoc
|
220
|
+
]
|
224
221
|
end
|
225
222
|
|
226
223
|
# Enable our interface and create necessary routes
|
@@ -245,7 +242,7 @@ module Aws
|
|
245
242
|
changes = 0
|
246
243
|
prefix = self.prefix # prevent exists? check on each use
|
247
244
|
|
248
|
-
meta_ips = Meta.
|
245
|
+
meta_ips = Meta.interface(hwaddr, 'local-ipv4s', cache: false).lines.map(&:strip)
|
249
246
|
local_primary, *local_aliases = local_ips
|
250
247
|
meta_primary, *meta_aliases = meta_ips
|
251
248
|
|
@@ -364,14 +361,14 @@ module Aws
|
|
364
361
|
when :ip, :has_ip
|
365
362
|
"Interface #{name} does not have IP #{val}" unless has_ip? val
|
366
363
|
when :public_ip
|
367
|
-
"Interface #{name} does not have
|
364
|
+
"Interface #{name} does not have public IP #{val}" unless public_ips.has_value? val
|
368
365
|
when :local_ip, :private_ip
|
369
366
|
"Interface #{name} does not have private IP #{val}" unless local_ips.include? val
|
370
367
|
else
|
371
368
|
"Unknown attribute: #{attr}"
|
372
369
|
end
|
373
370
|
end
|
374
|
-
raise
|
371
|
+
raise Errors::UnknownInterface, error if error
|
375
372
|
self
|
376
373
|
end
|
377
374
|
|
data/lib/aws-eni/meta.rb
CHANGED
@@ -6,42 +6,64 @@ module Aws
|
|
6
6
|
module ENI
|
7
7
|
module Meta
|
8
8
|
|
9
|
-
# EC2 instance meta-data connection settings
|
10
|
-
HOST = '169.254.169.254'
|
11
|
-
PORT = '80'
|
12
|
-
BASE = '/latest/meta-data/'
|
13
|
-
|
14
9
|
# These are the errors we trap when attempting to talk to the instance
|
15
|
-
# metadata service. Any of these imply the service is not present,
|
10
|
+
# metadata service. Any of these imply the service is not present, not
|
16
11
|
# responding or some other non-recoverable error.
|
17
12
|
FAILURES = [
|
18
13
|
Errno::EHOSTUNREACH,
|
19
14
|
Errno::ECONNREFUSED,
|
20
15
|
Errno::EHOSTDOWN,
|
21
16
|
Errno::ENETUNREACH,
|
22
|
-
SocketError,
|
23
17
|
Timeout::Error,
|
24
|
-
|
18
|
+
SocketError,
|
19
|
+
Errors::MetaBadResponse,
|
25
20
|
]
|
26
21
|
|
27
|
-
#
|
28
|
-
#
|
22
|
+
# Perform a GET request on an open HTTP connection to the EC2 instance
|
23
|
+
# meta-data and return the body of any 200 response.
|
29
24
|
def self.get(path, options = {})
|
30
|
-
|
31
|
-
|
25
|
+
@cache ||= {}
|
26
|
+
if @cache[path] && options[:cache] != false
|
27
|
+
@cache[path]
|
28
|
+
else
|
29
|
+
connection(options) do |http|
|
30
|
+
response = http.request(Net::HTTP::Get.new(path))
|
31
|
+
case response.code.to_i
|
32
|
+
when 200
|
33
|
+
@cache[path] = response.body
|
34
|
+
when 404
|
35
|
+
raise Errors::MetaNotFound unless options[:not_found]
|
36
|
+
options[:not_found]
|
37
|
+
else
|
38
|
+
raise Errors::MetaBadResponse
|
39
|
+
end
|
40
|
+
end
|
32
41
|
end
|
33
42
|
end
|
34
43
|
|
44
|
+
# Perform a GET request on the instance metadata and return the body of
|
45
|
+
# any 200 response.
|
46
|
+
def self.instance(path, options = {})
|
47
|
+
get("/latest/meta-data/#{path}", options)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Perform a GET request on the interface metadata and return the body of
|
51
|
+
# any 200 response.
|
52
|
+
def self.interface(hwaddr, path, options = {})
|
53
|
+
instance("network/interfaces/macs/#{hwaddr}/#{path}", options)
|
54
|
+
end
|
55
|
+
|
35
56
|
# Open a connection and attempt to execute the block `retries` times.
|
36
57
|
# Can specify open and read timeouts in addition to the number of retries.
|
37
|
-
def self.
|
58
|
+
def self.connection(options = {})
|
59
|
+
return yield(@open_connection) if @open_connection
|
38
60
|
retries = options[:retries] || 5
|
39
61
|
failed_attempts = 0
|
40
62
|
begin
|
41
|
-
http = Net::HTTP.new(
|
63
|
+
http = Net::HTTP.new('169.254.169.254', '80', nil)
|
42
64
|
http.open_timeout = options[:open_timeout] || 5
|
43
65
|
http.read_timeout = options[:read_timeout] || 5
|
44
|
-
http.start
|
66
|
+
@open_connection = http.start
|
45
67
|
yield(http).tap { http.finish }
|
46
68
|
rescue *FAILURES => e
|
47
69
|
if failed_attempts < retries
|
@@ -50,22 +72,10 @@ module Aws
|
|
50
72
|
failed_attempts += 1
|
51
73
|
retry
|
52
74
|
else
|
53
|
-
raise
|
75
|
+
raise Errors::MetaConnectionFailed, "EC2 Metadata request failed after #{retries} retries."
|
54
76
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
# Perform a GET request on an open connection to the instance metadata
|
59
|
-
# endpoint and return the body of any 200 response.
|
60
|
-
def self.http_get(connection, path)
|
61
|
-
response = connection.request(Net::HTTP::Get.new(BASE + path))
|
62
|
-
case response.code.to_i
|
63
|
-
when 200
|
64
|
-
response.body
|
65
|
-
when 404
|
66
|
-
nil
|
67
|
-
else
|
68
|
-
raise BadResponse
|
77
|
+
ensure
|
78
|
+
@open_connection = nil
|
69
79
|
end
|
70
80
|
end
|
71
81
|
end
|
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.2.
|
4
|
+
version: 0.2.2
|
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-05-
|
11
|
+
date: 2015-05-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gli
|
@@ -83,8 +83,9 @@ files:
|
|
83
83
|
- aws-eni.gemspec
|
84
84
|
- bin/aws-eni
|
85
85
|
- lib/aws-eni.rb
|
86
|
+
- lib/aws-eni/client.rb
|
86
87
|
- lib/aws-eni/errors.rb
|
87
|
-
- lib/aws-eni/
|
88
|
+
- lib/aws-eni/interface.rb
|
88
89
|
- lib/aws-eni/meta.rb
|
89
90
|
- lib/aws-eni/version.rb
|
90
91
|
homepage: http://pixelcog.com/
|