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 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