chef-provisioning-aws 1.0.4 → 1.1.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -0
  3. data/Rakefile +5 -0
  4. data/lib/chef/provider/aws_ebs_volume.rb +14 -4
  5. data/lib/chef/provider/aws_image.rb +31 -0
  6. data/lib/chef/provider/aws_instance.rb +14 -0
  7. data/lib/chef/provider/aws_load_balancer.rb +9 -0
  8. data/lib/chef/provider/aws_network_interface.rb +209 -0
  9. data/lib/chef/provider/aws_security_group.rb +9 -4
  10. data/lib/chef/provider/aws_subnet.rb +16 -1
  11. data/lib/chef/provider/aws_vpc.rb +16 -0
  12. data/lib/chef/provisioning/aws_driver/aws_provider.rb +44 -0
  13. data/lib/chef/provisioning/aws_driver/aws_resource.rb +1 -1
  14. data/lib/chef/provisioning/aws_driver/driver.rb +6 -5
  15. data/lib/chef/provisioning/aws_driver/version.rb +1 -1
  16. data/lib/chef/resource/aws_image.rb +1 -2
  17. data/lib/chef/resource/aws_instance.rb +1 -2
  18. data/lib/chef/resource/aws_load_balancer.rb +1 -1
  19. data/lib/chef/resource/aws_network_interface.rb +23 -5
  20. data/lib/chef/resource/aws_vpc.rb +0 -8
  21. data/spec/aws_support.rb +235 -0
  22. data/spec/aws_support/aws_resource_run_wrapper.rb +45 -0
  23. data/spec/aws_support/deep_matcher.rb +40 -0
  24. data/spec/aws_support/deep_matcher/fuzzy_match_objects.rb +57 -0
  25. data/spec/aws_support/deep_matcher/match_values_failure_messages.rb +145 -0
  26. data/spec/aws_support/deep_matcher/matchable_array.rb +24 -0
  27. data/spec/aws_support/deep_matcher/matchable_object.rb +25 -0
  28. data/spec/aws_support/deep_matcher/rspec_monkeypatches.rb +25 -0
  29. data/spec/aws_support/delayed_stream.rb +41 -0
  30. data/spec/aws_support/matchers/create_an_aws_object.rb +60 -0
  31. data/spec/aws_support/matchers/update_an_aws_object.rb +66 -0
  32. data/spec/integration/aws_ebs_volume_spec.rb +31 -0
  33. data/spec/integration/aws_key_pair_spec.rb +21 -0
  34. data/spec/integration/aws_route_table_spec.rb +40 -0
  35. data/spec/integration/aws_security_group_spec.rb +7 -5
  36. data/spec/integration/aws_subnet_spec.rb +56 -0
  37. data/spec/integration/aws_vpc_spec.rb +109 -0
  38. data/spec/integration/machine_batch_spec.rb +36 -0
  39. data/spec/integration/machine_image_spec.rb +49 -0
  40. data/spec/integration/machine_spec.rb +64 -0
  41. data/spec/spec_helper.rb +8 -2
  42. data/spec/unit/aws_driver/credentials_spec.rb +54 -0
  43. metadata +27 -5
  44. data/spec/support/aws_support.rb +0 -211
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd5c21364731d0404bcc87495c3a1e7fddc9b107
4
- data.tar.gz: b0f57238cd577c5be3b7ee652079d1185a3fef8f
3
+ metadata.gz: 66de7d20ac16f1c99866f9d4195f38586bfb667d
4
+ data.tar.gz: 5cbae8b8fcc08af3538333bbf6bc29c96beb0f22
5
5
  SHA512:
