chef-provisioning-aws 1.0.4 → 1.1.0

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