olery-aws 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|