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 +7 -0
- data/README.md +24 -0
- data/bin/olery-aws +52 -0
- data/lib/olery/aws/command/autoscaling_groups.rb +69 -0
- data/lib/olery/aws/command/instances.rb +108 -0
- data/lib/olery/aws/command/rotate.rb +563 -0
- data/lib/olery/aws/command.rb +49 -0
- data/lib/olery/aws/formatting.rb +26 -0
- data/lib/olery/aws/table.rb +88 -0
- data/lib/olery/aws/version.rb +7 -0
- data/lib/olery/aws.rb +13 -0
- data/olery-aws.gemspec +30 -0
- metadata +152 -0
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
|
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: []
|