6
- metadata.gz: 9676f4c941de6c694f9df2d58d04b0fd03a5958a23c10cacae91a50c4bfe52d1aadf249e36346ab2c003f4931d127d599a744ba9740505ee2d3c457f08ba6d79
7
- data.tar.gz: c84c5149b0127cfabed5ad71f967a48cc0dce1cf05ebc80b8b90e3433b1ac485f5ff2a42459d4167aa1e954112617cf8cf8d4f295c6623b547ef97f8c60c85f3
6
+ metadata.gz: 36907c00f23caf788569f528f6b12ad64afb4c316f02794dd05927c06e234b7271257d7d83c85f517c7bb370928507fe2616bb9379e5b161e48cdce50dc3fed6
7
+ data.tar.gz: 616a2bcd0b104326714aa001462f042eedec30d06825b5e9556b6e326a03037833ddd4d79879183bd4b20a7335272140f253be91b380817acf4a2b13c4c298ae
data/README.md CHANGED
@@ -12,3 +12,21 @@ An implementation of the AWS driver using the AWS Ruby SDK (v1). It also implem
12
12
  * Autoscaling Groups
13
13
  * SSH Key pairs
14
14
  * Launch configs
15
+
16
+ # Running Integration Tests
17
+
18
+ To run the integration tests execute `bundle exec rake integration`. If you have not set it up,
19
+ you should see an error message about a missing environment variable `AWS_TEST_DRIVER`. You can add
20
+ this as a normal environment variable or set it for a single run with `AWS_TEST_DRIVER=aws::eu-west-1
21
+ bundle exec rake integration`. The format should match what `with_driver` expects.
22
+
23
+ You will also need to have configured your `~/.aws/config` or environment variables with your
24
+ AWS credentials.
25
+
26
+ This creates real objects within AWS. The tests make their best effort to delete these objects
27
+ after each test finishes but errors can happen which prevent this. Be aware that this may charge
28
+ you!
29
+
30
+ If you find the tests leaving behind resources during normal conditions (IE, not when there is an
31
+ unexpected exception) please file a bug. Most objects can be cleaned up by deleting the `test_vpc`
32
+ from within the AWS browser console.
data/Rakefile CHANGED
@@ -10,3 +10,8 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
10
10
  # TODO add back integration tests whenever we have strategy for keys
11
11
  spec.exclude_pattern = 'spec/integration/**/*_spec.rb'
12
12
  end
13
+
14
+ desc "Run Integration Specs"
15
+ RSpec::Core::RakeTask.new(:integration) do |spec|
16
+ spec.pattern = 'spec/integration/**/*_spec.rb'
17
+ end
@@ -47,8 +47,8 @@ class Chef::Provider::AwsEbsVolume < Chef::Provisioning::AWSDriver::AWSProvider
47
47
 
48
48
  def update_aws_object(volume)
49
49
  if initial_options.has_key?(:availability_zone)
50
- if initial_options[:availability_zone] != volume.availability_zone_name
51
- raise "#{new_resource}.availability_zone is #{new_resource.availability_zone}, but actual volume has availability_zone_name set to #{volume.availability_zone_name}. Cannot be modified!"
50
+ if availability_zone != volume.availability_zone_name
51
+ raise "#{new_resource}.availability_zone is #{availability_zone}, but actual volume has availability_zone_name set to #{volume.availability_zone_name}. Cannot be modified!"
52
52
  end
53
53
  end
54
54
  if initial_options.has_key?(:size)
@@ -99,7 +99,7 @@ class Chef::Provider::AwsEbsVolume < Chef::Provisioning::AWSDriver::AWSProvider
99
99
  def initial_options
100
100
  @initial_options ||= begin
101
101
  options = {}
102
- options[:availability_zone] = new_resource.availability_zone if !new_resource.availability_zone.nil?
102
+ options[:availability_zone] = availability_zone if !new_resource.availability_zone.nil?
103
103
  options[:size] = new_resource.size if !new_resource.size.nil?
104
104
  options[:snapshot_id] = new_resource.snapshot if !new_resource.snapshot.nil?
105
105
  options[:iops] = new_resource.iops if !new_resource.iops.nil?
