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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 42199bc80153e575d437ce9c618b662e6861af3e
4
- data.tar.gz: 53e93477347e08429b03e74b4580d4cc714c424d
2
+ SHA256:
3
+ metadata.gz: e65ab6388e741e11ab2f46af451098bd7fe70cb847f3ea8f1b9b9d229c860c2c
4
+ data.tar.gz: 568f4aecc9c7518ed156355035b39843b04d006bec7876438d707e097e071d30
5
5
  SHA512:
6
- metadata.gz: 67d1742a36a3f003a849651d9f7b899414a738ce2ea2a684efaf77bee1399fc65d824c174a28abdf981c2aa739e137d973589d29b2a15ef8e94dfcc94e9f1b3d
7
- data.tar.gz: 0e916fa0c134eb26ff01cba67c6b1e90ae921fa932940f3596a5d09b0607a52abae2b99099d3569e3cf4b67ac4e2a91226666e32af9a250af0a33f081bbd4f71
6
+ metadata.gz: 53d4581e807c8c1373189437a5e0afb42e0dc00c62b3b2b18797a31ae72d483ac066af02c455cfc3fc9a98dcc5452f433bfef618b8f8e56643f5748cff22ce12
7
+ data.tar.gz: 431fe9ee22ac0f42ff40227f1ce39e74a88af53af1fb4dc571cace78d167e346d125a2615d35799060853767801412a8c8d4ba35603d269ebc1b5232ec4bbda5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.13.0
2
+ - Migrate to AWS SDK to v3
3
+ - Drop support for ClassicLink
4
+ - Removed `necrosis` script
5
+
1
6
  # 0.12.2
2
7
  - Support for using a custom EC2 endpoint
3
8
 
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 necrosis command and pass a space separated list of instance ids:
80
+ To terminate, use the AWS CLI and pass a space separated list of instance ids:
81
81
 
82
82
  ```bash
83
- $ necrosis i-12345678 i-12345679 i-12345670
83
+ $ aws ec2 terminate-instances --instance-ids i-12345678 i-12345679 i-12345670
84
84
  ```
85
85
 
86
86
  ## Automation ##
@@ -1,4 +1,5 @@
1
- require 'aws-sdk-v1'
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 specefied as inputs
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
- :count => opts['count'],
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
- launch_options[:security_group_ids] = opts['security_group_ids']
123
+ network_interface[:groups] = opts['security_group_ids']
114
124
  end
115
125
 
116
126
  if opts['security_groups'] && !opts['security_groups'].empty?
117
- if @vpc_id
118
- # convert sg names to sg ids as VPC only accepts ids
119
- security_group_ids = get_vpc_security_group_ids(@vpc_id, opts['security_groups'])
120
- launch_options[:security_group_ids] ||= []
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
- launch_options[:availability_zone] = opts['availability_zone']
136
+ placement[:availability_zone] = opts['availability_zone']
130
137
  end
131
138
 
132
139
  if opts['subnet']
133
- launch_options[:subnet] = opts['subnet']
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
- launch_options[:dedicated_tenancy] = opts['dedicated_tenancy']
148
+ placement[:tenancy] = 'dedicated'
142
149
  end
143
150
 
144
151
  if opts['associate_public_ip_address']
