chef-provisioning-aws 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  }