@@ -146,7 +146,7 @@ class Chef::Provider::AwsEbsVolume < Chef::Provisioning::AWSDriver::AWSProvider
146
146
  end
147
147
  volume
148
148
  end
149
-
149
+
150
150
  def wait_for_volume_status(volume, expected_status)
151
151
  initial_status = volume.status
152
152
  log_callback = proc {
@@ -204,4 +204,14 @@ class Chef::Provider::AwsEbsVolume < Chef::Provisioning::AWSDriver::AWSProvider
204
204
  volume
205
205
  end
206
206
  end
207
+
208
+ def availability_zone
209
+ az = new_resource.availability_zone
210
+ if /^#{region}/ =~ az
211
+ Chef::Log.warn("availability_zone attribute should only be set to the letter designation. Attempting to use '#{az[-1]}' to correct the issue.")
212
+ elsif az.length == 1
213
+ az = "#{region}#{az[-1]}"
214
+ end
215
+ az
216
+ end
207
217
  end
@@ -0,0 +1,31 @@
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
+
3
+ class Chef::Provider::AwsImage < Chef::Provisioning::AWSDriver::AWSProvider
4
+ def destroy_aws_object(image)
5
+ instance_id = image.tags['From-Instance']
6
+ Chef::Log.debug("Found From-Instance tag [#{instance_id}] on #{image.id}")
7
+ unless instance_id
8
+ # This is an old image and doesn't have the tag added - lets try and find it from the block device mapping
9
+ image.block_device_mappings.map do |dev, opts|
10
+ snapshot = ec2.snapshots[opts[:snapshot_id]]
11
+ desc = snapshot.description
12
+ m = /CreateImage\(([^\)]+)\)/.match(desc)
13
+ if m
14
+ Chef::Log.debug("Found [#{instance_id}] from snapshot #{snapshot.id} on #{image.id}")
15
+ instance_id = m[1]
16
+ end
17
+ end
18
+ end
19
+ converge_by "delete image #{new_resource} in #{region}" do
20
+ image.delete
21
+ end
22
+ if instance_id
23
+ # As part of the image creation process, the source instance was automatically
24
+ # destroyed - we just need to make sure that has completed successfully
25
+ instance = new_resource.driver.ec2.instances[instance_id]
26
+ converge_by "waiting until instance #{instance.id} is :terminated" do
27
+ wait_for_status(instance, :terminated, [AWS::EC2::Errors::InvalidInstanceID::NotFound])
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
+
3
+ class Chef::Provider::AwsInstance < Chef::Provisioning::AWSDriver::AWSProvider
4
+ def destroy_aws_object(instance)
5
+ converge_by "delete instance #{new_resource} in VPC #{instance.vpc.id} in #{region}" do
6
+ instance.delete
7
+ end
8
+ converge_by "waited until instance #{new_resource} is :terminated" do
9
+ # When purging, we must wait until the instance is fully terminated - thats the only way
10
+ # to delete the network interface that I can see
11
+ wait_for_status(instance, :terminated, [AWS::EC2::Errors::InvalidInstanceID::NotFound])
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
+
3
+ class Chef::Provider::AwsLoadBalancer < Chef::Provisioning::AWSDriver::AWSProvider
4
+ def destroy_aws_object(load_balancer)
5
+ converge_by "delete load balancer #{new_resource.name} (#{load_balancer.id}) in VPC #{load_balancer.vpc.id} in #{region}" do
6
+ load_balancer.delete
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,209 @@
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
+ require 'cheffish'
3
+ require 'date'
4
+ require 'retryable'
5
+
6
+ class Chef::Provider::AwsNetworkInterface < Chef::Provisioning::AWSDriver::AWSProvider
7
+ class NetworkInterfaceStatusTimeoutError < TimeoutError
8
+ def initialize(new_resource, initial_status, expected_status)
9
+ super("timed out waiting for #{new_resource} status to change from #{initial_status} to #{expected_status}!")
10
+ end
11
+ end
12
+
13
+ class NetworkInterfaceStatusTimeoutError < TimeoutError
14
+ def initialize(new_resource, initial_status, expected_status)
15
+ super("timed out waiting for #{new_resource} status to change from #{initial_status} to #{expected_status}!")
16
+ end
17
+ end
18
+
19
+ class NetworkInterfaceInvalidStatusError < RuntimeError
20
+ def initialize(new_resource, status)
21
+ super("#{new_resource} is in #{status} state!")
22
+ end
23
+ end
24
+
25
+ def action_create
26
+ eni = super
27
+
28
+ if !new_resource.machine.nil?
29
+ update_eni(eni)
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def create_aws_object
36
+ eni = nil
37
+ converge_by "create new #{new_resource} in #{region}" do
38
+ eni = new_resource.driver.ec2.network_interfaces.create(options)
39
+ eni.tags['Name'] = new_resource.name
40
+ end
41
+
42
+ converge_by "wait for new #{new_resource} in #{region} to become available" do
43
+ wait_for_eni_status(eni, :available)
44
+ eni
45
+ end
46
+ end
47
+
48
+ def update_aws_object(eni)
49
+ if options.has_key?(:subnet)
50
+ if Chef::Resource::AwsSubnet.get_aws_object(options[:subnet], resource: new_resource) != eni.subnet
51
+ raise "#{new_resource} subnet is #{new_resource.subnet}, but actual network interface has subnet set to #{eni.subnet_id}. Cannot be modified!"
52
+ end
53
+ end
54
+
55
+ # TODO implement private ip reassignment
56
+ if options.has_key?(:private_ip_address)
57
+ if options[:private_ip_address] != eni.private_ip_address
58
+ raise "#{new_resource} private IP is #{new_resource.private_ip_address}, but actual network interface has private IP set to #{eni.private_ip_address}. Private IP reassignment not implemented. Cannot be modified!"
59
+ end
60
+ end
61
+
62
+ if options.has_key?(:description)
63
+ if options[:description] != eni.description
64
+ converge_by "set #{new_resource} description to #{new_resource.description}" do
65
+ eni.client.modify_network_interface_attribute(:network_interface_id => eni.network_interface_id,
66
+ :description => {
67
+ :value => new_resource.description
68
+ })
69
+ end
70
+ end
71
+ end
72
+
73
+ if options.has_key?(:security_groups)
74
+ groups = new_resource.security_groups.map { |sg|
75
+ Chef::Resource::AwsSecurityGroup.get_aws_object(sg, resource: new_resource)
76
+ }
77
+ if groups.sort != eni.security_groups.sort
78
+ converge_by "set #{new_resource} security groups to #{groups}" do
79
+ eni.set_security_groups(groups)
80
+ eni
81
+ end
82
+ end
83
+ end
84
+
85
+ eni
86
+ end
87
+
88
+ def destroy_aws_object(eni)
89
+ detach(eni) if eni.status == :in_use
90
+ delete(eni)
91
+ end
92
+
93
+ private
94
+
95
+ def expected_instance
96
+ # use instance if already set
97
+ @expected_instance ||= new_resource.machine ?
98
+ # if not, and machine is set, find and return the instance
99
+ Chef::Resource::AwsInstance.get_aws_object(new_resource.machine, resource: new_resource) :
100
+ # otherwise return nil
101
+ nil
102
+ end
103
+
104
+ def options
105
+ @options ||= begin
106
+ options = {}
107
+ options[:subnet] = new_resource.subnet if !new_resource.subnet.nil?
108
+ options[:private_ip_address] = new_resource.private_ip_address if !new_resource.private_ip_address.nil?
109
+ options[:description] = new_resource.description if !new_resource.description.nil?
110
+ options[:security_groups] = new_resource.security_groups if !new_resource.security_groups.nil?
111
+ options[:device_index] = new_resource.device_index if !new_resource.device_index.nil?
112
+
113
+ AWSResource.lookup_options(options, resource: new_resource)
114
+ end
115
+ end
116
+
117
+ def update_eni(eni)
118
+ status = eni.status
119
+ #
120
+ # If we were told to attach the network interface to a machine, do so
121
+ #
122
+ if expected_instance.is_a?(AWS::EC2::Instance)
123
+ case status
124
+ when :available
125
+ attach(eni)
126
+ when :in_use
127
+ # We don't want to attempt to reattach to the same instance or device index
128
+ attachment = current_attachment(eni)
129
+ if attachment.instance != expected_instance || (options[:device_index] && attachment.device_index != new_resource.device_index)
130
+ detach(eni)
131
+ attach(eni)
132
+ end
133
+ when nil
134
+ raise NetworkInterfaceNotFoundError.new(new_resource)
135
+ else
136
+ raise NetworkInterfaceInvalidStatusError.new(new_resource, status)
137
+ end
138
+
139
+ #
140
+ # If we were told to set the machine to false, detach it.
141
+ #
142
+ else
143
+ case status
144
+ when nil
145
+ Chef::Log.warn NetworkInterfaceNotFoundError.new(new_resource)
146
+ when :in_use
147
+ detach(eni)
148
+ end
149
+ end
150
+ eni
151
+ end
152
+
153
+ def wait_for_eni_status(eni, expected_status)
154
+ initial_status = eni.status
155
+ log_callback = proc {
156
+ Chef::Log.info("waiting for #{new_resource} status to change to #{expected_status}...")
157
+ }
158
+
159
+ Retryable.retryable(:tries => 30, :sleep => 2, :on => NetworkInterfaceStatusTimeoutError, :ensure => log_callback) do
160
+ raise NetworkInterfaceStatusTimeoutError.new(new_resource, initial_status, expected_status) if eni.status != expected_status
161
+ end
162
+ end
163
+
164
+ def detach(eni)
165
+ attachment = current_attachment(eni)
166
+ instance = attachment.instance
167
+
168
+ converge_by "detach #{new_resource} from #{instance.instance_id}" do
169
+ eni.detach
170
+ end
171
+
172
+ converge_by "wait for #{new_resource} to detach" do
173
+ wait_for_eni_status(eni, :available)
174
+ eni
175
+ end
176
+ end
177
+
178
+ def attach(eni)
179
+ converge_by "attach #{new_resource} to #{new_resource.machine} (#{expected_instance.instance_id})" do
180
+ eni.attach(expected_instance, options)
181
+ end
182
+
183
+ converge_by "wait for #{new_resource} to attach" do
184
+ wait_for_eni_status(eni, :in_use)
185
+ eni
186
+ end
187
+ end
188
+
189
+ def current_attachment(eni)
190
+ eni.attachment
191
+ end
192
+
193
+ def delete(eni)
194
+ converge_by "delete #{new_resource} in #{region}" do
195
+ eni.delete
196
+ end
197
+
198
+ converge_by "wait for #{new_resource} in #{region} to delete" do
199
+ log_callback = proc {
200
+ Chef::Log.info('waiting for network interface to delete...')
201
+ }
202
+
203
+ Retryable.retryable(:tries => 30, :sleep => 2, :on => NetworkInterfaceStatusTimeoutError, :ensure => log_callback) do
204
+ raise NetworkInterfaceStatusTimeoutError.new(new_resource, 'exists', 'deleted') if eni.exists?
205
+ end
206
+ eni
207
+ end
208
+ end
209
+ end
@@ -71,7 +71,7 @@ class Chef::Provider::AwsSecurityGroup < Chef::Provisioning::AWSDriver::AWSProvi
71
71
  when Array
