olery-aws 1.2.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4592a112c2bfde7e1fc50a21e6f06e6d5308de8ccb1c6c6291494df50184e732
4
+ data.tar.gz: b6e2b32c49a1859ae05298850f081e55173b39ddb4605f122a63c03dc07ec4a2
5
+ SHA512:
6
+ metadata.gz: 44679fc3c631095891f66d11369119512136e06d8d6df86616d5b68e7256cc89106bb53c28781394c0c89c569ff91cc62084d1d6922ac0db55d14e30f4eca8b3
7
+ data.tar.gz: 7c6d2d6ddd2faf261fc64879759bd5cdf1cbbd1a95dbec7aba3834e4160dfa71b001ddeacc0dd977393bb294872a1a9b60bdaa5a05d32b95313b543bddc31cdd
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Olery AWS
2
+
3
+ Command-line tools for interacting with the Olery services running on AWS. This
4
+ includes tools for rotating instances, listing running instances, retrieving
5
+ information about autoscaling groups, etc.
6
+
7
+ ## Requirements
8
+
9
+ * Ruby 2.0 or newer
10
+ * AWS credentials (e.g. in `~/.bashrc`)
11
+
12
+ ## Installation
13
+
14
+ Install all the required Gems:
15
+
16
+ bundle install
17
+
18
+ Install the application as a Gem (so you can use it everywhere):
19
+
20
+ rake install
21
+
22
+ Now you can use it, for example:
23
+
24
+ olery-aws rotate kaf_processor-olery-com
data/bin/olery-aws ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/olery/aws'
4
+
5
+ command = ARGV[0]
6
+
7
+ if command and Olery::AWS::Command.exists?(command)
8
+ cmd_class = Olery::AWS::Command.get(command)
9
+ cmd_opts = cmd_class.parse(ARGV[1..-1])
10
+
11
+ if cmd_class.instance_method(:initialize).arity != 0
12
+ instance = cmd_class.new(cmd_opts.to_hash)
13
+ else
14
+ instance = cmd_class.new
15
+ end
16
+
17
+ if instance.method(:run).arity != 0
18
+ instance.run(*cmd_opts.arguments)
19
+ else
20
+ instance.run
21
+ end
22
+ else
23
+ opts = Slop.parse do |opt|
24
+ opt.banner = 'Usage: olery-aws [COMMAND] [OPTIONS]'
25
+
26
+ opt.separator <<-EOF
27
+
28
+ Examples:
29
+ olery-aws instances --running --grep reputation
30
+ olery-aws rotate scrapers-high-as-group-1
31
+
32
+ Commands:
33
+ #{Olery::AWS::Command.command_descriptions.join("\n ")}
34
+ EOF
35
+
36
+ opt.separator 'Options:'
37
+
38
+ opt.on '-h', '--help', 'Shows this help message' do
39
+ puts opt
40
+ exit
41
+ end
42
+
43
+ opt.on '--version', 'Shows version information' do
44
+ puts "olery-aws v#{Olery::AWS::VERSION} on #{RUBY_DESCRIPTION}"
45
+ exit
46
+ end
47
+ end
48
+
49
+ puts opts
50
+ end
51
+
52
+ # vim: set ft=ruby:
@@ -0,0 +1,69 @@
1
+ module Olery
2
+ module AWS
3
+ module Command
4
+ ##
5
+ # Command for listing autoscaling groups and their details.
6
+ #
7
+ class AutoscalingGroups
8
+ NAME = 'as-groups'
9
+ DESCRIPTION = 'Lists autoscaling groups and their details'
10
+
11
+ ##
12
+ # @param [Array] argv
13
+ # @return [Slop]
14
+ #
15
+ def self.parse(argv)
16
+ Slop.parse(argv) do |opt|
17
+ opt.banner = "Usage: olery-aws #{NAME} [OPTIONS]"
18
+
19
+ opt.separator <<-EOF
20
+
21
+ Examples:
22
+ olery-aws #{NAME}
23
+ olery-aws #{NAME} --pattern scraper
24
+ EOF
25
+
26
+ opt.separator 'Options:'
27
+
28
+ opt.regexp '-p', '--pattern', 'Matches group names using a Regexp'
29
+
30
+ opt.on '-h', '--help', 'Shows this help message' do
31
+ puts opt
32
+ exit
33
+ end
34
+ end
35
+ end
36
+
37
+ ##
38
+ # @param [Regexp] pattern When given only autoscaling groups who's names
39
+ # match this pattern will be included.
40
+ #
41
+ def initialize(pattern: nil)
42
+ @pattern = pattern
43
+ end
44
+
45
+ def run
46
+ auto_scaling_groups.each do |group|
47
+ next if @pattern && group.auto_scaling_group_name !~ @pattern
48
+
49
+ puts group.auto_scaling_group_name
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # @return [Array]
56
+ def auto_scaling_groups
57
+ response = auto_scaling.describe_auto_scaling_groups
58
+
59
+ response.auto_scaling_groups
60
+ end
61
+
62
+ # @return [Aws::AutoScaling::Client]
63
+ def auto_scaling
64
+ @auto_scaling ||= Aws::AutoScaling::Client.new
65
+ end
66
+ end # AutoscalingGroups
67
+ end # Command
68
+ end # AWS
69
+ end # Olery
@@ -0,0 +1,108 @@
1
+ module Olery
2
+ module AWS
3
+ module Command
4
+ ##
5
+ # Command for listing EC2 instances and basic details.
6
+ #
7
+ class Instances
8
+ NAME = 'instances'
9
+ DESCRIPTION = 'Lists EC2 instances'
10
+
11
+ ##
12
+ # @param [Array] argv
13
+ # @return [Slop]
14
+ #
15
+ def self.parse(argv)
16
+ Slop.parse(argv) do |opt|
17
+ opt.banner = "Usage: olery-aws #{NAME} [OPTIONS]"
18
+
19
+ opt.separator <<-EOF
20
+
21
+ Examples:
22
+ olery-aws #{NAME}
23
+ olery-aws #{NAME} --running
24
+ olery-aws #{NAME} --running --name api
25
+ EOF
26
+
27
+ opt.separator 'Options:'
28
+
29
+ opt.bool '-r', '--running', 'List running instances only'
30
+
31
+ opt.regexp '-p', '--pattern', 'List instances with a matching name'
32
+
33
+ opt.on '-h', '--help', 'Shows this help message' do
34
+ puts opt
35
+ exit
36
+ end
37
+ end
38
+ end
39
+
40
+ ##
41
+ # @param [Regexp] pattern
42
+ # @param [TrueClass|FalseClass] running
43
+ #
44
+ def initialize(pattern: nil, running: false)
45
+ @pattern = pattern
46
+ @running = running
47
+ end
48
+
49
+ def run
50
+ if filters.empty?
51
+ instances = ec2.instances
52
+ else
53
+ instances = ec2.instances(:filters => filters)
54
+ end
55
+
56
+ table = Table.new
57
+
58
+ instances.each do |instance|
59
+ name = instance_name(instance)
60
+
61
+ if (@pattern and name and name =~ @pattern) or !@pattern
62
+ table << [
63
+ instance.instance_id,
64
+ instance.state.name,
65
+ instance.public_dns_name,
66
+ name.to_s
67
+ ]
68
+ end
69
+ end
70
+
71
+ table.sort_column!(3)
72
+
73
+ puts table.to_s
74
+ end
75
+
76
+ # @return [Array]
77
+ def filters
78
+ filters = []
79
+
80
+ if @running
81
+ filters << {:name => 'instance-state-name', :values => %w{running}}
82
+ end
83
+
84
+ filters
85
+ end
86
+
87
+ ##
88
+ # Returns the name of an EC2 instance.
89
+ #
90
+ # @param [Aws::EC2::Instance] instance
91
+ # @return [String|NilClass]
92
+ #
93
+ def instance_name(instance)
94
+ tag = instance.tags.find { |tag| tag.key == 'Name' }
95
+
96
+ tag ? tag.value : nil
97
+ end
98
+
99
+ private
100
+
101
+ # @return [Aws::EC2::Client]
102
+ def ec2
103
+ @ec2 ||= Aws::EC2::Resource.new
104
+ end
105
+ end # Instances
106
+ end # Command
107
+ end # AWS
108
+ end # Olery
@@ -0,0 +1,563 @@
1
+ module Olery
2
+ module AWS
3
+ module Command
4
+ ##
5
+ # Command for rotating EC2 instances in an autoscaling group.
6
+ #
7
+ # Two different rotation strategies can be used:
8
+ #
9
+ # 1. Rotating via an ELB (if present)
10
+ # 2. Simply scaling down and back up again
11
+ #
12
+ # If an ELB is present the autoscaling group capacity is doubled and the
13
+ # old instances terminated. This ensures that the autoscaling group
14
+ # remains operational during a deploy.
15
+ #
16
+ # The 2nd strategy is used when no ELB is present, this is usually only
17
+ # the case for autoscaling groups used for background processing. In these
18
+ # cases downtime doesn't really matter since users won't notice it.
19
+ #
20
+ # ## Synchronization
21
+ #
22
+ # To ensure deployments operate smoothly only 1 entity can deploy to the
23
+ # same autoscaling group at a given time. Synchronization is done by
24
+ # emulating a semaphore using autoscaling group tags. This setup should
25
+ # prevent parallel deploys in 99,99% of the cases (fingers crossed).
26
+ #
27
+ class Rotate
28
+ NAME = 'rotate'
29
+ DESCRIPTION = 'Rotates EC2 instances in an autoscaling group'
30
+
31
+ # The name of the tag used for locking deployments
32
+ LOCK_TAG = 'olery-deployment-lock'
33
+
34
+ ##
35
+ # @param [Array] argv
36
+ # @return [Slop]
37
+ #
38
+ def self.parse(argv)
39
+ Slop.parse(argv) do |opt|
40
+ opt.banner = "Usage: olery-aws #{NAME} [OPTIONS]"
41
+
42
+ opt.separator <<-EOF
43
+
44
+ Examples:
45
+ olery-aws #{NAME} scrapers-high-as-group-1
46
+ EOF
47
+
48
+ opt.separator 'Options:'
49
+
50
+ opt.on '-h', '--help', 'Shows this help message' do
51
+ puts opt
52
+ exit
53
+ end
54
+ end
55
+ end
56
+
57
+ def initialize
58
+ @logger = Logger.new(STDERR)
59
+
60
+ @logger.formatter = proc do |level, time, prog, msg|
61
+ "#{time.strftime('%H:%M:%S')} #{level}: #{msg}\n"
62
+ end
63
+ end
64
+
65
+ ##
66
+ # @param [String] group_name The name of the autoscaling group.
67
+ #
68
+ def run(group_name)
69
+ info 'Acquiring deployment lock'
70
+
71
+ synchronize_group(group_name) do
72
+ info 'Lock acquired'
73
+
74
+ group = get_group(group_name)
75
+
76
+ if group.instances.length > 0
77
+ if has_elbs?(group)
78
+ rotate_elb(group)
79
+ else
80
+ rotate(group)
81
+ end
82
+ else
83
+ info 'No running instances in autoscaling group'
84
+ end
85
+ end
86
+ end
87
+
88
+ ##
89
+ # Rotates all instances attached to an autoscaling group with a number
90
+ # of ELBs.
91
+ #
92
+ # The process for this is as following:
93
+ #
94
+ # 1. Double the capacity of the autoscaling group.
95
+ # 2. Wait for all instances to be registered with all ELBs.
96
+ # 3. Wait for all instances to be in service.
97
+ # 4. Detach the old instances and wait for them to be de-registered from
98
+ # the autoscaling group.
99
+ # 5. Terminate the old instances and wait for them to be terminated.
100
+ # 6. Reset the autoscaling group's settings to their original values.
101
+ #
102
+ # @param [Struct] group
103
+ #
104
+ def rotate_elb(group)
105
+ info 'Disabling terminating of instances'
106
+
107
+ suspend_processes(group.auto_scaling_group_name, 'Terminate')
108
+
109
+ new_size = group.max_size * 2
110
+ new_desired = group.desired_capacity * 2
111
+
112
+ info "Doubling autoscaling capacity to #{new_size}"
113
+
114
+ update_group(
115
+ group.auto_scaling_group_name,
116
+ max_size: new_size,
117
+ desired_capacity: new_desired,
118
+ )
119
+
120
+ info 'Waiting for ELBs to register instances'
121
+
122
+ wait_for_elbs group.load_balancer_names, new_desired
123
+
124
+ info 'Waiting for all instances to be in service'
125
+
126
+ group.load_balancer_names.each do |elb_name|
127
+ wait_until_in_service(elb_name)
128
+ end
129
+
130
+ instance_ids = group.instances.map(&:instance_id)
131
+
132
+ info 'Disabling launching of new instances'
133
+
134
+ suspend_processes(group.auto_scaling_group_name, 'Launch')
135
+
136
+ info 'Detaching old instances'
137
+
138
+ detach_instances(group.auto_scaling_group_name, instance_ids)
139
+
140
+ info 'Waiting for instances to be removed from autoscaling group'
141
+
142
+ wait_until_removed_from_group(
143
+ group.auto_scaling_group_name,
144
+ instance_ids
145
+ )
146
+
147
+ info 'Terminating old instances'
148
+
149
+ terminate_instances(instance_ids)
150
+
151
+ wait_until_terminated(instance_ids)
152
+
153
+ # In the event of any errors/timeouts/etc we want to be absolutely sure
154
+ # the autoscaling group is back in its initial state.
155
+ ensure
156
+ info 'Restoring autoscaling group'
157
+
158
+ update_group(
159
+ group.auto_scaling_group_name,
160
+ :max_size => group.max_size
161
+ )
162
+
163
+ info 'Re-enabling all autoscaling processes'
164
+
165
+ resume_processes(group.auto_scaling_group_name)
166
+ end
167
+
168
+ ##
169
+ # Terminates all instances in an autoscaling group and adds the same
170
+ # amount of new instances.
171
+ #
172
+ # The process for this is as following:
173
+ #
174
+ # 1. Scale down to 0 instances and wait for all instances to be
175
+ # terminated.
176
+ # 2. Scale back up to the original capacity and wait for all instances
177
+ # to be in service.
178
+ # 3. Reset the autoscaling group's settings to their original values.
179
+ #
180
+ # @param [Struct] group
181
+ #
182
+ def rotate(group)
183
+ info 'Disabling launching of new instances'
184
+
185
+ suspend_processes(group.auto_scaling_group_name, 'Launch')
186
+
187
+ info 'Setting minimum amount of instances to 0'
188
+
189
+ instance_ids = group.instances.map(&:instance_id)
190
+
191
+ info 'Scaling down to 0 instances'
192
+
193
+ update_group(
194
+ group.auto_scaling_group_name,
195
+ :min_size => 0,
196
+ :desired_capacity => 0
197
+ )
198
+
199
+ wait_until_terminated(instance_ids)
200
+
201
+ info 'Waiting for instances to be removed from autoscaling group'
202
+
203
+ wait_until_removed_from_group(
204
+ group.auto_scaling_group_name,
205
+ instance_ids
206
+ )
207
+
208
+ resume_processes(group.auto_scaling_group_name)
209
+
210
+ info 'Disabling terminating of instances'
211
+
212
+ suspend_processes(group.auto_scaling_group_name, 'Terminate')
213
+
214
+ info 'Scaling back to original capacity'
215
+
216
+ update_group(
217
+ group.auto_scaling_group_name,
218
+ :desired_capacity => group.desired_capacity
219
+ )
220
+
221
+ # Spot instances may take a long time to start up due to pricing. As
222
+ # such, in case an autoscaling group runs spot instances we _won't_
223
+ # wait for the instances to start up.
224
+ unless spot_instances?(group.auto_scaling_group_name)
225
+ wait_until_group_capacity(
226
+ group.auto_scaling_group_name,
227
+ group.desired_capacity
228
+ )
229
+
230
+ info 'Waiting for instances to start'
231
+
232
+ new_group = get_group(group.auto_scaling_group_name)
233
+ new_instance_ids = new_group.instances.map(&:instance_id)
234
+
235
+ wait_until_running(new_instance_ids) unless new_instance_ids.empty?
236
+ end
237
+
238
+ # In the event of any errors/timeouts/etc we want to be absolutely sure
239
+ # the autoscaling group is back in its initial state.
240
+ ensure
241
+ info 'Restoring autoscaling group'
242
+
243
+ update_group(
244
+ group.auto_scaling_group_name,
245
+ :min_size => group.min_size
246
+ )
247
+
248
+ info 'Re-enabling all autoscaling processes'
249
+
250
+ resume_processes(group.auto_scaling_group_name)
251
+ end
252
+
253
+ ##
254
+ # @param [String] group_name
255
+ # @param [Array] processes
256
+ #
257
+ def suspend_processes(group_name, *processes)
258
+ auto_scaling.suspend_processes(
259
+ :auto_scaling_group_name => group_name,
260
+ :scaling_processes => processes
261
+ )
262
+ end
263
+
264
+ ##
265
+ # @param [String] group_name
266
+ #
267
+ def resume_processes(group_name)
268
+ auto_scaling.resume_processes(:auto_scaling_group_name => group_name)
269
+ end
270
+
271
+ ##
272
+ # @param [String] group_name
273
+ # @return [Mixed]
274
+ #
275
+ def get_group(group_name)
276
+ group = auto_scaling.describe_auto_scaling_groups(
277
+ :auto_scaling_group_names => [group_name]
278
+ )
279
+
280
+ group.auto_scaling_groups[0]
281
+ end
282
+
283
+ ##
284
+ # @param [Struct] group
285
+ # @return [TrueClass|FalseClass]
286
+ #
287
+ def has_elbs?(group)
288
+ group.load_balancer_names.length > 0
289
+ end
290
+
291
+ ##
292
+ # @param [String] name
293
+ # @param [Hash] options
294
+ #
295
+ def update_group(name, options)
296
+ auto_scaling.update_auto_scaling_group(
297
+ options.merge(:auto_scaling_group_name => name)
298
+ )
299
+ end
300
+
301
+ ##
302
+ # @param [Array] elb_names
303
+ # @param [Struct] group
304
+ #
305
+ def wait_for_elbs(elb_names, instances)
306
+ loop_with_limit do
307
+ response = load_balancing.describe_load_balancers(
308
+ :load_balancer_names => elb_names
309
+ )
310
+
311
+ ok_elbs = response.load_balancer_descriptions.count do |elb|
312
+ elb.instances.length == instances
313
+ end
314
+
315
+ break if ok_elbs == elb_names.length
316
+
317
+ sleep(5)
318
+ end
319
+ end
320
+
321
+ ##
322
+ # @param [String] elb_name
323
+ #
324
+ def wait_until_in_service(elb_name)
325
+ load_balancing.wait_until(
326
+ :instance_in_service,
327
+ :load_balancer_name => elb_name
328
+ ) do |waiter|
329
+ waiter.delay = 30
330
+ end
331
+ end
332
+
333
+ ##
334
+ # @param [String] group_name
335
+ # @param [Fixnum] capacity
336
+ #
337
+ def wait_until_group_capacity(group_name, capacity)
338
+ loop_with_limit do
339
+ group = get_group(group_name)
340
+ current = group.instances.length
341
+
342
+ break if current == capacity
343
+
344
+ sleep(5)
345
+ end
346
+ end
347
+
348
+ ##
349
+ # @param [Array] ids
350
+ #
351
+ def terminate_instances(ids)
352
+ ec2.terminate_instances(:instance_ids => ids)
353
+ end
354
+
355
+ ##
356
+ # @param [String] group_name
357
+ # @param [Array] ids
358
+ #
359
+ def detach_instances(group_name, ids)
360
+ auto_scaling.detach_instances(
361
+ :auto_scaling_group_name => group_name,
362
+ :instance_ids => ids,
363
+ :should_decrement_desired_capacity => true
364
+ )
365
+ end
366
+
367
+ ##
368
+ # Waits until the given instances are terminated.
369
+ #
370
+ # @param [Array] ids
371
+ #
372
+ def wait_until_terminated(ids)
373
+ ec2.wait_until(:instance_terminated, :instance_ids => ids) do |waiter|
374
+ waiter.delay = 30
375
+ waiter.max_attempts = 60
376
+ end
377
+ end
378
+
379
+ ##
380
+ # Waits until the given instances are running.
381
+ #
382
+ # @param [Array] ids
383
+ #
384
+ def wait_until_running(ids)
385
+ ec2.wait_until(:instance_running, :instance_ids => ids) do |waiter|
386
+ waiter.delay = 30
387
+ waiter.max_attempts = 60
388
+ end
389
+ end
390
+
391
+ ##
392
+ # Waits until the given instances are removed from the autoscaling
393
+ # group.
394
+ #
395
+ # @param [String] group_name
396
+ # @param [Array] instance_ids
397
+ #
398
+ def wait_until_removed_from_group(group_name, instance_ids)
399
+ loop_with_limit do
400
+ group = get_group(group_name)
401
+ current = group.instances.map(&:instance_id)
402
+
403
+ break if (current & instance_ids).empty?
404
+
405
+ sleep(5)
406
+ end
407
+ end
408
+
409
+ ##
410
+ # Yields the given block at most `max` times.
411
+ #
412
+ def loop_with_limit(max = 60)
413
+ max.times { yield }
414
+ end
415
+
416
+ ##
417
+ # Locks an autoscaling group to prevent concurrent deploys from messing
418
+ # up the capacity. Once locked the block is yielded, once the block
419
+ # returns the lock is released.
420
+ #
421
+ # @param [String] group_name
422
+ #
423
+ def synchronize_group(group_name)
424
+ loop do
425
+ # Somebody else has the lock
426
+ sleep(5) while group_locked_by(group_name)
427
+
428
+ # Make sure we only acquire the lock if nobody else did so in the
429
+ # mean time.
430
+ unless group_locked_by(group_name)
431
+ add_group_tag(group_name, LOCK_TAG, uuid)
432
+ end
433
+
434
+ held_by = group_locked_by(group_name)
435
+
436
+ # Did we _actually_ acquire the lock, or did somebody else overwrite
437
+ # ours?
438
+ if held_by == uuid
439
+ yield
440
+ break
441
+ end
442
+ end
443
+
444
+ # Lets try to not leave behind a locked group that needs manual
445
+ # unlocking.
446
+ ensure
447
+ unlock_group(group_name)
448
+ end
449
+
450
+ ##
451
+ # Unlocks an autoscaling group.
452
+ #
453
+ # @param [String] group_name
454
+ #
455
+ def unlock_group(group_name)
456
+ if group_locked_by(group_name) == uuid
457
+ remove_group_tag(group_name, LOCK_TAG)
458
+ end
459
+ end
460
+
461
+ ##
462
+ # @param [String] group_name
463
+ # @return [String|NilClass]
464
+ #
465
+ def group_locked_by(group_name)
466
+ tag = get_group(group_name).tags.find { |tag| tag.key == LOCK_TAG }
467
+
468
+ tag ? tag.value : nil
469
+ end
470
+
471
+ ##
472
+ # @param [String] group_name
473
+ # @param [String] tag_name
474
+ # @param [String] tag_value
475
+ #
476
+ def add_group_tag(group_name, tag_name, tag_value)
477
+ auto_scaling.create_or_update_tags(
478
+ :tags => [
479
+ {
480
+ :resource_id => group_name,
481
+ :resource_type => 'auto-scaling-group',
482
+ :key => tag_name,
483
+ :value => tag_value,
484
+ :propagate_at_launch => false
485
+ }
486
+ ]
487
+ )
488
+ end
489
+
490
+ ##
491
+ # @param [String] group_name
492
+ # @param [String] tag_name
493
+ #
494
+ def remove_group_tag(group_name, tag_name)
495
+ auto_scaling.delete_tags(
496
+ :tags => [
497
+ {
498
+ :resource_id => group_name,
499
+ :resource_type => 'auto-scaling-group',
500
+ :key => tag_name
501
+ }
502
+ ]
503
+ )
504
+ end
505
+
506
+ ##
507
+ # Checks if an autoscaling group has any spot instances.
508
+ #
509
+ # @param [String] group_name
510
+ # @return [TrueClass|FalseClass]
511
+ #
512
+ def spot_instances?(group_name)
513
+ group = get_group(group_name)
514
+ group_launch_spot? group
515
+ end
516
+
517
+ ##
518
+ # @param [String] group
519
+ # @return [Boolean]
520
+ #
521
+ def group_launch_spot? group
522
+ if config_name = group.launch_configuration_name
523
+ response = auto_scaling.describe_launch_configurations launch_configuration_names: [config_name]
524
+ !!response.launch_configurations[0].spot_price
525
+ else
526
+ versions = ec2.describe_launch_template_versions launch_template_id: group.launch_template.launch_template_id
527
+ final = versions.launch_template_versions.first
528
+ final.launch_template_data.instance_market_options.market_type == 'spot'
529
+ end
530
+ end
531
+
532
+ private
533
+
534
+ ##
535
+ # @param [String] message
536
+ #
537
+ def info(message)
538
+ @logger.info(message)
539
+ end
540
+
541
+ # @return [Aws::AutoScaling::Client]
542
+ def auto_scaling
543
+ @auto_scaling ||= Aws::AutoScaling::Client.new
544
+ end
545
+
546
+ # @return [Aws::ElasticLoadBalancing::Client]
547
+ def load_balancing
548
+ @load_balancing ||= Aws::ElasticLoadBalancing::Client.new
549
+ end
550
+
551
+ # @return [Aws::EC2::Client]
552
+ def ec2
553
+ @ec2 ||= Aws::EC2::Client.new
554
+ end
555
+
556
+ # @return [String]
557
+ def uuid
558
+ @uuid ||= UUID.generate
559
+ end
560
+ end # Rotate
561
+ end # Command
562
+ end # AWS
563
+ end # Olery
@@ -0,0 +1,49 @@
1
+ module Olery
2
+ module AWS
3
+ module Command
4
+ ##
5
+ # Returns all the available command classes.
6
+ #
7
+ # @return [Array<Class>]
8
+ #
9
+ def self.commands
10
+ Command.constants.map { |name| Command.const_get(name) }
11
+ end
12
+
13
+ ##
14
+ # Returns an Array with command names and their descriptions. The command
15
+ # names contain padding so they can be aligned.
16
+ #
17
+ # @return [Array]
18
+ #
19
+ def self.command_descriptions
20
+ lines = commands.map { |const| [const::NAME, const::DESCRIPTION] }
21
+ names = Formatting.pad_strings(lines.map { |line| line[0] })
22
+
23
+ names.sort.map.with_index do |name, index|
24
+ "#{name} #{lines[index][1]}"
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Returns `true` if the given command exists.
30
+ #
31
+ # @param [String] name
32
+ # @return [TrueClass|FalseClass]
33
+ #
34
+ def self.exists?(name)
35
+ commands.map { |const| const::NAME }.include?(name)
36
+ end
37
+
38
+ ##
39
+ # Returns the command for the given name.
40
+ #
41
+ # @param [String] name
42
+ # @return [Class]
43
+ #
44
+ def self.get(name)
45
+ commands.find { |const| const::NAME == name }
46
+ end
47
+ end # Command
48
+ end # AWS
49
+ end # Olery
@@ -0,0 +1,26 @@
1
+ module Olery
2
+ module AWS
3
+ ##
4
+ # Helper module for formatting output, aligning text, etc.
5
+ #
6
+ module Formatting
7
+ module_function
8
+
9
+ ##
10
+ # Returns an Array with all strings padded with whitespace.
11
+ #
12
+ # @example
13
+ # pad_strings(%w{foo foobar}) # => ["foo ", "foobar"]
14
+ #
15
+ # @param [Array] values
16
+ # @return [Array]
17
+ #
18
+ def pad_strings(values)
19
+ longest = values.sort { |left, right| right.length <=> left.length }
20
+ padding = longest[0] ? longest[0].length : 0
21
+
22
+ values.map { |value| value.ljust(padding, ' ') }
23
+ end
24
+ end # Formatting
25
+ end # AWS
26
+ end # Olery
@@ -0,0 +1,88 @@
1
+ module Olery
2
+ module AWS
3
+ ##
4
+ # Class for generating an ASCII table with the columns aligned. The output
5
+ # format is grep/awk/etc friendly.
6
+ #
7
+ class Table
8
+ # @return [Array]
9
+ attr_reader :rows
10
+
11
+ def initialize
12
+ @rows = []
13
+
14
+ @column_count = 0
15
+ end
16
+
17
+ ##
18
+ # Adds a new row to the table.
19
+ #
20
+ # @param [Array] row
21
+ #
22
+ def push(row)
23
+ @rows << row
24
+
25
+ if row.length > @column_count
26
+ @column_count = row.length
27
+ end
28
+ end
29
+
30
+ alias_method :<<, :push
31
+
32
+ ##
33
+ # Sorts the table rows.
34
+ #
35
+ # @yieldparam [Array]
36
+ #
37
+ def sort!(&block)
38
+ @rows.sort_by!(&block)
39
+ end
40
+
41
+ ##
42
+ # Sorts all rows by the given column index.
43
+ #
44
+ # @param [Fixnum] column
45
+ #
46
+ def sort_column!(column)
47
+ sort! { |row| row[column] }
48
+ end
49
+
50
+ # @return [String]
51
+ def to_s
52
+ lengths = column_lengths
53
+ lines = []
54
+
55
+ @rows.each do |row|
56
+ line = row.map.with_index do |column, index|
57
+ column.ljust(lengths[index], ' ')
58
+ end
59
+
60
+ lines << line.join(' ')
61
+ end
62
+
63
+ lines.join("\n")
64
+ end
65
+
66
+ private
67
+
68
+ ##
69
+ # Returns an array containing the longest length for every column.
70
+ #
71
+ # @return [Array]
72
+ #
73
+ def column_lengths
74
+ lengths = Array.new(@column_count, 0)
75
+
76
+ @rows.each do |row|
77
+ row.each_with_index do |column, index|
78
+ length = column.length
79
+
80
+ lengths[index] = length if length > lengths[index]
81
+ end
82
+ end
83
+
84
+ lengths
85
+ end
86
+ end # Table
87
+ end # AWS
88
+ end # Olery
@@ -0,0 +1,7 @@
1
+ module Olery
2
+ module AWS
3
+
4
+ VERSION = '1.2.1'
5
+
6
+ end
7
+ end
data/lib/olery/aws.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'aws-sdk'
2
+ require 'slop'
3
+ require 'uuid'
4
+
5
+ require 'logger'
6
+
7
+ require_relative 'aws/version'
8
+ require_relative 'aws/command'
9
+ require_relative 'aws/formatting'
10
+ require_relative 'aws/table'
11
+ require_relative 'aws/command/autoscaling_groups'
12
+ require_relative 'aws/command/instances'
13
+ require_relative 'aws/command/rotate'
data/olery-aws.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ require File.expand_path('../lib/olery/aws/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'olery-aws'
5
+ gem.version = Olery::AWS::VERSION
6
+ gem.authors = ['Olery B.V.']
7
+ gem.email = 'development@olery.com'
8
+ gem.summary = 'Command-line tools for interacting with the Olery services running on AWS.'
9
+ gem.homepage = 'https://github.com/olery/olery-aws/'
10
+ gem.description = gem.summary
11
+ gem.executables = %w{olery-aws}
12
+
13
+ gem.files = Dir.glob([
14
+ 'bin/**/*',
15
+ 'lib/**/*.rb',
16
+ 'README.md',
17
+ '*.gemspec'
18
+ ]).select { |file| File.file?(file) }
19
+
20
+ gem.required_ruby_version = '>= 2.0'
21
+
22
+ gem.add_dependency 'aws-sdk', '~> 2.0'
23
+ gem.add_dependency 'slop', '~> 4.0'
24
+ gem.add_dependency 'uuid'
25
+
26
+ gem.add_development_dependency 'pry'
27
+ gem.add_development_dependency 'rake'
28
+ gem.add_development_dependency 'bundler'
29
+ gem.add_development_dependency 'rspec', '~> 3.0'
30
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: olery-aws
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Olery B.V.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: uuid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ description: Command-line tools for interacting with the Olery services running on
112
+ AWS.
113
+ email: development@olery.com
114
+ executables:
115
+ - olery-aws
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - README.md
120
+ - bin/olery-aws
121
+ - lib/olery/aws.rb
122
+ - lib/olery/aws/command.rb
123
+ - lib/olery/aws/command/autoscaling_groups.rb
124
+ - lib/olery/aws/command/instances.rb
125
+ - lib/olery/aws/command/rotate.rb
126
+ - lib/olery/aws/formatting.rb
127
+ - lib/olery/aws/table.rb
128
+ - lib/olery/aws/version.rb
129
+ - olery-aws.gemspec
130
+ homepage: https://github.com/olery/olery-aws/
131
+ licenses: []
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '2.0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.0.9
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: Command-line tools for interacting with the Olery services running on AWS.
152
+ test_files: []