145
- launch_options[:associate_public_ip_address] = opts['associate_public_ip_address']
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] = opts['iam_role']
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
- launch_options[:placement] = {
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
- wait(instances)
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(instances, opts={})
231
- return if !instances || instances.empty?
233
+ def kill(instance_ids, opts={})
234
+ return if !instance_ids || instance_ids.empty?
232
235
 
233
- errors = run_batch_operation(instances) do |instance|
234
- begin
235
- @log.warn "Terminating instance #{instance.id}"
236
- instance.terminate
237
- nil # nil == success
238
- rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
239
- opts[:ignore_not_found] ? nil : e
240
- end
241
- end
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 "genereated template is #{generated_template}"
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(instances)
270
- @log.info "Waiting up to #{MAX_RUNNING_STATE_WAIT_TIME} seconds for #{instances.count} " \
271
- "instance(s): (#{instances.inspect})"
272
-
273
- times_out_at = Time.now + MAX_RUNNING_STATE_WAIT_TIME
274
- until instances.all?{ |i| i.status == :running }
275
- wait_time_expire_or_sleep(times_out_at)
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.instances.create(opts)
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 = AWS::EC2::VPC.new(vpc_id)
320
- vpc.security_groups.each do |sg|
321
- next if sg.vpc_id != vpc_id
322
- group_map[sg.name] = sg.group_id
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
- return @ec2 if @ec2
436
-
437
- if @vpc_id
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
- AWS.config(aws_configs)
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
 
@@ -1,3 +1,3 @@
1
1
  module Stemcell
2
- VERSION = "0.12.2"
2
+ VERSION = "0.13.0"
3
3
  end
@@ -1,45 +1,56 @@
1
1
  require 'spec_helper'
2
+ require 'base64'
2
3
 
3
- class MockInstance
4
- def initialize(id)
5
- @id = id
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) { (1..4).map { |id| MockInstance.new(id) } }
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) { instance_double(AWS::EC2) }
41
- let(:client) { double(AWS::EC2::Client) }
42
- let(:response) { instance_double(AWS::Core::Response) }
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
- 'wait' => false
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
- expect_any_instance_of(AWS::EC2::VPC).to receive(:security_groups).
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
- :count => 2,
84
- :security_group_ids => ['sg-1', 'sg-2'],
85
- :availability_zone => 'us-east-1a',
86
- :user_data => 'template'
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
- expect(launcher).to receive(:set_tags).with(kind_of(Array), kind_of(Hash)).and_return(nil)
89
- # set_classic_link should not be set on vpc hosts.
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 '#set_classic_link' do
105
- let(:ec2) { instance_double(AWS::EC2) }
106
- let(:client) { double(AWS::EC2::Client) }
107
- let(:response) { instance_double(AWS::Core::Response) }
108
- before do
109
- allow(launcher).to receive(:ec2).and_return(ec2)
110
- allow(ec2).to receive(:client).and_return(client)
111
- allow(response).to receive(:error).and_return(nil)
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(:classic_link) {
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
- describe '#run_batch_operation' do
140
- it "raises no exception when no internal error occur" do
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
- it "runs full batch even when there are two error" do
146
- errors = launcher.send(:run_batch_operation,
147
- instances) do |instance, error|
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
- it "retries after an intermittent error" do
156
- count = 0
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
- it "retries up to max_attempts option per instance" do
170
- max_attempts = 6
171
- opts = {'region' => 'region', 'max_attempts' => max_attempts}
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 instanciated' do
190
- expect(AWS.config.region).to be_eql('region')
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(AWS.config.region).to be_eql('ap-northeast-1')
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' => 'region1', 'ec2_endpoint' => nil})
169
+ launcher = Stemcell::Launcher.new({'region' => 'us-east-1', 'ec2_endpoint' => nil})
202
170
  client = launcher.send(:ec2)
203
- expect(client.config.ec2_endpoint).to be_eql('ec2.region1.amazonaws.com')
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({'region' => 'region1', 'ec2_endpoint' => 'endpoint1'})
175
+ launcher = Stemcell::Launcher.new({
176
+ 'region' => 'region1',
177
+ 'ec2_endpoint' => 'https://endpoint1',
178
+ })
208
179
  client = launcher.send(:ec2)
209
- expect(client.config.ec2_endpoint).to be_eql('endpoint1')
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
- s.add_runtime_dependency 'aws-sdk-v1', '~> 1.63'
21
- s.add_runtime_dependency 'net-ssh', '~> 2.9'
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.12.2
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: 2019-06-18 00:00:00.000000000 Z
14
+ date: 2021-12-14 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
- name: aws-sdk-v1
17
+ name: aws-eventstream
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
20
  - - "~>"
21
21
  - !ruby/object:Gem::Version
22
- version: '1.63'
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: '1.63'
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
- rubyforge_project:
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