72
72
  # [ { port: X, protocol: Y, sources: [ ... ]}]
73
73
  new_resource.inbound_rules.each do |rule|
74
- port_ranges = get_port_ranges(port_range: rule[:port], protocol: rule[:protocol])
74
+ port_ranges = get_port_ranges(rule)
75
75
  add_rule(desired_rules, port_ranges, get_actors(vpc, rule[:sources]))
76
76
  end
77
77
 
@@ -115,7 +115,7 @@ class Chef::Provider::AwsSecurityGroup < Chef::Provisioning::AWSDriver::AWSProvi
115
115
  when Array
116
116
  # [ { port: X, protocol: Y, sources: [ ... ]}]
117
117
  new_resource.outbound_rules.each do |rule|
118
- port_ranges = get_port_ranges(port_range: rule[:port], protocol: rule[:protocol])
118
+ port_ranges = get_port_ranges(rule)
119
119
  add_rule(desired_rules, port_ranges, get_actors(vpc, rule[:destinations]))
120
120
  end
121
121
 
@@ -204,11 +204,16 @@ class Chef::Provider::AwsSecurityGroup < Chef::Provisioning::AWSDriver::AWSProvi
204
204
  [ { port_range: port_spec, protocol: :tcp } ]
205
205
  when Array
206
206
  port_spec.map { |p| get_port_ranges(p) }.flatten
