ec2ctl 0.7.9 → 0.8.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.
- checksums.yaml +4 -4
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE +21 -0
- data/README.md +32 -2
- data/Rakefile +0 -5
- data/ec2ctl.gemspec +17 -15
- data/lib/ec2ctl/cli.rb +202 -72
- data/lib/ec2ctl/client.rb +549 -21
- data/lib/ec2ctl/logger.rb +179 -0
- data/lib/ec2ctl/version.rb +1 -1
- metadata +43 -38
data/lib/ec2ctl/client.rb
CHANGED
@@ -1,39 +1,567 @@
|
|
1
1
|
require "aws-sdk"
|
2
|
+
require "timeout"
|
2
3
|
|
3
4
|
module EC2Ctl
|
4
5
|
class Client
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
PingCheckError = Class.new RuntimeError
|
7
|
+
CommandNotSucceeded = Class.new RuntimeError
|
8
|
+
NoInstanceForExecCommand = Class.new RuntimeError
|
9
|
+
InvalidFilter = Class.new RuntimeError
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
acc[attribute] = case attribute
|
13
|
-
when /\Atag:/i
|
14
|
-
tag = instance.tags.find {|tag| tag.key == attribute.split(":")[1..-1].join(":")}
|
11
|
+
INSTANCE_IDS_LIMIT = 50
|
12
|
+
COMMAND_STATUS_FINISHED = %w(Success TimedOut Cancelled Failed).freeze
|
13
|
+
COMMAND_STATUS_NOT_SUCCEEDED = %w(TimedOut Cancelled Failed).freeze
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
SSM_DOCUMENT_NAMES = {
|
16
|
+
Linux: "AWS-RunShellScript",
|
17
|
+
Windows: "AWS-RunPowerShellScript",
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def initialize(
|
21
|
+
logger: nil,
|
22
|
+
load_balancer_name: nil,
|
23
|
+
platform_type: "Linux",
|
24
|
+
skip_ping_check: false,
|
25
|
+
commands: [],
|
26
|
+
working_directory: nil,
|
27
|
+
execution_timeout: 3600,
|
28
|
+
skip_command_waits: false,
|
29
|
+
wait_interval: 5,
|
30
|
+
timeout_seconds: nil,
|
31
|
+
comment: "Started at #{Time.now} (#{self.class}@#{VERSION})",
|
32
|
+
output_s3_bucket_name: nil,
|
33
|
+
output_s3_key_prefix: nil,
|
34
|
+
service_role_arn: nil,
|
35
|
+
notification_arn: nil,
|
36
|
+
notification_events: nil,
|
37
|
+
notification_type: nil,
|
38
|
+
rolling_group_size: 1,
|
39
|
+
skip_draining_waits: false,
|
40
|
+
skip_inservice_waits: false,
|
41
|
+
inservice_wait_timeout: 180,
|
42
|
+
instance_ids: [],
|
43
|
+
filters: [],
|
44
|
+
attributes: [],
|
45
|
+
search: []
|
46
|
+
)
|
47
|
+
@logger = logger
|
48
|
+
|
49
|
+
@ec2_resource = Aws::EC2::Resource.new
|
50
|
+
@elb_client = Aws::ElasticLoadBalancing::Client.new
|
51
|
+
@ssm_client = Aws::SSM::Client.new
|
52
|
+
|
53
|
+
@load_balancer_name = load_balancer_name
|
54
|
+
@platform_type = platform_type
|
55
|
+
@skip_ping_check = skip_ping_check
|
56
|
+
@commands = commands
|
57
|
+
@working_directory = working_directory
|
58
|
+
@execution_timeout = execution_timeout
|
59
|
+
@skip_command_waits = skip_command_waits
|
60
|
+
@wait_interval = wait_interval
|
61
|
+
@timeout_seconds = timeout_seconds
|
62
|
+
@comment = comment
|
63
|
+
@output_s3_bucket_name = output_s3_bucket_name
|
64
|
+
@output_s3_key_prefix = output_s3_key_prefix
|
65
|
+
@service_role_arn = service_role_arn
|
66
|
+
@notification_arn = notification_arn
|
67
|
+
@notification_events = notification_events
|
68
|
+
@notification_type = notification_type
|
69
|
+
@skip_draining_waits = skip_draining_waits
|
70
|
+
@skip_inservice_waits = skip_inservice_waits
|
71
|
+
@inservice_wait_timeout = inservice_wait_timeout
|
72
|
+
@instance_ids = instance_ids
|
73
|
+
@filters = filters
|
74
|
+
@attributes = attributes
|
75
|
+
@search = search
|
76
|
+
|
77
|
+
if @load_balancer_name
|
78
|
+
@elb_instance_ids = elb_instance_states.map(&:instance_id).select do |instance_id|
|
79
|
+
if instance_ids.empty?
|
80
|
+
true
|
21
81
|
else
|
22
|
-
|
23
|
-
|
82
|
+
instance_ids.include? instance_id
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
@rolling_group_size = [rolling_group_size, @elb_instance_ids.size].min
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def ec2_list
|
91
|
+
if @logger
|
92
|
+
if @logger.debug?
|
93
|
+
@logger.debug ec2_instances: ec2_instances
|
94
|
+
else
|
95
|
+
ec2_instances_summary = ec2_instances.each_with_object Array.new do |instance, instances_memo|
|
96
|
+
instance_summary = @attributes.each_with_object Hash.new do |attribute, attributes_memo|
|
97
|
+
attributes_memo[attribute] = query_instance_attribute(instance, attribute)
|
24
98
|
end
|
99
|
+
|
100
|
+
instances_memo.push instance_summary
|
25
101
|
end
|
26
102
|
|
27
|
-
|
103
|
+
@logger.info ec2_instances_summary: ec2_instances_summary
|
28
104
|
end
|
105
|
+
end
|
106
|
+
|
107
|
+
{ec2_instances: ec2_instances}
|
108
|
+
end
|
29
109
|
|
30
|
-
|
31
|
-
|
110
|
+
def ec2_execute
|
111
|
+
ping_check ec2_instances.map(&:instance_id) unless @skip_ping_check
|
112
|
+
execute_commands ec2_instances.map(&:instance_id)
|
113
|
+
end
|
114
|
+
|
115
|
+
def elb_list
|
116
|
+
if @logger
|
117
|
+
if @logger.debug?
|
118
|
+
@logger.debug load_balancer_descriptions: load_balancer_descriptions
|
32
119
|
else
|
33
|
-
|
120
|
+
@logger.info load_balancer_descriptions_summary: load_balancer_descriptions.map {|lb|
|
121
|
+
{
|
122
|
+
load_balancer_name: lb.load_balancer_name,
|
123
|
+
dns_name: lb.dns_name,
|
124
|
+
instances: lb.instances.size,
|
125
|
+
}
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
{load_balancer_descriptions: load_balancer_descriptions}
|
131
|
+
end
|
132
|
+
|
133
|
+
def elb_status
|
134
|
+
load_balancer_description = load_balancer_descriptions.first
|
135
|
+
|
136
|
+
if @logger
|
137
|
+
@logger.info elb_instance_counts: elb_instance_counts
|
138
|
+
@logger.info elb_instance_states: elb_instance_states
|
139
|
+
|
140
|
+
instance_ids = elb_instance_states.map(&:instance_id)
|
141
|
+
|
142
|
+
@logger.debug ssm_instance_ping_counts: ssm_instance_ping_counts(instance_ids)
|
143
|
+
@logger.debug ssm_instance_states: ssm_instance_states(instance_ids)
|
144
|
+
@logger.debug load_balancer_description: load_balancer_description
|
145
|
+
@logger.debug load_balancer_attributes: load_balancer_attributes
|
146
|
+
end
|
147
|
+
|
148
|
+
{
|
149
|
+
load_balancer_description: load_balancer_description,
|
150
|
+
load_balancer_attributes: load_balancer_attributes,
|
151
|
+
elb_instance_states: elb_instance_states,
|
152
|
+
}
|
153
|
+
end
|
154
|
+
|
155
|
+
def elb_attach
|
156
|
+
attach_instances @instance_ids
|
157
|
+
end
|
158
|
+
|
159
|
+
def elb_detach
|
160
|
+
detach_instances @instance_ids
|
161
|
+
end
|
162
|
+
|
163
|
+
def elb_execute
|
164
|
+
ping_check @elb_instance_ids unless @skip_ping_check
|
165
|
+
execute_commands @elb_instance_ids
|
166
|
+
end
|
167
|
+
|
168
|
+
def elb_graceful
|
169
|
+
ping_check @elb_instance_ids unless @skip_ping_check
|
170
|
+
|
171
|
+
@elb_instance_ids.each_slice(@rolling_group_size).to_a.tap do |instance_id_groups|
|
172
|
+
instance_id_groups.each.with_index do |instance_id_group, group_index|
|
173
|
+
@logger.info(progress: {
|
174
|
+
completed: @rolling_group_size * group_index,
|
175
|
+
remaining: @elb_instance_ids.size - @rolling_group_size * group_index,
|
176
|
+
total: @elb_instance_ids.size,
|
177
|
+
}) if @logger
|
178
|
+
|
179
|
+
detach_instances instance_id_group
|
180
|
+
wait_draining instance_id_group unless @skip_draining_waits
|
181
|
+
execute_commands instance_id_group
|
182
|
+
attach_instances instance_id_group
|
183
|
+
wait_inservice instance_id_group unless @skip_inservice_waits
|
184
|
+
end
|
185
|
+
|
186
|
+
@logger.info "Everything done!" if @logger
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def detach_instances(instance_ids = [])
|
191
|
+
response = @elb_client.deregister_instances_from_load_balancer(
|
192
|
+
load_balancer_name: @load_balancer_name,
|
193
|
+
instances: instance_ids.map {|i| {instance_id: i}},
|
194
|
+
)
|
195
|
+
|
196
|
+
status = {
|
197
|
+
detached: instance_ids,
|
198
|
+
registered: response.instances.map(&:instance_id),
|
199
|
+
}
|
200
|
+
|
201
|
+
if @logger
|
202
|
+
if @logger.debug?
|
203
|
+
@logger.debug status: status
|
204
|
+
else
|
205
|
+
@logger.info detached: instance_ids
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
status
|
210
|
+
end
|
211
|
+
|
212
|
+
def attach_instances(instance_ids = [])
|
213
|
+
response = @elb_client.register_instances_with_load_balancer(
|
214
|
+
load_balancer_name: @load_balancer_name,
|
215
|
+
instances: instance_ids.map {|i| {instance_id: i}},
|
216
|
+
)
|
217
|
+
|
218
|
+
status = {
|
219
|
+
attached: instance_ids,
|
220
|
+
registered: response.instances.map(&:instance_id),
|
221
|
+
}
|
222
|
+
|
223
|
+
if @logger
|
224
|
+
if @logger.debug?
|
225
|
+
@logger.debug status: status
|
226
|
+
else
|
227
|
+
@logger.info attached: instance_ids
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
status
|
232
|
+
end
|
233
|
+
|
234
|
+
def wait_draining(instance_ids = [])
|
235
|
+
connection_draining = load_balancer_attributes.connection_draining
|
236
|
+
|
237
|
+
unless connection_draining.enabled
|
238
|
+
@logger.info wait_draining_timeout: "Disabled.".freeze if @logger
|
239
|
+
return
|
240
|
+
end
|
241
|
+
|
242
|
+
if @logger
|
243
|
+
@logger.debug detached_instances: elb_instance_states!(instance_ids)
|
244
|
+
@logger.info wait_draining_timeout: connection_draining.timeout
|
245
|
+
end
|
246
|
+
|
247
|
+
sleep connection_draining.timeout
|
248
|
+
end
|
249
|
+
|
250
|
+
def wait_inservice(instance_ids = [])
|
251
|
+
s = elb_instance_states! instance_ids
|
252
|
+
|
253
|
+
@logger.info wait_instance_inservice: s if @logger
|
254
|
+
|
255
|
+
Timeout.timeout @inservice_wait_timeout do
|
256
|
+
loop do
|
257
|
+
sleep @wait_interval
|
258
|
+
|
259
|
+
s = elb_instance_states! instance_ids
|
260
|
+
@logger.debug wait_instance_inservice: s if @logger
|
261
|
+
|
262
|
+
break if s.all? {|i| i.state == "InService".freeze}
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
@logger.info wait_instance_inservice: s if @logger
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
def elb_instance_states!(instance_ids = nil)
|
272
|
+
@elb_instance_states = @elb_client.describe_instance_health(
|
273
|
+
load_balancer_name: @load_balancer_name
|
274
|
+
).instance_states
|
275
|
+
|
276
|
+
if instance_ids
|
277
|
+
elb_instance_states.select do |i|
|
278
|
+
instance_ids.include? i.instance_id
|
279
|
+
end
|
280
|
+
else
|
281
|
+
@elb_instance_states
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def command_console_url(command_id)
|
286
|
+
[
|
287
|
+
"https://".freeze,
|
288
|
+
@ssm_client.config.region,
|
289
|
+
".console.aws.amazon.com/ec2/v2/home?region=".freeze,
|
290
|
+
@ssm_client.config.region,
|
291
|
+
"#Commands:CommandId=".freeze,
|
292
|
+
command_id,
|
293
|
+
].join
|
294
|
+
end
|
295
|
+
|
296
|
+
def wait_command_finish(command_id)
|
297
|
+
Timeout.timeout(@execution_timeout + @wait_interval * 3) do
|
298
|
+
loop do
|
299
|
+
command_invocations = get_command_invocations(command_id)
|
300
|
+
|
301
|
+
@logger.debug command_invocations: command_invocations if @logger
|
302
|
+
|
303
|
+
return command_invocations if command_invocations.all? {|i| COMMAND_STATUS_FINISHED.include? i.status}
|
304
|
+
|
305
|
+
sleep @wait_interval
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def get_command_invocations(command_id)
|
311
|
+
command_invocations = []
|
312
|
+
next_token = nil
|
313
|
+
|
314
|
+
loop do
|
315
|
+
response = @ssm_client.list_command_invocations(
|
316
|
+
command_id: command_id,
|
317
|
+
details: true,
|
318
|
+
next_token: next_token,
|
319
|
+
)
|
320
|
+
|
321
|
+
command_invocations += response.command_invocations
|
322
|
+
next_token = response.next_token
|
323
|
+
|
324
|
+
return command_invocations unless next_token
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def command_params
|
329
|
+
return @command_params if @command_params
|
330
|
+
|
331
|
+
@command_params = {
|
332
|
+
commands: @commands,
|
333
|
+
}
|
334
|
+
|
335
|
+
@command_params.update workingDirectory: [@working_directory] if @working_directory
|
336
|
+
@command_params.update executionTimeout: [@execution_timeout.to_s] if @execution_timeout
|
337
|
+
@command_params
|
338
|
+
end
|
339
|
+
|
340
|
+
def nortification_config
|
341
|
+
return @nortification_config if @nortification_config
|
342
|
+
|
343
|
+
@nortification_config = {}
|
344
|
+
@nortification_config.update notification_arn: @notification_arn if @notification_arn
|
345
|
+
@nortification_config.update notification_events: @notification_events if @notification_events
|
346
|
+
@nortification_config.update notification_type: @notification_type if @notification_type
|
347
|
+
@nortification_config
|
348
|
+
end
|
349
|
+
|
350
|
+
def send_command_params(instance_ids)
|
351
|
+
{
|
352
|
+
document_name: SSM_DOCUMENT_NAMES[@platform_type.intern],
|
353
|
+
instance_ids: instance_ids,
|
354
|
+
parameters: command_params,
|
355
|
+
comment: @comment,
|
356
|
+
output_s3_bucket_name: @output_s3_bucket_name,
|
357
|
+
output_s3_key_prefix: @output_s3_key_prefix,
|
358
|
+
service_role_arn: @service_role_arn,
|
359
|
+
notification_config: @notification_config,
|
360
|
+
}
|
361
|
+
end
|
362
|
+
|
363
|
+
def ping_check(instance_ids = [])
|
364
|
+
messages = []
|
365
|
+
|
366
|
+
check = instance_ids.each_with_object(Hash.new(0)) do |instance_id, memo|
|
367
|
+
instance = ssm_instance_states(instance_ids).find {|si| si.instance_id == instance_id}
|
368
|
+
status = instance.nil? ? "NotFound" : instance.ping_status
|
369
|
+
|
370
|
+
messages.push "#{instance_id}'s SSM agent is #{status}." unless status == "Online"
|
371
|
+
|
372
|
+
memo[status] += 1
|
373
|
+
end
|
374
|
+
|
375
|
+
@logger.debug ping_check: check if @logger
|
376
|
+
|
377
|
+
unless check.reject {|k, v| k == "Online"}.empty?
|
378
|
+
fail PingCheckError, messages.join(" ")
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def execute_commands(instance_ids = [])
|
383
|
+
fail NoInstanceForExecCommand, "No instance for executing commands." if instance_ids.empty?
|
384
|
+
|
385
|
+
instance_ids.each_slice INSTANCE_IDS_LIMIT do |instance_ids_slice|
|
386
|
+
send_command_result = @ssm_client.send_command(send_command_params(instance_ids_slice))
|
387
|
+
command_id = send_command_result.command.command_id
|
388
|
+
|
389
|
+
if @logger
|
390
|
+
if @logger.debug?
|
391
|
+
@logger.debug command: send_command_result.command
|
392
|
+
else
|
393
|
+
@logger.info(
|
394
|
+
command_summary: {
|
395
|
+
command_id: command_id,
|
396
|
+
console_url: command_console_url(command_id),
|
397
|
+
commands: @commands,
|
398
|
+
instance_ids: instance_ids_slice,
|
399
|
+
}
|
400
|
+
)
|
401
|
+
end
|
34
402
|
end
|
35
403
|
|
36
|
-
|
404
|
+
unless @skip_command_waits
|
405
|
+
command_invocations = wait_command_finish(command_id)
|
406
|
+
|
407
|
+
if @logger
|
408
|
+
if @logger.debug?
|
409
|
+
@logger.debug command_invocations: command_invocations
|
410
|
+
else
|
411
|
+
@logger.info(
|
412
|
+
command_invocations_status_counts: command_invocations.each_with_object(Hash.new(0)) {|invocation, memo|
|
413
|
+
memo[invocation.status] += 1
|
414
|
+
}
|
415
|
+
)
|
416
|
+
|
417
|
+
@logger.info(
|
418
|
+
command_invocations_summary: command_invocations.map {|invocation|
|
419
|
+
{
|
420
|
+
instance_id: invocation.instance_id,
|
421
|
+
status: invocation.status,
|
422
|
+
output: invocation.command_plugins.first.output,
|
423
|
+
}
|
424
|
+
}
|
425
|
+
)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
if command_invocations.any? {|i| COMMAND_STATUS_NOT_SUCCEEDED.include? i.status}
|
430
|
+
fail CommandNotSucceeded, "One or more command invocation(s) has not succeeded."
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
get_command_invocations(command_id)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def elb_instance_states
|
439
|
+
@elb_instance_states ||= elb_instance_states!
|
440
|
+
end
|
441
|
+
|
442
|
+
def ssm_instance_states(instance_ids = [])
|
443
|
+
return @ssm_instance_states if @ssm_instance_states
|
444
|
+
|
445
|
+
@ssm_instance_states = []
|
446
|
+
|
447
|
+
loop do
|
448
|
+
next_token = nil
|
449
|
+
|
450
|
+
response = @ssm_client.describe_instance_information(
|
451
|
+
next_token: next_token,
|
452
|
+
|
453
|
+
instance_information_filter_list: [
|
454
|
+
{
|
455
|
+
key: "PlatformTypes".freeze,
|
456
|
+
value_set: [@platform_type],
|
457
|
+
},
|
458
|
+
{
|
459
|
+
key: "InstanceIds".freeze,
|
460
|
+
value_set: instance_ids,
|
461
|
+
},
|
462
|
+
]
|
463
|
+
)
|
464
|
+
|
465
|
+
@ssm_instance_states += response.instance_information_list
|
466
|
+
next_token = response.next_token
|
467
|
+
break unless next_token
|
468
|
+
end
|
469
|
+
|
470
|
+
@ssm_instance_states
|
471
|
+
end
|
472
|
+
|
473
|
+
def load_balancer_descriptions
|
474
|
+
return @load_balancer_descriptions if @load_balancer_descriptions
|
475
|
+
|
476
|
+
@load_balancer_descriptions = []
|
477
|
+
|
478
|
+
loop do
|
479
|
+
next_marker = nil
|
480
|
+
|
481
|
+
params = {marker: next_marker}
|
482
|
+
params.update load_balancer_names: [@load_balancer_name] if @load_balancer_name
|
483
|
+
|
484
|
+
response = @elb_client.describe_load_balancers params
|
485
|
+
|
486
|
+
@load_balancer_descriptions += response.load_balancer_descriptions
|
487
|
+
next_marker = response.next_marker
|
488
|
+
break unless next_marker
|
489
|
+
end
|
490
|
+
|
491
|
+
@load_balancer_descriptions
|
492
|
+
end
|
493
|
+
|
494
|
+
def load_balancer_attributes
|
495
|
+
@load_balancer_attributes ||= @elb_client.describe_load_balancer_attributes(
|
496
|
+
load_balancer_name: @load_balancer_name,
|
497
|
+
).load_balancer_attributes
|
498
|
+
end
|
499
|
+
|
500
|
+
def elb_instance_counts
|
501
|
+
elb_instance_states.each_with_object Hash.new(0) do |i, memo|
|
502
|
+
memo[i.state] += 1
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def ssm_instance_ping_counts(instance_ids = [])
|
507
|
+
ssm_instance_states(instance_ids).each_with_object Hash.new(0) do |i, memo|
|
508
|
+
memo[i.ping_status] += 1
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
def ec2_instances
|
513
|
+
return @ec2_instances if @ec2_instances
|
514
|
+
|
515
|
+
params = {}
|
516
|
+
|
517
|
+
params.update instance_ids: @instance_ids unless @instance_ids.empty?
|
518
|
+
|
519
|
+
unless @filters.empty?
|
520
|
+
filters = @filters.map do |f|
|
521
|
+
match = f.match(/\A(.*)=(.*)\z/)
|
522
|
+
|
523
|
+
fail InvalidFilter, "Filter should be `key=value` format (got `#{f}`)." unless match
|
524
|
+
|
525
|
+
{
|
526
|
+
name: match[1],
|
527
|
+
values: [match[2]],
|
528
|
+
}
|
529
|
+
end
|
530
|
+
|
531
|
+
params.update filters: filters
|
532
|
+
end
|
533
|
+
|
534
|
+
@ec2_instances = @ec2_resource.instances(params)
|
535
|
+
|
536
|
+
unless @search.empty?
|
537
|
+
search_hash = @search.each_with_object Hash.new do |s, search_memo|
|
538
|
+
match = s.match(/\A(.*)=(.*)\z/)
|
539
|
+
|
540
|
+
fail InvalidFilter, "Search should be `key=value` format (got `#{s}`)." unless match
|
541
|
+
|
542
|
+
search_memo.update match[1] => match[2]
|
543
|
+
end
|
544
|
+
|
545
|
+
@ec2_instances = @ec2_instances.select do |instance|
|
546
|
+
search_hash.all? do |k, v|
|
547
|
+
Regexp.new(v).match query_instance_attribute(instance, k)
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
@ec2_instances
|
553
|
+
end
|
554
|
+
|
555
|
+
def query_instance_attribute(instance, attribute)
|
556
|
+
case attribute
|
557
|
+
when /\Atag:/i
|
558
|
+
tag = instance.tags.find {|t| t.key == attribute.split(":")[1..-1].join(":")}
|
559
|
+
|
560
|
+
tag ? tag.value : nil
|
561
|
+
else
|
562
|
+
attribute.split(".").map(&:intern).inject instance.data do |_acc, method|
|
563
|
+
_acc.send method
|
564
|
+
end
|
37
565
|
end
|
38
566
|
end
|
39
567
|
end
|