stemcell 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +5 -0
- data/README.md +2 -2
- data/lib/stemcell/launcher.rb +74 -199
- data/lib/stemcell/option_parser.rb +0 -29
- data/lib/stemcell/version.rb +1 -1
- data/spec/lib/stemcell/launcher_spec.rb +98 -127
- data/stemcell.gemspec +5 -2
- metadata +34 -9
- data/bin/necrosis +0 -114
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e65ab6388e741e11ab2f46af451098bd7fe70cb847f3ea8f1b9b9d229c860c2c
|
4
|
+
data.tar.gz: 568f4aecc9c7518ed156355035b39843b04d006bec7876438d707e097e071d30
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53d4581e807c8c1373189437a5e0afb42e0dc00c62b3b2b18797a31ae72d483ac066af02c455cfc3fc9a98dcc5452f433bfef618b8f8e56643f5748cff22ce12
|
7
|
+
data.tar.gz: 431fe9ee22ac0f42ff40227f1ce39e74a88af53af1fb4dc571cace78d167e346d125a2615d35799060853767801412a8c8d4ba35603d269ebc1b5232ec4bbda5
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -77,10 +77,10 @@ $ stemcell $your_chef_role --tail
|
|
77
77
|
|
78
78
|
### Terminating:
|
79
79
|
|
80
|
-
To terminate, use the
|
80
|
+
To terminate, use the AWS CLI and pass a space separated list of instance ids:
|
81
81
|
|
82
82
|
```bash
|
83
|
-
$
|
83
|
+
$ aws ec2 terminate-instances --instance-ids i-12345678 i-12345679 i-12345670
|
84
84
|
```
|
85
85
|
|
86
86
|
## Automation ##
|
data/lib/stemcell/launcher.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
require 'aws-sdk-
|
1
|
+
require 'aws-sdk-ec2'
|
2
|
+
require 'base64'
|
2
3
|
require 'logger'
|
3
4
|
require 'erb'
|
4
5
|
require 'set'
|
@@ -48,7 +49,6 @@ module Stemcell
|
|
48
49
|
'security_groups',
|
49
50
|
'security_group_ids',
|
50
51
|
'tags',
|
51
|
-
'classic_link',
|
52
52
|
'iam_role',
|
53
53
|
'ebs_optimized',
|
54
54
|
'termination_protection',
|
@@ -90,7 +90,7 @@ module Stemcell
|
|
90
90
|
opts['git_key'] = try_file(opts['git_key'])
|
91
91
|
opts['chef_data_bag_secret'] = try_file(opts['chef_data_bag_secret'])
|
92
92
|
|
93
|
-
# generate tags and merge in any that were
|
93
|
+
# generate tags and merge in any that were specified as inputs
|
94
94
|
tags = {
|
95
95
|
'Name' => "#{opts['chef_role']}-#{opts['chef_environment']}",
|
96
96
|
'Group' => "#{opts['chef_role']}-#{opts['chef_environment']}",
|
@@ -106,31 +106,38 @@ module Stemcell
|
|
106
106
|
:image_id => opts['image_id'],
|
107
107
|
:instance_type => opts['instance_type'],
|
108
108
|
:key_name => opts['key_name'],
|
109
|
-
:
|
109
|
+
:min_count => opts['count'],
|
110
|
+
:max_count => opts['count'],
|
110
111
|
}
|
111
112
|
|
113
|
+
|
114
|
+
# Associate Public IP can only bet set on network_interfaces, and if present
|
115
|
+
# security groups and subnet should be set on the interface. VPC-only.
|
116
|
+
# Primary network interface
|
117
|
+
network_interface = {
|
118
|
+
device_index: 0,
|
119
|
+
}
|
120
|
+
launch_options[:network_interfaces] = [network_interface]
|
121
|
+
|
112
122
|
if opts['security_group_ids'] && !opts['security_group_ids'].empty?
|
113
|
-
|
123
|
+
network_interface[:groups] = opts['security_group_ids']
|
114
124
|
end
|
115
125
|
|
116
126
|
if opts['security_groups'] && !opts['security_groups'].empty?
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
launch_options[:security_group_ids].concat(security_group_ids)
|
122
|
-
else
|
123
|
-
launch_options[:security_groups] = opts['security_groups']
|
124
|
-
end
|
127
|
+
# convert sg names to sg ids as VPC only accepts ids
|
128
|
+
security_group_ids = get_vpc_security_group_ids(@vpc_id, opts['security_groups'])
|
129
|
+
network_interface[:groups] ||= []
|
130
|
+
network_interface[:groups].concat(security_group_ids)
|
125
131
|
end
|
126
132
|
|
133
|
+
launch_options[:placement] = placement = {}
|
127
134
|
# specify availability zone (optional)
|
128
135
|
if opts['availability_zone']
|
129
|
-
|
136
|
+
placement[:availability_zone] = opts['availability_zone']
|
130
137
|
end
|
131
138
|
|
132
139
|
if opts['subnet']
|
133
|
-
|
140
|
+
network_interface[:subnet_id] = opts['subnet']
|
134
141
|
end
|
135
142
|
|
136
143
|
if opts['private_ip_address']
|
@@ -138,23 +145,23 @@ module Stemcell
|
|
138
145
|
end
|
139
146
|
|
140
147
|
if opts['dedicated_tenancy']
|
141
|
-
|
148
|
+
placement[:tenancy] = 'dedicated'
|
142
149
|
end
|
143
150
|
|
144
151
|
if opts['associate_public_ip_address']
|
145
|
-
|
152
|
+
network_interface[:associate_public_ip_address] = opts['associate_public_ip_address']
|
146
153
|
end
|
147
154
|
|
148
155
|
# specify IAM role (optional)
|
149
156
|
if opts['iam_role']
|
150
|
-
launch_options[:iam_instance_profile] =
|
157
|
+
launch_options[:iam_instance_profile] = {
|
158
|
+
name: opts['iam_role']
|
159
|
+
}
|
151
160
|
end
|
152
161
|
|
153
162
|
# specify placement group (optional)
|
154
163
|
if opts['placement_group']
|
155
|
-
|
156
|
-
:group_name => opts['placement_group'],
|
157
|
-
}
|
164
|
+
placement[:group_name] = opts['placement_group']
|
158
165
|
end
|
159
166
|
|
160
167
|
# specify an EBS-optimized instance (optional)
|
@@ -182,35 +189,31 @@ module Stemcell
|
|
182
189
|
end
|
183
190
|
end
|
184
191
|
|
192
|
+
if opts['termination_protection']
|
193
|
+
launch_options[:disable_api_termination] = true
|
194
|
+
end
|
195
|
+
|
185
196
|
# generate user data script to bootstrap instance, include in launch
|
186
197
|
# options UNLESS we have manually set the user-data (ie. for ec2admin)
|
187
|
-
launch_options[:user_data] = opts.fetch('user_data', render_template(opts))
|
198
|
+
launch_options[:user_data] = Base64.encode64(opts.fetch('user_data', render_template(opts)))
|
199
|
+
|
200
|
+
# add tags to launch options so we don't need to make a separate CreateTags call
|
201
|
+
launch_options[:tag_specifications] = [{
|
202
|
+
resource_type: 'instance',
|
203
|
+
tags: tags.map { |k, v| { key: k, value: v } }
|
204
|
+
}]
|
188
205
|
|
189
206
|
# launch instances
|
190
207
|
instances = do_launch(launch_options)
|
191
208
|
|
192
209
|
# everything from here on out must succeed, or we kill the instances we just launched
|
193
210
|
begin
|
194
|
-
# set tags on all instances launched
|
195
|
-
set_tags(instances, tags)
|
196
|
-
@log.info "sent ec2 api tag requests successfully"
|
197
|
-
|
198
|
-
# link to classiclink
|
199
|
-
unless @vpc_id
|
200
|
-
set_classic_link(instances, opts['classic_link'])
|
201
|
-
@log.info "successfully applied classic link settings (if any)"
|
202
|
-
end
|
203
|
-
|
204
|
-
# turn on termination protection
|
205
|
-
# we do this now to make sure all other settings worked
|
206
|
-
if opts['termination_protection']
|
207
|
-
enable_termination_protection(instances)
|
208
|
-
@log.info "successfully enabled termination protection"
|
209
|
-
end
|
210
|
-
|
211
211
|
# wait for aws to report instance stats
|
212
212
|
if opts.fetch('wait', true)
|
213
|
-
|
213
|
+
instance_ids = instances.map(&:instance_id)
|
214
|
+
@log.info "Waiting up to #{MAX_RUNNING_STATE_WAIT_TIME} seconds for #{instances.count} " \
|
215
|
+
"instance(s): (#{instance_ids})"
|
216
|
+
instances = wait(instance_ids)
|
214
217
|
print_run_info(instances)
|
215
218
|
@log.info "launched instances successfully"
|
216
219
|
end
|
@@ -227,19 +230,18 @@ module Stemcell
|
|
227
230
|
return instances
|
228
231
|
end
|
229
232
|
|
230
|
-
def kill(
|
231
|
-
return if !
|
233
|
+
def kill(instance_ids, opts={})
|
234
|
+
return if !instance_ids || instance_ids.empty?
|
232
235
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
check_errors(:kill, instances.map(&:id), errors)
|
236
|
+
@log.warn "Terminating instances #{instance_ids}"
|
237
|
+
ec2.terminate_instances(instance_ids: instance_ids)
|
238
|
+
nil # nil == success
|
239
|
+
rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e
|
240
|
+
raise unless opts[:ignore_not_found]
|
241
|
+
|
242
|
+
invalid_ids = e.message.scan(/i-[a-z0-9]+/)
|
243
|
+
instance_ids -= invalid_ids
|
244
|
+
retry unless instance_ids.empty? || invalid_ids.empty? # don't retry if we couldn't find any instance ids
|
243
245
|
end
|
244
246
|
|
245
247
|
# this is made public for ec2admin usage
|
@@ -249,7 +251,7 @@ module Stemcell
|
|
249
251
|
erb_template = ERB.new(template_file)
|
250
252
|
last_bootstrap_line = LAST_BOOTSTRAP_LINE
|
251
253
|
generated_template = erb_template.result(binding)
|
252
|
-
@log.debug "
|
254
|
+
@log.debug "generated template is #{generated_template}"
|
253
255
|
return generated_template
|
254
256
|
end
|
255
257
|
|
@@ -266,16 +268,16 @@ module Stemcell
|
|
266
268
|
puts "install logs will be in /var/log/init and /var/log/init.err"
|
267
269
|
end
|
268
270
|
|
269
|
-
def wait(
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
271
|
+
def wait(instance_ids)
|
272
|
+
started_at = Time.now
|
273
|
+
result = ec2.wait_until(:instance_running, instance_ids: instance_ids) do |w|
|
274
|
+
w.max_attempts = nil
|
275
|
+
w.delay = RUNNING_STATE_WAIT_SLEEP_TIME
|
276
|
+
w.before_wait do |attempts, response|
|
277
|
+
throw :failure if Time.now - started_at > MAX_RUNNING_STATE_WAIT_TIME
|
278
|
+
end
|
276
279
|
end
|
277
|
-
|
278
|
-
@log.info "all instances in running state"
|
280
|
+
result.map { |page| page.reservations.map(&:instances) }.flatten
|
279
281
|
end
|
280
282
|
|
281
283
|
def verify_required_options(params, required_options)
|
@@ -291,35 +293,22 @@ module Stemcell
|
|
291
293
|
def do_launch(opts={})
|
292
294
|
@log.debug "about to launch instance(s) with options #{opts}"
|
293
295
|
@log.info "launching instances"
|
294
|
-
instances = ec2.
|
295
|
-
instances = [instances] unless Array === instances
|
296
|
+
instances = ec2.run_instances(opts).instances
|
296
297
|
instances.each do |instance|
|
297
298
|
@log.info "launched instance #{instance.instance_id}"
|
298
299
|
end
|
299
300
|
return instances
|
300
301
|
end
|
301
302
|
|
302
|
-
def set_tags(instances=[], tags)
|
303
|
-
@log.info "setting tags on instance(s)"
|
304
|
-
errors = run_batch_operation(instances) do |instance|
|
305
|
-
begin
|
306
|
-
instance.tags.set(tags)
|
307
|
-
nil # nil == success
|
308
|
-
rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
|
309
|
-
e
|
310
|
-
end
|
311
|
-
end
|
312
|
-
check_errors(:set_tags, instances.map(&:id), errors)
|
313
|
-
end
|
314
|
-
|
315
303
|
# Resolve security group names to their ids in the given VPC
|
316
304
|
def get_vpc_security_group_ids(vpc_id, group_names)
|
317
305
|
group_map = {}
|
318
306
|
@log.info "resolving security groups #{group_names} in #{vpc_id}"
|
319
|
-
vpc
|
320
|
-
|
321
|
-
|
322
|
-
|
307
|
+
ec2.describe_security_groups(filters: [{ name: 'vpc-id', values: [vpc_id] }]).
|
308
|
+
each do |response|
|
309
|
+
response.security_groups.each do |sg|
|
310
|
+
group_map[sg.group_name] = sg.group_id
|
311
|
+
end
|
323
312
|
end
|
324
313
|
group_ids = []
|
325
314
|
group_names.each do |sg_name|
|
@@ -329,135 +318,21 @@ module Stemcell
|
|
329
318
|
group_ids
|
330
319
|
end
|
331
320
|
|
332
|
-
def set_classic_link(left_to_process, classic_link)
|
333
|
-
return unless classic_link
|
334
|
-
return unless classic_link['vpc_id']
|
335
|
-
|
336
|
-
security_group_ids = classic_link['security_group_ids'] || []
|
337
|
-
security_group_names = classic_link['security_groups'] || []
|
338
|
-
return if security_group_ids.empty? && security_group_names.empty?
|
339
|
-
|
340
|
-
if !security_group_names.empty?
|
341
|
-
extra_group_ids = get_vpc_security_group_ids(classic_link['vpc_id'], security_group_names)
|
342
|
-
security_group_ids = security_group_ids + extra_group_ids
|
343
|
-
end
|
344
|
-
|
345
|
-
@log.info "applying classic link settings on #{left_to_process.count} instance(s)"
|
346
|
-
|
347
|
-
errors = []
|
348
|
-
processed = []
|
349
|
-
times_out_at = Time.now + MAX_RUNNING_STATE_WAIT_TIME
|
350
|
-
until left_to_process.empty?
|
351
|
-
wait_time_expire_or_sleep(times_out_at)
|
352
|
-
|
353
|
-
# we can only apply classic link when instances are in the running state
|
354
|
-
# lets apply classiclink as instances become available so we don't wait longer than necessary
|
355
|
-
recently_running = left_to_process.select{ |inst| inst.status == :running }
|
356
|
-
left_to_process = left_to_process.reject{ |inst| recently_running.include?(inst) }
|
357
|
-
|
358
|
-
processed += recently_running
|
359
|
-
errors += run_batch_operation(recently_running) do |instance|
|
360
|
-
begin
|
361
|
-
result = ec2.client.attach_classic_link_vpc({
|
362
|
-
:instance_id => instance.id,
|
363
|
-
:vpc_id => classic_link['vpc_id'],
|
364
|
-
:groups => security_group_ids,
|
365
|
-
})
|
366
|
-
result.error
|
367
|
-
rescue StandardError => e
|
368
|
-
e
|
369
|
-
end
|
370
|
-
end
|
371
|
-
end
|
372
|
-
|
373
|
-
check_errors(:set_classic_link, processed.map(&:id), errors)
|
374
|
-
end
|
375
|
-
|
376
|
-
def enable_termination_protection(instances)
|
377
|
-
@log.info "enabling termination protection on instance(s)"
|
378
|
-
errors = run_batch_operation(instances) do |instance|
|
379
|
-
begin
|
380
|
-
resp = ec2.client.modify_instance_attribute({
|
381
|
-
:instance_id => instance.id,
|
382
|
-
:disable_api_termination => {
|
383
|
-
:value => true
|
384
|
-
}
|
385
|
-
})
|
386
|
-
resp.error # returns nil (success) unless there was an error
|
387
|
-
rescue StandardError => e
|
388
|
-
e
|
389
|
-
end
|
390
|
-
end
|
391
|
-
check_errors(:enable_termination_protection, instances.map(&:id), errors)
|
392
|
-
end
|
393
|
-
|
394
321
|
# attempt to accept keys as file paths
|
395
322
|
def try_file(opt="")
|
396
323
|
File.read(File.expand_path(opt)) rescue opt
|
397
324
|
end
|
398
325
|
|
399
|
-
INITIAL_RETRY_SEC = 1
|
400
|
-
|
401
|
-
# Return a Hash of instance => error. Empty hash indicates "no error"
|
402
|
-
# for code block:
|
403
|
-
# - if block returns nil, success
|
404
|
-
# - if block returns non-nil value (e.g., exception), retry 3 times w/ backoff
|
405
|
-
# - if block raises exception, fail
|
406
|
-
def run_batch_operation(instances)
|
407
|
-
instances.map do |instance|
|
408
|
-
begin
|
409
|
-
attempt = 0
|
410
|
-
result = nil
|
411
|
-
while attempt < @max_attempts
|
412
|
-
# sleep idempotently except for the first attempt
|
413
|
-
sleep(INITIAL_RETRY_SEC * 2 ** attempt) if attempt != 0
|
414
|
-
result = yield(instance)
|
415
|
-
break if result.nil? # nil indicates success
|
416
|
-
attempt += 1
|
417
|
-
end
|
418
|
-
result # result for this instance is nil or returned exception
|
419
|
-
rescue => e
|
420
|
-
e # result for this instance is caught exception
|
421
|
-
end
|
422
|
-
end
|
423
|
-
end
|
424
|
-
|
425
|
-
def check_errors(operation, instance_ids, errors)
|
426
|
-
return if errors.all?(&:nil?)
|
427
|
-
raise IncompleteOperation.new(
|
428
|
-
operation,
|
429
|
-
instance_ids,
|
430
|
-
instance_ids.zip(errors).reject { |i, e| e.nil? }
|
431
|
-
)
|
432
|
-
end
|
433
|
-
|
434
326
|
def ec2
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
@ec2 = AWS::EC2::VPC.new(@vpc_id)
|
439
|
-
else
|
440
|
-
@ec2 = AWS::EC2.new
|
441
|
-
end
|
442
|
-
|
443
|
-
@ec2
|
444
|
-
end
|
445
|
-
|
446
|
-
def wait_time_expire_or_sleep(times_out_at)
|
447
|
-
now = Time.now
|
448
|
-
if now >= times_out_at
|
449
|
-
raise TimeoutError, "exceded timeout of #{MAX_RUNNING_STATE_WAIT_TIME} seconds"
|
450
|
-
else
|
451
|
-
sleep [RUNNING_STATE_WAIT_SLEEP_TIME, times_out_at - now].min
|
327
|
+
@ec2 ||= begin
|
328
|
+
opts = @ec2_endpoint ? { endpoint: @ec2_endpoint } : {}
|
329
|
+
Aws::EC2::Client.new(opts)
|
452
330
|
end
|
453
331
|
end
|
454
332
|
|
455
333
|
def configure_aws_creds_and_region
|
456
334
|
# configure AWS with creds/region
|
457
335
|
aws_configs = {:region => @region}
|
458
|
-
aws_configs.merge!({
|
459
|
-
:ec2_endpoint => @ec2_endpoint
|
460
|
-
}) if @ec2_endpoint
|
461
336
|
aws_configs.merge!({
|
462
337
|
:access_key_id => @aws_access_key,
|
463
338
|
:secret_access_key => @aws_secret_key
|
@@ -465,7 +340,7 @@ module Stemcell
|
|
465
340
|
aws_configs.merge!({
|
466
341
|
:session_token => @aws_session_token,
|
467
342
|
}) if @aws_session_token
|
468
|
-
|
343
|
+
Aws.config.update(aws_configs)
|
469
344
|
end
|
470
345
|
end
|
471
346
|
end
|
@@ -96,24 +96,6 @@ module Stemcell
|
|
96
96
|
:type => String,
|
97
97
|
:env => 'VPC_ID'
|
98
98
|
},
|
99
|
-
{
|
100
|
-
:name => 'classic_link_vpc_id',
|
101
|
-
:desc => 'VPC ID to which this instance will be classic-linked',
|
102
|
-
:type => String,
|
103
|
-
:env => 'CLASSIC_LINK_VPC_ID',
|
104
|
-
},
|
105
|
-
{
|
106
|
-
:name => 'classic_link_security_group_ids',
|
107
|
-
:desc => 'comma-separated list of security group IDs to link into ClassicLink; not used unless classic_link_vpc_id is set',
|
108
|
-
:type => String,
|
109
|
-
:env => 'CLASSIC_LINK_SECURITY_GROUP_IDS',
|
110
|
-
},
|
111
|
-
{
|
112
|
-
:name => 'classic_link_security_groups',
|
113
|
-
:desc => 'comma-separated list of security groups to link into ClassicLink; not used unless classic_link_vpc_id is set',
|
114
|
-
:type => String,
|
115
|
-
:env => 'CLASSIC_LINK_SECURITY_GROUPS',
|
116
|
-
},
|
117
99
|
{
|
118
100
|
:name => 'subnet',
|
119
101
|
:desc => "VPC subnet for which to launch this instance",
|
@@ -430,17 +412,6 @@ module Stemcell
|
|
430
412
|
# convert chef_cookbook_attributes from comma separated string to ruby array
|
431
413
|
options['chef_cookbook_attributes'] &&= options['chef_cookbook_attributes'].split(',')
|
432
414
|
|
433
|
-
# format the classic link options
|
434
|
-
if options['classic_link_vpc_id']
|
435
|
-
options['classic_link']['vpc_id'] = options['classic_link_vpc_id']
|
436
|
-
end
|
437
|
-
if options['classic_link_security_group_ids']
|
438
|
-
options['classic_link']['security_group_ids'] = options['classic_link_security_group_ids'].split(',')
|
439
|
-
end
|
440
|
-
if options['classic_link_security_groups']
|
441
|
-
options['classic_link']['security_groups'] = options['classic_link_security_groups'].split(',')
|
442
|
-
end
|
443
|
-
|
444
415
|
options
|
445
416
|
end
|
446
417
|
|
data/lib/stemcell/version.rb
CHANGED
@@ -1,45 +1,56 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'base64'
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
end
|
7
|
-
|
8
|
-
def id
|
9
|
-
@id
|
10
|
-
end
|
11
|
-
|
12
|
-
def status
|
13
|
-
:running
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class MockSecurityGroup
|
18
|
-
attr_reader :group_id, :name, :vpc_id
|
19
|
-
def initialize(id, name, vpc_id)
|
20
|
-
@group_id = id
|
21
|
-
@name = name
|
22
|
-
@vpc_id = vpc_id
|
4
|
+
describe Stemcell::Launcher do
|
5
|
+
before do
|
6
|
+
Aws.config[:stub_responses] = true
|
23
7
|
end
|
24
|
-
end
|
25
8
|
|
26
|
-
class MockException < StandardError
|
27
|
-
end
|
28
|
-
|
29
|
-
describe Stemcell::Launcher do
|
30
9
|
let(:launcher) {
|
31
10
|
opts = {'region' => 'region'}
|
32
11
|
launcher = Stemcell::Launcher.new(opts)
|
33
12
|
launcher
|
34
13
|
}
|
35
14
|
let(:operation) { 'op' }
|
36
|
-
let(:instances)
|
15
|
+
let(:instances) do
|
16
|
+
('1'..'4').map do |id|
|
17
|
+
Aws::EC2::Types::Instance.new(
|
18
|
+
instance_id: id,
|
19
|
+
private_ip_address: "10.10.10.#{id}",
|
20
|
+
state: Aws::EC2::Types::InstanceState.new(name: 'pending')
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
37
24
|
let(:instance_ids) { instances.map(&:id) }
|
38
25
|
|
39
26
|
describe '#launch' do
|
40
|
-
let(:ec2)
|
41
|
-
|
42
|
-
|
27
|
+
let(:ec2) do
|
28
|
+
ec2 = Aws::EC2::Client.new
|
29
|
+
ec2.stub_responses(
|
30
|
+
:describe_security_groups,
|
31
|
+
security_groups: [
|
32
|
+
{group_id: 'sg-1', group_name: 'sg_name1', vpc_id:'vpc-1'},
|
33
|
+
{group_id: 'sg-2', group_name: 'sg_name2', vpc_id:'vpc-1'},
|
34
|
+
],
|
35
|
+
)
|
36
|
+
ec2.stub_responses(
|
37
|
+
:describe_instances,
|
38
|
+
reservations: [{
|
39
|
+
instances: ('1'..'4').map do |id|
|
40
|
+
{
|
41
|
+
instance_id: id,
|
42
|
+
private_ip_address: "10.10.10.#{id}",
|
43
|
+
public_ip_address: "24.10.10.#{id}",
|
44
|
+
state: {
|
45
|
+
name: 'running'
|
46
|
+
}
|
47
|
+
}
|
48
|
+
end
|
49
|
+
}]
|
50
|
+
)
|
51
|
+
ec2
|
52
|
+
end
|
53
|
+
let(:response) { instance_double(Seahorse::Client::Response) }
|
43
54
|
let(:launcher) {
|
44
55
|
opts = {'region' => 'region', 'vpc_id' => 'vpc-1'}
|
45
56
|
launcher = Stemcell::Launcher.new(opts)
|
@@ -59,7 +70,8 @@ describe Stemcell::Launcher do
|
|
59
70
|
'availability_zone' => 'us-east-1a',
|
60
71
|
'count' => 2,
|
61
72
|
'security_groups' => ['sg_name1', 'sg_name2'],
|
62
|
-
'
|
73
|
+
'user' => 'some_user',
|
74
|
+
'wait' => true
|
63
75
|
}
|
64
76
|
}
|
65
77
|
|
@@ -67,146 +79,105 @@ describe Stemcell::Launcher do
|
|
67
79
|
allow(launcher).to receive(:try_file).and_return('secret')
|
68
80
|
allow(launcher).to receive(:render_template).and_return('template')
|
69
81
|
allow(launcher).to receive(:ec2).and_return(ec2)
|
70
|
-
allow(ec2).to receive(:client).and_return(client)
|
71
82
|
allow(response).to receive(:error).and_return(nil)
|
72
83
|
end
|
73
84
|
|
74
85
|
it 'launches all of the instances' do
|
75
86
|
expect(launcher).to receive(:get_vpc_security_group_ids).
|
76
87
|
with('vpc-1', ['sg_name1', 'sg_name2']).and_call_original
|
77
|
-
|
78
|
-
and_return([1,2].map { |i| MockSecurityGroup.new("sg-#{i}", "sg_name#{i}", 'vpc-1')})
|
88
|
+
expect(ec2).to receive(:describe_security_groups).and_call_original
|
79
89
|
expect(launcher).to receive(:do_launch).with(a_hash_including(
|
80
90
|
:image_id => 'ami-d9d6a6b0',
|
81
91
|
:instance_type => 'c1.xlarge',
|
82
92
|
:key_name => 'key',
|
83
|
-
:
|
84
|
-
:
|
85
|
-
:availability_zone
|
86
|
-
:
|
93
|
+
:min_count => 2,
|
94
|
+
:max_count => 2,
|
95
|
+
:placement => { :availability_zone => 'us-east-1a' },
|
96
|
+
:network_interfaces => [{
|
97
|
+
:device_index => 0,
|
98
|
+
:groups => ['sg-1', 'sg-2' ]
|
99
|
+
}],
|
100
|
+
:tag_specifications => [
|
101
|
+
{
|
102
|
+
:resource_type => 'instance',
|
103
|
+
:tags => [
|
104
|
+
{ :key => "Name", :value => "role-environment" },
|
105
|
+
{ :key => "Group", :value => "role-environment" },
|
106
|
+
{ :key => "created_by", :value => "some_user" },
|
107
|
+
{ :key => "stemcell", :value => Stemcell::VERSION },
|
108
|
+
]},
|
109
|
+
],
|
110
|
+
:user_data => Base64.encode64('template')
|
87
111
|
)).and_return(instances)
|
88
|
-
|
89
|
-
|
90
|
-
expect(launcher).not_to receive(:set_classic_link)
|
91
|
-
|
92
|
-
launcher.send(:launch, launch_options)
|
93
|
-
end
|
94
|
-
|
95
|
-
it 'calls set_classic_link for non vpc instances' do
|
96
|
-
launcher = Stemcell::Launcher.new({'region' => 'region', 'vpc_id' => false})
|
97
|
-
expect(launcher).to receive(:set_classic_link)
|
98
|
-
expect(launcher).to receive(:set_tags).with(kind_of(Array), kind_of(Hash)).and_return(nil)
|
99
|
-
expect(launcher).to receive(:do_launch).and_return(instances)
|
100
|
-
launcher.send(:launch, launch_options)
|
112
|
+
launched_instances = launcher.send(:launch, launch_options)
|
113
|
+
expect(launched_instances.map(&:public_ip_address)).to all(be_truthy)
|
101
114
|
end
|
102
115
|
end
|
103
116
|
|
104
|
-
describe '#
|
105
|
-
let(:ec2)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
117
|
+
describe '#kill' do
|
118
|
+
let(:ec2) do
|
119
|
+
ec2 = Aws::EC2::Client.new
|
120
|
+
ec2.stub_responses(
|
121
|
+
:terminate_instances, -> (context) {
|
122
|
+
instance_ids = context.params[:instance_ids]
|
123
|
+
if instance_ids.include? 'i-3'
|
124
|
+
Aws::EC2::Errors::InvalidInstanceIDNotFound.new(nil, "The instance ID 'i-3' do not exist")
|
125
|
+
else
|
126
|
+
{} # success
|
127
|
+
end
|
128
|
+
})
|
129
|
+
ec2
|
112
130
|
end
|
113
131
|
|
114
|
-
let(:
|
115
|
-
{
|
116
|
-
'vpc_id' => 'vpc-1',
|
117
|
-
'security_group_ids' => ['sg-1', 'sg-2'],
|
118
|
-
'security_groups' => ['sg_name']
|
119
|
-
}
|
120
|
-
}
|
121
|
-
|
122
|
-
it 'invokes classic link on all of the instances' do
|
123
|
-
expect(launcher).to receive(:get_vpc_security_group_ids).with('vpc-1', ['sg_name']).
|
124
|
-
and_call_original
|
125
|
-
expect_any_instance_of(AWS::EC2::VPC).to receive(:security_groups).
|
126
|
-
and_return([MockSecurityGroup.new('sg-3', 'sg_name', 'vpc-1')])
|
127
|
-
instances.each do |instance|
|
128
|
-
expect(client).to receive(:attach_classic_link_vpc).ordered.with(a_hash_including(
|
129
|
-
:instance_id => instance.id,
|
130
|
-
:vpc_id => classic_link['vpc_id'],
|
131
|
-
:groups => ['sg-1', 'sg-2', 'sg-3'],
|
132
|
-
)).and_return(response)
|
133
|
-
end
|
134
|
-
|
135
|
-
launcher.send(:set_classic_link, instances, classic_link)
|
136
|
-
end
|
137
|
-
end
|
132
|
+
let(:instance_ids) { ('i-1'..'i-4').to_a }
|
138
133
|
|
139
|
-
|
140
|
-
|
141
|
-
errors = launcher.send(:run_batch_operation, instances) {}
|
142
|
-
expect(errors.all?(&:nil?)).to be true
|
134
|
+
before do
|
135
|
+
allow(launcher).to receive(:ec2).and_return(ec2)
|
143
136
|
end
|
144
137
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
raise "error-#{instance.id}" if instance.id % 2 == 0
|
138
|
+
context 'when ignore_not_found is true' do
|
139
|
+
it 'terminates valid instances even if an invalid instance id is provided' do
|
140
|
+
launcher.kill(instance_ids, ignore_not_found: true)
|
149
141
|
end
|
150
|
-
expect(errors.count(&:nil?)).to be_eql(2)
|
151
|
-
expect(errors.reject(&:nil?).map { |e| e.message }).to \
|
152
|
-
be_eql([2, 4].map { |id| "error-#{id}" })
|
153
|
-
end
|
154
142
|
|
155
|
-
|
156
|
-
|
157
|
-
errors = launcher.send(:run_batch_operation,
|
158
|
-
instances) do |instance|
|
159
|
-
if instance.id == 3
|
160
|
-
count += 1
|
161
|
-
count < 3 ?
|
162
|
-
AWS::EC2::Errors::InvalidInstanceID::NotFound.new("error-#{instance.id}"):
|
163
|
-
nil
|
164
|
-
end
|
143
|
+
it 'finishes without error even if no instance ids are valid' do
|
144
|
+
launcher.kill(['i-3'], ignore_not_found: true)
|
165
145
|
end
|
166
|
-
expect(errors.all?(&:nil?)).to be true
|
167
146
|
end
|
168
147
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
launcher = Stemcell::Launcher.new(opts)
|
173
|
-
allow(launcher).to receive(:sleep).and_return(0)
|
174
|
-
tags = double("Tags")
|
175
|
-
instances = (1..2).map do |id|
|
176
|
-
inst = MockInstance.new(id)
|
177
|
-
allow(inst).to receive(:tags).and_return(tags)
|
178
|
-
inst
|
148
|
+
context 'when ignore_not_found is false' do
|
149
|
+
it 'raises an error' do
|
150
|
+
expect { launcher.kill(instance_ids) }.to raise_error(Aws::EC2::Errors::InvalidInstanceIDNotFound)
|
179
151
|
end
|
180
|
-
expect(tags).to receive(:set).with({'a' => 'b'}).exactly(12).times.
|
181
|
-
and_raise(AWS::EC2::Errors::InvalidInstanceID::NotFound.new("error"))
|
182
|
-
expect do
|
183
|
-
launcher.send(:set_tags, instances, {'a' => 'b'})
|
184
|
-
end.to raise_error(Stemcell::IncompleteOperation)
|
185
152
|
end
|
186
153
|
end
|
187
154
|
|
188
155
|
describe '#configure_aws_creds_and_region' do
|
189
|
-
it 'AWS region is configured after launcher is
|
190
|
-
expect(
|
156
|
+
it 'AWS region is configured after launcher is instantiated' do
|
157
|
+
expect(Aws.config[:region]).to be_eql('region')
|
191
158
|
end
|
192
159
|
|
193
160
|
it 'AWS region configuration changed' do
|
194
161
|
mock_launcher = Stemcell::Launcher.new('region' => 'ap-northeast-1')
|
195
|
-
expect(
|
162
|
+
expect(Aws.config[:region]).to be_eql('ap-northeast-1')
|
196
163
|
end
|
197
164
|
end
|
198
165
|
|
199
166
|
describe '#ec2' do
|
167
|
+
|
200
168
|
it 'can return a client with regional endpoint' do
|
201
|
-
launcher = Stemcell::Launcher.new({'region' => '
|
169
|
+
launcher = Stemcell::Launcher.new({'region' => 'us-east-1', 'ec2_endpoint' => nil})
|
202
170
|
client = launcher.send(:ec2)
|
203
|
-
expect(client.config.
|
171
|
+
expect(client.config[:endpoint].to_s).to be_eql('https://ec2.us-east-1.amazonaws.com')
|
204
172
|
end
|
205
173
|
|
206
174
|
it 'can return a client with custom endpoint' do
|
207
|
-
launcher = Stemcell::Launcher.new({
|
175
|
+
launcher = Stemcell::Launcher.new({
|
176
|
+
'region' => 'region1',
|
177
|
+
'ec2_endpoint' => 'https://endpoint1',
|
178
|
+
})
|
208
179
|
client = launcher.send(:ec2)
|
209
|
-
expect(client.config.
|
180
|
+
expect(client.config[:endpoint].to_s).to be_eql('https://endpoint1')
|
210
181
|
end
|
211
182
|
end
|
212
183
|
end
|
data/stemcell.gemspec
CHANGED
@@ -17,8 +17,11 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
|
20
|
-
|
21
|
-
s.add_runtime_dependency '
|
20
|
+
# pins several aws sdk transitive dependencies to maintain compatibility with Ruby < 2.3
|
21
|
+
s.add_runtime_dependency 'aws-eventstream', '~> 1.1.1'
|
22
|
+
s.add_runtime_dependency 'aws-sdk-ec2', '~> 1'
|
23
|
+
s.add_runtime_dependency 'aws-sigv4', '~> 1.2.4'
|
24
|
+
s.add_runtime_dependency 'net-ssh', '~> 2.9'
|
22
25
|
if RUBY_VERSION >= '2.0'
|
23
26
|
s.add_runtime_dependency 'chef', '>= 11.4.0'
|
24
27
|
else
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stemcell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Rhoads
|
@@ -11,22 +11,50 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2021-12-14 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
|
-
name: aws-
|
17
|
+
name: aws-eventstream
|
18
18
|
requirement: !ruby/object:Gem::Requirement
|
19
19
|
requirements:
|
20
20
|
- - "~>"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
22
|
+
version: 1.1.1
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
29
|
+
version: 1.1.1
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: aws-sdk-ec2
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - "~>"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '1'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1'
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: aws-sigv4
|
46
|
+
requirement: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - "~>"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 1.2.4
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 1.2.4
|
30
58
|
- !ruby/object:Gem::Dependency
|
31
59
|
name: net-ssh
|
32
60
|
requirement: !ruby/object:Gem::Requirement
|
@@ -157,7 +185,6 @@ description: A tool for launching and bootstrapping EC2 instances
|
|
157
185
|
email:
|
158
186
|
- igor.serebryany@airbnb.com
|
159
187
|
executables:
|
160
|
-
- necrosis
|
161
188
|
- stemcell
|
162
189
|
extensions: []
|
163
190
|
extra_rdoc_files: []
|
@@ -169,7 +196,6 @@ files:
|
|
169
196
|
- LICENSE.txt
|
170
197
|
- README.md
|
171
198
|
- Rakefile
|
172
|
-
- bin/necrosis
|
173
199
|
- bin/stemcell
|
174
200
|
- examples/stemcell.json
|
175
201
|
- examples/stemcellrc
|
@@ -241,8 +267,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
241
267
|
- !ruby/object:Gem::Version
|
242
268
|
version: '0'
|
243
269
|
requirements: []
|
244
|
-
|
245
|
-
rubygems_version: 2.5.2.3
|
270
|
+
rubygems_version: 3.0.3
|
246
271
|
signing_key:
|
247
272
|
specification_version: 4
|
248
273
|
summary: no summary
|
data/bin/necrosis
DELETED
@@ -1,114 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# -*- mode: shell -*-
|
4
|
-
|
5
|
-
require 'aws-sdk-v1'
|
6
|
-
require 'trollop'
|
7
|
-
|
8
|
-
options = Trollop::options do
|
9
|
-
version "Necrosis 0.1.0 (c) Airbnb, Inc."
|
10
|
-
banner "Necrosis: the killing script"
|
11
|
-
|
12
|
-
opt('aws_access_key',
|
13
|
-
"aws access key",
|
14
|
-
:type => String,
|
15
|
-
:default => ENV['AWS_ACCESS_KEY']
|
16
|
-
)
|
17
|
-
|
18
|
-
opt('aws_secret_key',
|
19
|
-
"aws secret key",
|
20
|
-
:type => String,
|
21
|
-
:default => ENV['AWS_SECRET_KEY']
|
22
|
-
)
|
23
|
-
|
24
|
-
opt('aws_regions',
|
25
|
-
"comma-separated list of aws regions to search",
|
26
|
-
:type => String,
|
27
|
-
:default => ENV['AWS_REGIONS'],
|
28
|
-
:short => :r
|
29
|
-
)
|
30
|
-
|
31
|
-
opt :non_interactive, 'Do not ask confirmation', :short => :f
|
32
|
-
end
|
33
|
-
|
34
|
-
NON_INTERACTIVE = options[:non_interactive]
|
35
|
-
|
36
|
-
required_parameters = %w(aws_access_key aws_secret_key)
|
37
|
-
|
38
|
-
required_parameters.each do |arg|
|
39
|
-
raise ArgumentError, "--#{arg.gsub('_','-')} needs to be specified on the commandline or set \
|
40
|
-
by the #{arg.upcase.gsub('-','_')} environment variable" if
|
41
|
-
options[arg].nil? or ! options[arg]
|
42
|
-
end
|
43
|
-
|
44
|
-
raise ArgumentError, "you did not provide any instance ids to kill" if ARGV.empty?
|
45
|
-
|
46
|
-
# a hash from instance_id => [ec2 instance objects]
|
47
|
-
instances = ARGV.inject({}) { |h, n| h[n] = []; h }
|
48
|
-
|
49
|
-
# convert comma-separated list to Array
|
50
|
-
regions = (options['aws_regions'] || '').split(',')
|
51
|
-
|
52
|
-
def authorized? instance
|
53
|
-
return true if NON_INTERACTIVE
|
54
|
-
|
55
|
-
puts 'Terminate? (y/N)'
|
56
|
-
confirm = $stdin.gets
|
57
|
-
confirmed = confirm && confirm.chomp.downcase == 'y'
|
58
|
-
|
59
|
-
if confirmed
|
60
|
-
age = Time.now - instance.launch_time
|
61
|
-
days = age / (24*3600)
|
62
|
-
if days > 2
|
63
|
-
puts "Instance is #{days} days old. REALLY terminate? (y/N)"
|
64
|
-
confirm = $stdin.gets
|
65
|
-
confirmed = confirm && confirm.chomp.downcase == 'y'
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
return confirmed
|
70
|
-
end
|
71
|
-
|
72
|
-
def delete_instance id, instance, region
|
73
|
-
if instance.api_termination_disabled?
|
74
|
-
puts "Cannot terminate instance #{id} -- termination protection enabled"
|
75
|
-
return
|
76
|
-
end
|
77
|
-
|
78
|
-
puts "Instance #{id} (#{instance.status} in #{region.name})"
|
79
|
-
puts "\tKey name: #{instance.key_name}"
|
80
|
-
puts "\tLaunched: #{instance.launch_time}"
|
81
|
-
instance.tags.to_h.each do |k, v|
|
82
|
-
puts "\t#{k} : #{v}"
|
83
|
-
end
|
84
|
-
|
85
|
-
if authorized? instance
|
86
|
-
instance.terminate
|
87
|
-
puts "Instance #{id} terminated"
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
AWS.memoize do
|
92
|
-
ec2 = AWS::EC2.new(:access_key_id => options['aws_access_key'], :secret_access_key => options['aws_secret_key'])
|
93
|
-
ec2.regions.each do |region|
|
94
|
-
next unless regions.empty? || regions.include?(region.name)
|
95
|
-
instances.each do |id, objects|
|
96
|
-
instance = region.instances[id]
|
97
|
-
objects << [instance, region] if instance.exists?
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
instances.each do |id, objects|
|
102
|
-
case objects.count
|
103
|
-
when 0
|
104
|
-
STDERR.puts "No instance #{id} found"
|
105
|
-
next
|
106
|
-
when 1
|
107
|
-
instance, region = objects.first
|
108
|
-
delete_instance id, instance, region
|
109
|
-
else
|
110
|
-
puts "Found multiple instances named #{id}"
|
111
|
-
next
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|