207
+ when :icmp
208
+ { port_range: port_spec, protocol: :icmp }
207
209
  when Hash
210
+ port_range = port_spec[:port_range] || port_spec[:ports] || port_spec[:port]
211
+ port_range = port_range..port_range if port_range.is_a?(Integer)
208
212
  if port_spec[:protocol]
209
- [ { port_range: port_spec[:port_range] || port_spec[:port], protocol: port_spec[:protocol].to_s.to_sym } ]
213
+ port_range ||= -1..-1
214
+ [ { port_range: port_range, protocol: port_spec[:protocol].to_s.to_sym || :tcp } ]
210
215
  else
211
- get_port_ranges(port_spec[:port_range] || port_spec[:port])
216
+ get_port_ranges(port_range)
212
217
  end
213
218
  # The to_s.to_sym dance is because if you specify a protocol number, AWS symbolifies it,
214
219
  # but 26.to_sym doesn't work (so we have to to_s it first).
@@ -51,8 +51,23 @@ class Chef::Provider::AwsSubnet < Chef::Provisioning::AWSDriver::AWSProvider
51
51
  end
52
52
 
53
53
  def destroy_aws_object(subnet)
54
+ if purging
55
+ # TODO possibly convert to http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/Client.html#terminate_instances-instance_method
56
+ p = Chef::ChefFS::Parallelizer.new(5)
57
+ p.parallel_do(subnet.instances.to_a) do |instance|
58
+ Cheffish.inline_resource(self, action) do
59
+ aws_instance instance do
60
+ action :purge
61
+ end
62
+ end
63
+ end
64
+ end
54
65
  converge_by "delete subnet #{new_resource.name} in VPC #{new_resource.vpc} in #{region}" do
