chef-provisioning-aws 1.1.1 → 1.2.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.
@@ -121,6 +121,8 @@ class AWSProvider < Chef::Provider::LWRPBase
121
121
  aws_object = create_aws_object
122
122
  end
123
123
 
124
+ converge_tags(aws_object)
125
+
124
126
  #
125
127
  # Associate the managed entry with the AWS object
126
128
  #
@@ -221,6 +223,45 @@ class AWSProvider < Chef::Provider::LWRPBase
221
223
  raise NotImplementedError, :destroy_aws_object
222
224
  end
223
225
 
226
+ # Update AWS resource tags
227
+ #
228
+ # AWS resources which include the TaggedItem Module
229
+ # will have an 'aws_tags' attribute available.
230
+ # The 'aws_tags' Hash will apply all the tags within
231
+ # the hash, and remove existing tags not included within
232
+ # the hash. The 'Name' tag will not removed. The 'Name'
233
+ # tag can still be updated in the hash.
234
+ #
235
+ # @param aws_object Aws SDK Object to update tags
236
+ #
237
+ def converge_tags(aws_object)
238
+ desired_tags = new_resource.aws_tags
239
+ # If aws_tags were not provided we exit
240
+ if desired_tags.nil?
241
+ Chef::Log.debug "aws_tags not provided, nothing to converge"
242
+ return
243
+ end
244
+ current_tags = aws_object.tags.to_h
245
+ # AWS always returns tags as strings, and we don't want to overwrite a
246
+ # tag-as-string with the same tag-as-symbol
247
+ desired_tags = Hash[desired_tags.map {|k, v| [k.to_s, v.to_s] }]
248
+ tags_to_update = desired_tags.reject {|k,v| current_tags[k] == v}
249
+ tags_to_delete = current_tags.keys - desired_tags.keys
250
+ # We don't want to delete `Name`, just all other tags
251
+ tags_to_delete.delete('Name')
252
+
253
+ unless tags_to_update.empty?
254
+ converge_by "applying tags #{tags_to_update}" do
255
+ aws_object.tags.set(tags_to_update)
256
+ end
257
+ end
258
+ unless tags_to_delete.empty?
259
+ converge_by "deleting tags #{tags_to_delete.inspect}" do
260
+ aws_object.tags.delete(*tags_to_delete)
261
+ end
262
+ end
263
+ end
264
+
224
265
  # Wait until aws_object obtains one of expected_status
225
266
  #
226
267
  # @param aws_object Aws SDK Object to check status on
@@ -3,6 +3,13 @@ require 'chef/provisioning/aws_driver/resources'
3
3
 
4
4
  # Common AWS resource - contains metadata that all AWS resources will need
5
5
  class Chef::Provisioning::AWSDriver::AWSResourceWithEntry < Chef::Provisioning::AWSDriver::AWSResource
6
+
7
+ # This should be a hash of tags to apply to the AWS object
8
+ #
9
+ # @param aws_tags [Hash] Should be a hash of keys & values to add. Keys and values
10
+ # can be provided as symbols or strings, but will be stored in AWS as strings.
11
+ attribute :aws_tags, kind_of: Hash
12
+
6
13
  #
7
14
  # Dissociate the ID of this object from Chef.
8
15
  #
@@ -20,6 +20,8 @@ require 'chef/provisioning/aws_driver/credentials'
20
20
 
21
21
  require 'yaml'
22
22
  require 'aws-sdk-v1'
23
+ require 'retryable'
24
+
23
25
 
24
26
  # loads the entire aws-sdk
25
27
  AWS.eager_autoload!
@@ -84,10 +86,14 @@ module AWSDriver
84
86
  updates << " attach subnets #{lb_options[:subnets].join(', ')}" if lb_options[:subnets]
85
87
  updates << " with listeners #{lb_options[:listeners]}" if lb_options[:listeners]
86
88
  updates << " with security groups #{lb_options[:security_groups]}" if lb_options[:security_groups]
89
+ updates << " with tags #{lb_options[:aws_tags]}" if lb_options[:aws_tags]
87
90
 
88
91
 
92
+ lb_aws_tags = lb_options[:aws_tags]
93
+ lb_options.delete(:aws_tags)
89
94
  action_handler.perform_action updates do
90
95
  actual_elb = elb.load_balancers.create(lb_spec.name, lb_options)
96
+ lb_options[:aws_tags] = lb_aws_tags
91
97
 
