stemcell 0.12.2 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|