55
- subnet.delete
66
+ # If the subnet doesn't exist we can't check state on it - state can only be :pending or :available
67
+ begin
68
+ subnet.delete
69
+ rescue AWS::EC2::Errors::InvalidSubnetID::NotFound
70
+ end
56
71
  end
57
72
  end
58
73
 
@@ -55,6 +55,22 @@ class Chef::Provider::AwsVpc < Chef::Provisioning::AWSDriver::AWSProvider
55
55
  end
56
56
 
57
57
  def destroy_aws_object(vpc)
58
+ if purging
59
+ vpc.subnets.each do |s|
60
+ Cheffish.inline_resource(self, action) do # if action isn't defined, we want :purge
61
+ aws_subnet s do
62
+ action :purge
63
+ end
64
+ end
65
+ end
66
+ # If any of the below resources start needing complicated delete logic (dependent resources needing to
67
+ # be deleted) move that logic into `delete_aws_resource` and add the purging logic to the resource
68
+ vpc.network_acls.each { |o| o.delete unless o.default? }
69
+ vpc.network_interfaces.each { |o| o.delete }
70
+ vpc.route_tables.each { |o| o.delete unless o.main? }
71
+ vpc.security_groups.each { |o| o.delete unless o.name == 'default' }
72
+ end
73
+
58
74
  # Detach or destroy the internet gateway