92
98
  lb_spec.reference = {
93
99
  'driver_version' => Chef::Provisioning::AWSDriver::VERSION,
@@ -269,6 +275,35 @@ module AWSDriver
269
275
  end
270
276
  end
271
277
 
278
+ # GRRRR curse you AWS and your crappy tagging support for ELBs
279
+ read_tags_block = lambda {|aws_object|
280
+ resp = elb.client.describe_tags load_balancer_names: [aws_object.name]
281
+ tags = {}
282
+ resp.data[:tag_descriptions] && resp.data[:tag_descriptions].each do |td|
283
+ td[:tags].each do |t|
284
+ tags[t[:key]] = t[:value]
285
+ end
286
+ end
287
+ tags
288
+ }
289
+
290
+ set_tags_block = lambda {|aws_object, desired_tags|
291
+ aws_form_tags = []
292
+ desired_tags.each do |k, v|
293
+ aws_form_tags << {key: k, value: v}
294
+ end
295
+ elb.client.add_tags load_balancer_names: [aws_object.name], tags: aws_form_tags
296
+ }
297
+
298
+ delete_tags_block=lambda {|aws_object, tags_to_delete|
299
+ aws_form_tags = []
300
+ tags_to_delete.each do |k, v|
301
+ aws_form_tags << {key: k}
302
+ end
303
+ elb.client.remove_tags load_balancer_names: [aws_object.name], tags: aws_form_tags
304
+ }
305
+ converge_tags(actual_elb, lb_options[:aws_tags], action_handler, read_tags_block, set_tags_block, delete_tags_block)
306
+
272
307
  # Update instance list, but only if there are machines specified
273
308
  if machine_specs
274
309
  actual_instance_ids = actual_elb.instances.map { |i| i.instance_id }
@@ -326,22 +361,24 @@ module AWSDriver
326
361
  # Image methods
327
362
  def allocate_image(action_handler, image_spec, image_options, machine_spec, machine_options)
328
363
  actual_image = image_for(image_spec)
364
+ aws_tags = image_options.delete(:aws_tags) || {}
329
365
  if actual_image.nil? || !actual_image.exists? || actual_image.state == :failed
330
366
  action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
331
367
  image_options[:name] ||= image_spec.name
332
368
  image_options[:instance_id] ||= machine_spec.reference['instance_id']
333
369
  image_options[:description] ||= "Image #{image_spec.name} created from machine #{machine_spec.name}"
334
370
  Chef::Log.debug "AWS Image options: #{image_options.inspect}"
335
- image = ec2.images.create(image_options.to_hash)
336
- image.add_tag('From-Instance', :value => image_options[:instance_id]) if image_options[:instance_id]
371
+ actual_image = ec2.images.create(image_options.to_hash)
337
372
  image_spec.reference = {
338
373
  'driver_version' => Chef::Provisioning::AWSDriver::VERSION,
339
- 'image_id' => image.id,
374
+ 'image_id' => actual_image.id,
340
375
  'allocated_at' => Time.now.to_i
341
376
  }
342
377
  image_spec.driver_url = driver_url
343
378
  end
344
379
  end
380
+ aws_tags['From-Instance'] = image_options[:instance_id] if image_options[:instance_id]
381
+ converge_tags(actual_image, aws_tags, action_handler)
345
382
  end
346
383
 
347
384
  def ready_image(action_handler, image_spec, image_options)
@@ -359,22 +396,12 @@ module AWSDriver
359
396
  end
360
397
 
361
398
  def destroy_image(action_handler, image_spec, image_options)
362
- actual_image = image_for(image_spec)
363
- if actual_image.nil? || !actual_image.exists?
364
- Chef::Log.warn "Image #{image_spec.name} doesn't exist"
365
- else
366
- snapshots = actual_image.block_device_mappings.map do |dev, opts|
367
- ec2.snapshots[opts[:snapshot_id]]
368
- end
369
- action_handler.perform_action "De-registering image #{image_spec.name}" do
370
- actual_image.deregister
371
- end
372
- if snapshots.any?
373
- action_handler.perform_action "Deleting image #{image_spec.name} snapshots" do
374
- snapshots.each do |snap|
375
- snap.delete
376
- end
377
- end
399
+ # TODO the driver should automatically be set by `inline_resource`
400
+ d = self
401
+ Provisioning.inline_resource(action_handler) do
402
+ aws_image image_spec.name do
403
+ action :destroy
404
+ driver d
378
405
  end
379
406
  end
380
407
  end
@@ -402,25 +429,28 @@ EOD
402
429
  # Machine methods
403
430
  def allocate_machine(action_handler, machine_spec, machine_options)
404
431
  actual_instance = instance_for(machine_spec)
432
+ bootstrap_options = bootstrap_options_for(action_handler, machine_spec, machine_options)
433
+
405
434
  if actual_instance == nil || !actual_instance.exists? || actual_instance.status == :terminated
406
- bootstrap_options = bootstrap_options_for(action_handler, machine_spec, machine_options)
407
435
 
408
436
  action_handler.perform_action "Create #{machine_spec.name} with AMI #{bootstrap_options[:image_id]} in #{aws_config.region}" do
409
437
  Chef::Log.debug "Creating instance with bootstrap options #{bootstrap_options}"
410
438
 
411
- instance = ec2.instances.create(bootstrap_options.to_hash)
439
+ actual_instance = ec2.instances.create(bootstrap_options.to_hash)
412
440
 
413
441
  # Make sure the instance is ready to be tagged
414
- sleep 5 while instance.status == :pending
442
+ Retryable.retryable(:tries => 12, :sleep => 5, :on => [AWS::EC2::Errors::InvalidInstanceID::NotFound, TimeoutError]) do
443
+ raise TimeoutError unless actual_instance.status == :pending || actual_instance.status == :running
444
+ end
415
445
  # TODO add other tags identifying user / node url (same as fog)
416
- instance.tags['Name'] = machine_spec.name
417
- instance.source_dest_check = machine_options[:source_dest_check] if machine_options.has_key?(:source_dest_check)
446
+ actual_instance.tags['Name'] = machine_spec.name
447
+ actual_instance.source_dest_check = machine_options[:source_dest_check] if machine_options.has_key?(:source_dest_check)
418
448
  machine_spec.reference = {
419
449
  'driver_version' => Chef::Provisioning::AWSDriver::VERSION,
420
450
  'allocated_at' => Time.now.utc.to_s,
421
451
  'host_node' => action_handler.host_node,
422
452
  'image_id' => bootstrap_options[:image_id],
423
- 'instance_id' => instance.id
453
+ 'instance_id' => actual_instance.id
424
454
  }
425
455
  machine_spec.driver_url = driver_url
426
456
  machine_spec.reference['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
@@ -429,6 +459,9 @@ EOD
429
459
  end
430
460
  end
431
461
  end
462
+ # TODO because we don't want to add `provider_tags` as a base attribute,
463
+ # we have to update the tags here in driver.rb instead of the providers
464
+ converge_tags(actual_instance, machine_options[:aws_tags], action_handler)
432
465
  end
433
466
 
434
467
  def allocate_machines(action_handler, specs_and_options, parallelizer)
@@ -470,17 +503,15 @@ EOD
470
503
  end
471
504
 
472
505
  def destroy_machine(action_handler, machine_spec, machine_options)
473
- instance = instance_for(machine_spec)
474
- if instance && instance.exists?
475
- # TODO do we need to wait_until(action_handler, machine_spec, instance) { instance.status != :shutting_down } ?
476
- action_handler.perform_action "Terminate #{machine_spec.name} (#{machine_spec.reference['instance_id']}) in #{aws_config.region} ..." do
477
- instance.terminate
478
- machine_spec.reference = nil
506
+ d = self
507
+ Provisioning.inline_resource(action_handler) do
508
+ aws_instance machine_spec.name do
509
+ action :destroy
510
+ driver d
479
511
  end
480
- else
481
- Chef::Log.warn "Instance #{machine_spec.reference['instance_id']} doesn't exist for #{machine_spec.name}"
482
512
  end
483
513
 
514
+ # TODO move this into the aws_instance provider somehow
484
515
  strategy = convergence_strategy_for(machine_spec, machine_options)
485
516
  strategy.cleanup_convergence(action_handler, machine_spec)
486
517
  end
@@ -563,6 +594,7 @@ EOD
563
594
 
564
595
  def bootstrap_options_for(action_handler, machine_spec, machine_options)
565
596
  bootstrap_options = (machine_options[:bootstrap_options] || {}).to_h.dup
597
+ bootstrap_options[:instance_type] ||= default_instance_type
566
598
  image_id = bootstrap_options[:image_id] || machine_options[:image_id] || default_ami_for_region(aws_config.region)
567
599
  bootstrap_options[:image_id] = image_id
568
600
  if !bootstrap_options[:key_name]
@@ -650,23 +682,23 @@ EOD
650
682
 
651
683
  case region
652
684
  when 'ap-northeast-1'
653
- 'ami-c786dcc6'
685
+ 'ami-6cbca76d'
654
686
  when 'ap-southeast-1'
655
- 'ami-eefca7bc'
687
+ 'ami-04c6ec56'
656
688
  when 'ap-southeast-2'
657
- 'ami-996706a3'
689
+ 'ami-c9eb9ff3'
658
690
  when 'eu-west-1'
659
- 'ami-4ab46b3d'
691
+ 'ami-5f9e1028'
660
692
  when 'eu-central-1'
661
- 'ami-7c3c0a61'
693
+ 'ami-56c2f14b'
662
694
  when 'sa-east-1'
663
- 'ami-6770d87a'
695
+ 'ami-81f14e9c'
664
696
  when 'us-east-1'
665
- 'ami-d2ff23ba'
697
+ 'ami-12793a7a'
666
698
  when 'us-west-1'
667
- 'ami-73717d36'
699
+ 'ami-6ebca42b'
668
700
  when 'us-west-2'
669
- 'ami-f1ce8bc1'
701
+ 'ami-b9471c89'
670
702
  else
671
703
  raise 'Unsupported region!'
672
704
  end
@@ -912,6 +944,7 @@ EOD
912
944
  if actual_instance.status == :terminated
913
945
  Chef::Log.warn "Machine #{machine_spec.name} (#{actual_instance.id}) is terminated. Recreating ..."
914
946
  else
947
+ converge_tags(actual_instance, machine_options[:aws_tags], action_handler)
915
948
  yield machine_spec, actual_instance if block_given?
916
949
  next
917
950
  end
@@ -951,6 +984,7 @@ EOD
951
984
  machine_spec.driver_url = driver_url
952
985
  instance.tags['Name'] = machine_spec.name
953
986
  instance.source_dest_check = machine_options[:source_dest_check] if machine_options.has_key?(:source_dest_check)
987
+ converge_tags(instance, machine_options[:aws_tags], action_handler)
954
988
  machine_spec.reference['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
955
989
  %w(is_windows ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
956
990
  machine_spec.reference[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
@@ -977,6 +1011,43 @@ EOD
977
1011
  end.to_a
978
1012
  end
979
1013
 
1014
+ # TODO This is currently duplicated from AWS Provider
1015
+ # Set the tags on the aws object to desired_tags, while ignoring any `Name` tag
1016
+ # If no tags need to be modified, will not perform a write call on AWS
1017
+ def converge_tags(
1018
+ aws_object,
1019
+ desired_tags,
1020
+ action_handler,
1021
+ read_tags_block=lambda {|aws_object| aws_object.tags.to_h},
1022
+ set_tags_block=lambda {|aws_object, desired_tags| aws_object.tags.set(desired_tags) },
1023
+ delete_tags_block=lambda {|aws_object, tags_to_delete| aws_object.tags.delete(*tags_to_delete) }
1024
+ )
1025
+ # If aws_tags were not provided we exit
1026
+ if desired_tags.nil?
1027
+ Chef::Log.debug "aws_tags not provided, nothing to converge"
1028
+ return
1029
+ end
1030
+ current_tags = read_tags_block.call(aws_object)
1031
+ # AWS always returns tags as strings, and we don't want to overwrite a
1032
+ # tag-as-string with the same tag-as-symbol
1033
+ desired_tags = Hash[desired_tags.map {|k, v| [k.to_s, v.to_s] }]
1034
+ tags_to_update = desired_tags.reject {|k,v| current_tags[k] == v}
1035
+ tags_to_delete = current_tags.keys - desired_tags.keys
1036
+ # We don't want to delete `Name`, just all other tags
1037
+ tags_to_delete.delete('Name')
1038
+
1039
+ unless tags_to_update.empty?
1040
+ action_handler.perform_action "applying tags #{tags_to_update}" do
1041
+ set_tags_block.call(aws_object, tags_to_update)
1042
+ end
1043
+ end
1044
+ unless tags_to_delete.empty?
1045
+ action_handler.perform_action "deleting tags #{tags_to_delete.inspect}" do
1046
+ delete_tags_block.call(aws_object, tags_to_delete)
1047
+ end
1048
+ end
1049
+ end
1050
+
980
1051
  def get_listeners(listeners)
981
1052
  case listeners
982
1053
  when Hash
@@ -1035,7 +1106,7 @@ EOD
1035
1106
  end
1036
1107
 
1037
1108
  def default_instance_type
1038
- 't1.micro'
1109
+ 't2.micro'
1039
1110
  end
1040
1111
 
1041
1112
  PORT_DEFAULTS = {
@@ -0,0 +1,16 @@
1
+ class Chef
2
+ module Provisioning
3
+ module AWSDriver
4
+ module Exceptions
5
+
6
+ class MultipleSecurityGroupError < RuntimeError
7
+ def initialize(name, groups)
8
+ super "Found security groups with ids [#{groups.map {|sg| sg.id}}] that share name #{name}. " \
9
+ "Names are unique within VPCs - specify VPC to find by name."
10
+ end
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
@@ -45,7 +45,7 @@ end
45
45
 
46
46
  module NoResourceCloning
47
47
  def prior_resource
48
- if resource_class.kind_of?(Chef::Provisioning::AWSDriver::SuperLWRP)
48
+ if resource_class <= Chef::Provisioning::AWSDriver::SuperLWRP
49
49
  Chef::Log.debug "Canceling resource cloning for #{resource_class}"
50
50
  nil
51
51
  else
@@ -1,7 +1,7 @@
1
1
  class Chef
2
2
  module Provisioning
3
3
  module AWSDriver
4
- VERSION = '1.1.1'
4
+ VERSION = '1.2.0'
5
5
  end
6
6
  end
7
7
  end
@@ -8,8 +8,8 @@ class Chef::Resource::AwsEbsVolume < Chef::Provisioning::AWSDriver::AWSResourceW
8
8
 
9
9
  attribute :machine, kind_of: [ String, FalseClass, AwsInstance, AWS::EC2::Instance ]
10
10
 
11
- attribute :availability_zone, kind_of: String
12
- attribute :size, kind_of: Integer
11
+ attribute :availability_zone, kind_of: String, default: 'a'
12
+ attribute :size, kind_of: Integer, default: 8
13
13
  attribute :snapshot, kind_of: String
14
14
 
15
15
  attribute :iops, kind_of: Integer
@@ -6,6 +6,9 @@ class Chef::Resource::AwsEipAddress < Chef::Provisioning::AWSDriver::AWSResource
6
6
 
7
7
  attribute :name, kind_of: String, name_attribute: true
8
8
 
9
+ # guh - every other AWSResourceWithEntry accepts tags EXCEPT this one
10
+ undef_method(:aws_tags)
11
+
9
12
  # TODO network interface
10
13
  attribute :machine, kind_of: [String, FalseClass]
11
14
  attribute :associate_to_vpc, kind_of: [TrueClass, FalseClass]
@@ -34,6 +34,17 @@ class Chef::Resource::AwsRouteTable < Chef::Provisioning::AWSDriver::AWSResource
34
34
  #
35
35
  attribute :vpc, kind_of: [ String, AwsVpc, AWS::EC2::VPC ], required: true
36
36
 
37
+ #
38
+ # Enable route propagation from one or more virtual private gateways
39
+ #
40
+ # The value should be an array of virtual private gateway ID:
41
+ # ```ruby
42
+ # virtual_private_gateways ['vgw-abcd1234', 'vgw-abcd5678']
43
+ # ```
44
+ #
45
+ attribute :virtual_private_gateways, kind_of: [ String, Array ],
46
+ coerce: proc { |v| [v].flatten }
47
+
37
48
  #
38
49
  # The routes for this route table.
39
50
  #
@@ -58,6 +69,26 @@ class Chef::Resource::AwsRouteTable < Chef::Provisioning::AWSDriver::AWSResource
58
69
  #
59
70
  attribute :routes, kind_of: Hash
60
71
 
72
+ #
73
+ # Regex to ignore one or more route targets.
74
+ #
75
+ # This is helpful when configuring HA NAT instances. If a NAT instance fails
76
+ # a auto-scaling group may launch a new NAT instance and update the route
77
+ # table accordingly. Chef provisioning should not attempt to change or remove
78
+ # this route.
79
+ #
80
+ # This attribute is specified as a regex since the full ID of the
81
+ # instance/network interface is not known ahead of time. In most cases the
82
+ # NAT instance route will point at a network interface attached to the NAT
83
+ # instance. The ID prefix for network interfaces is 'eni'. The following
84
+ # example shows how to ignore network interface routes.
85
+ #
86
+ # ```ruby
87
+ # ignore_route_targets ['^eni-']
88
+ # ```
89
+ attribute :ignore_route_targets, kind_of: [ String, Array ], default: [],
90
+ coerce: proc { |v| [v].flatten }
91
+
61
92
  attribute :route_table_id, kind_of: String, aws_id_attribute: true, lazy_default: proc {
62
93
  name =~ /^rtb-[a-f0-9]{8}$/ ? name : nil
63
94
  }