59
75
  ig = vpc.internet_gateway
60
76
  if ig
@@ -3,6 +3,7 @@ require 'chef/provisioning/aws_driver/aws_resource'
3
3
  require 'chef/provisioning/aws_driver/aws_resource_with_entry'
4
4
  require 'chef/provisioning/chef_managed_entry_store'
5
5
  require 'chef/provisioning/chef_provider_action_handler'
6
+ require 'retryable'
6
7
 
7
8
  module Chef::Provisioning::AWSDriver
8
9
  class AWSProvider < Chef::Provider::LWRPBase
@@ -10,6 +11,12 @@ class AWSProvider < Chef::Provider::LWRPBase
10
11
 
11
12
  AWSResource = Chef::Provisioning::AWSDriver::AWSResource
12
13
 
14
+ class StatusTimeoutError < TimeoutError
15
+ def initialize(aws_object, initial_status, expected_status)
16
+ super("timed out waiting for #{aws_object.id} status to change from #{initial_status.inspect} to #{expected_status.inspect}!")
17
+ end
18
+ end
19
+
13
20
  def action_handler
14
21
  @action_handler ||= Chef::Provisioning::ChefProviderActionHandler.new(self)
15
22
  end
@@ -124,6 +131,18 @@ class AWSProvider < Chef::Provider::LWRPBase
124
131
  aws_object
125
132
  end
126
133
 
134
+ # TODO having a @purging flag feels weird
135
+ action :purge do
136
+ @purging = true
137
+ begin
138
+ action_destroy
139
+ ensure
140
+ @purging = false
141
+ end
142
+ end
143
+
144
+ attr_reader :purging
145
+
127
146
  action :destroy do
128
147
  desired_driver = new_resource.driver
129
148
  desired_id = new_resource.public_send(new_resource.class.aws_id_attribute) if new_resource.class.aws_id_attribute
@@ -202,5 +221,30 @@ class AWSProvider < Chef::Provider::LWRPBase
202
221
  raise NotImplementedError, :destroy_aws_object
203
222
  end
204
223
 
224
+ # Wait until aws_object obtains one of expected_status
225
+ #
226
+ # @param aws_object Aws SDK Object to check status on
227
+ # @param expected_status [Symbol,Array<Symbol>] Final status(s) to look for
228
+ # @param acceptable_errors [Exception,Array<Exception>] Acceptable errors that are caught and squelched
229
+ # @param tries [Integer] Number of times to check status
230
+ # @param sleep [Integer] Time to wait between checking status
231
+ #
232
+ def wait_for_status(aws_object, expected_status, acceptable_errors = [], tries=60, sleep=5)
233
+ acceptable_errors = [acceptable_errors].flatten
234
+ expected_status = [expected_status].flatten
235
+ current_status = aws_object.status
236
+
237
+ Retryable.retryable(:tries => tries, :sleep => sleep, :on => StatusTimeoutError) do |retries, exception|
238
+ action_handler.report_progress "waited #{retries*sleep}/#{tries*sleep}s for #{aws_object.id} status to change to #{expected_status.inspect}..."
239
+ begin
240
+ current_status = aws_object.status
241
+ unless expected_status.include?(current_status)
242
+ raise StatusTimeoutError.new(aws_object, current_status, expected_status)
243
+ end
244
+ rescue *acceptable_errors
245
+ end
246
+ end
247
+ end
248
+
205
249
  end
206
250
  end