vm_shepherd 1.0.3 → 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.
@@ -1,40 +1,41 @@
1
1
  iaas_type: vcloud
2
- vm_shepherd_configs:
3
- - creds:
4
- url: VAPP_URL
5
- organization: VAPP_ORGANIZATION
6
- user: VAPP_USERNAME
7
- password: VAPP_PASSWORD
8
- vdc:
9
- name: VDC_NAME
10
- catalog: VDC_CATALOG
11
- network: VDC_NETWORK
12
- vapp:
13
- ops_manager_name: VAPP_NAME
14
- product_catalog: PRODUCT_CATALOG
15
- product_names:
16
- - PRODUCT_1
17
- - PRODUCT_2
18
- ip: VAPP_IP
19
- public_ip:
20
- gateway: VAPP_GATEWAY
21
- netmask: VAPP_NETMASK
22
- dns: VAPP_DNS
23
- ntp: VAPP_NTP
24
- - creds:
25
- url: VAPP_URL-2
26
- organization: VAPP_ORGANIZATION-2
27
- user: VAPP_USERNAME-2
28
- password: VAPP_PASSWORD-2
29
- vdc:
30
- name: VDC_NAME-2
31
- catalog: VDC_CATALOG-2
32
- network: VDC_NETWORK-2
33
- vapp:
34
- ops_manager_name: VAPP_NAME-2
35
- ip: VAPP_IP-2
36
- public_ip:
37
- gateway: VAPP_GATEWAY-2
38
- netmask: VAPP_NETMASK-2
39
- dns: VAPP_DNS-2
40
- ntp: VAPP_NTP-2
2
+ vm_shepherd:
3
+ vm_configs:
4
+ - creds:
5
+ url: VAPP_URL
6
+ organization: VAPP_ORGANIZATION
7
+ user: VAPP_USERNAME
8
+ password: VAPP_PASSWORD
9
+ vdc:
10
+ name: VDC_NAME
11
+ catalog: VDC_CATALOG
12
+ network: VDC_NETWORK
13
+ vapp:
14
+ ops_manager_name: VAPP_NAME
15
+ product_catalog: PRODUCT_CATALOG
16
+ product_names:
17
+ - PRODUCT_1
18
+ - PRODUCT_2
19
+ ip: VAPP_IP
20
+ public_ip:
21
+ gateway: VAPP_GATEWAY
22
+ netmask: VAPP_NETMASK
23
+ dns: VAPP_DNS
24
+ ntp: VAPP_NTP
25
+ - creds:
26
+ url: VAPP_URL-2
27
+ organization: VAPP_ORGANIZATION-2
28
+ user: VAPP_USERNAME-2
29
+ password: VAPP_PASSWORD-2
30
+ vdc:
31
+ name: VDC_NAME-2
32
+ catalog: VDC_CATALOG-2
33
+ network: VDC_NETWORK-2
34
+ vapp:
35
+ ops_manager_name: VAPP_NAME-2
36
+ ip: VAPP_IP-2
37
+ public_ip:
38
+ gateway: VAPP_GATEWAY-2
39
+ netmask: VAPP_NETMASK-2
40
+ dns: VAPP_DNS-2
41
+ ntp: VAPP_NTP-2
@@ -1,56 +1,57 @@
1
1
  iaas_type: vsphere
2
- vm_shepherd_configs:
3
- - vcenter_creds:
4
- ip: OVA_URL
5
- username: OVA_ORGANIZATION
6
- password: OVA_PASSWORD
7
- vsphere:
8
- datacenter: VSPHERE_DATACENTER
9
- cluster: VSPHERE_CLUSTER
10
- network: VSPHERE_NETWORK
11
- resource_pool: VSPHERE_RESOURCE_POOL
12
- datastore: VSPHERE_DATASTORE
13
- folder: VSPHERE_FOLDER
14
- vm:
15
- ip: OVA_IP
16
- gateway: OVA_GATEWAY
17
- netmask: OVA_NETMASK
18
- dns: OVA_DNS
19
- ntp_servers: OVA_NTP
20
- cleanup:
21
- datacenter: VSPHERE_OTHER_DATACENTER
22
- datastores:
23
- - VSPHERE_DATASTORE_ONE
24
- - VSPHERE_DATASTORE_TWO
25
- datacenter_folders_to_clean:
26
- - DC_FOLDER_ONE
27
- - DC_FOLDER_TWO
28
- datastore_folders_to_clean:
29
- - DS_DISK_FOLDER
30
- - vcenter_creds:
31
- ip: OVA_URL-2
32
- username: OVA_ORGANIZATION-2
33
- password: OVA_PASSWORD-2
34
- vsphere:
35
- datacenter: VSPHERE_DATACENTER-2
36
- cluster: VSPHERE_CLUSTER-2
37
- network: VSPHERE_NETWORK-2
38
- resource_pool: VSPHERE_RESOURCE_POOL-2
39
- datastore: VSPHERE_DATASTORE-2
40
- folder: VSPHERE_FOLDER-2
41
- vm:
42
- ip: OVA_IP-2
43
- gateway: OVA_GATEWAY-2
44
- netmask: OVA_NETMASK-2
45
- dns: OVA_DNS-2
46
- ntp_servers: OVA_NTP-2
47
- cleanup:
48
- datacenter: VSPHERE_OTHER_DATACENTER-2
49
- datastores:
50
- - VSPHERE_DATASTORE_ONE-2
51
- - VSPHERE_DATASTORE_TWO-2
52
- datacenter_folders_to_clean:
53
- - DC_FOLDER_ONE-2
54
- - DC_FOLDER_TWO-2
55
- datastore_folders_to_clean:
56
- - DS_DISK_FOLDER-2
2
+ vm_shepherd:
3
+ vm_configs:
4
+ - vcenter_creds:
5
+ ip: OVA_URL
6
+ username: OVA_ORGANIZATION
7
+ password: OVA_PASSWORD
8
+ vsphere:
9
+ datacenter: VSPHERE_DATACENTER
10
+ cluster: VSPHERE_CLUSTER
11
+ network: VSPHERE_NETWORK
12
+ resource_pool: VSPHERE_RESOURCE_POOL
13
+ datastore: VSPHERE_DATASTORE
14
+ folder: VSPHERE_FOLDER
15
+ vm:
16
+ ip: OVA_IP
17
+ gateway: OVA_GATEWAY
18
+ netmask: OVA_NETMASK
19
+ dns: OVA_DNS
20
+ ntp_servers: OVA_NTP
21
+ cleanup:
22
+ datacenter: VSPHERE_OTHER_DATACENTER
23
+ datastores:
24
+ - VSPHERE_DATASTORE_ONE
25
+ - VSPHERE_DATASTORE_TWO
26
+ datacenter_folders_to_clean:
27
+ - DC_FOLDER_ONE
28
+ - DC_FOLDER_TWO
29
+ datastore_folders_to_clean:
30
+ - DS_DISK_FOLDER
31
+ - vcenter_creds:
32
+ ip: OVA_URL-2
33
+ username: OVA_ORGANIZATION-2
34
+ password: OVA_PASSWORD-2
35
+ vsphere:
36
+ datacenter: VSPHERE_DATACENTER-2
37
+ cluster: VSPHERE_CLUSTER-2
38
+ network: VSPHERE_NETWORK-2
39
+ resource_pool: VSPHERE_RESOURCE_POOL-2
40
+ datastore: VSPHERE_DATASTORE-2
41
+ folder: VSPHERE_FOLDER-2
42
+ vm:
43
+ ip: OVA_IP-2
44
+ gateway: OVA_GATEWAY-2
45
+ netmask: OVA_NETMASK-2
46
+ dns: OVA_DNS-2
47
+ ntp_servers: OVA_NTP-2
48
+ cleanup:
49
+ datacenter: VSPHERE_OTHER_DATACENTER-2
50
+ datastores:
51
+ - VSPHERE_DATASTORE_ONE-2
52
+ - VSPHERE_DATASTORE_TWO-2
53
+ datacenter_folders_to_clean:
54
+ - DC_FOLDER_ONE-2
55
+ - DC_FOLDER_TWO-2
56
+ datastore_folders_to_clean:
57
+ - DS_DISK_FOLDER-2
@@ -9,38 +9,97 @@ module VmShepherd
9
9
  let(:elastic_ip_id) { 'elastic-ip-id' }
10
10
  let(:ec2) { double('AWS.ec2') }
11
11
 
12
- let(:aws_options) do
12
+ let(:env_config) do
13
13
  {
14
+ stack_name: 'aws-stack-name',
14
15
  aws_access_key: 'aws-access-key',
15
16
  aws_secret_key: 'aws-secret-key',
16
- ssh_key_name: 'ssh-key-name',
17
- security_group_id: 'security-group-id',
18
- public_subnet_id: 'public-subnet-id',
19
- private_subnet_id: 'private-subnet-id',
20
- elastic_ip_id: elastic_ip_id,
21
- vm_name: 'Ops Manager: clean_install_spec'
17
+ json_file: 'cloudformation.json',
18
+ parameters: {
19
+ 'some_parameter' => 'some-answer',
20
+ },
21
+ outputs: {
22
+ ssh_key_name: 'ssh-key-name',
23
+ security_group: 'security-group-id',
24
+ public_subnet_id: 'public-subnet-id',
25
+ private_subnet_id: 'private-subnet-id',
26
+ },
22
27
  }
23
28
  end
24
29
 
25
- subject(:ami_manager) { AwsManager.new(aws_options) }
30
+ let(:vm_config) do
31
+ {
32
+ vm_name: 'some-vm-name',
33
+ }
34
+ end
35
+
36
+ subject(:ami_manager) { AwsManager.new(env_config) }
26
37
 
27
38
  before do
28
39
  expect(AWS).to receive(:config).with(
29
- access_key_id: aws_options.fetch(:aws_access_key),
30
- secret_access_key: aws_options.fetch(:aws_secret_key),
40
+ access_key_id: env_config.fetch(:aws_access_key),
41
+ secret_access_key: env_config.fetch(:aws_secret_key),
31
42
  region: 'us-east-1',
32
43
  )
33
44
 
34
45
  allow(AWS).to receive(:ec2).and_return(ec2)
46
+ allow(ami_manager).to receive(:sleep) # speed up retry logic
47
+ end
48
+
49
+ describe '#prepare_environment' do
50
+ let(:cloudformation_template_file) { Tempfile.new('cloudformation_template_file').tap { |f| f.write('{}'); f.close } }
51
+ let(:cfm) { instance_double(AWS::CloudFormation, stacks: stack_collection) }
52
+ let(:stack) { instance_double(AWS::CloudFormation::Stack, status: 'CREATE_COMPLETE') }
53
+ let(:stack_collection) { instance_double(AWS::CloudFormation::StackCollection) }
54
+
55
+ before do
56
+ allow(AWS::CloudFormation).to receive(:new).and_return(cfm)
57
+ allow(stack_collection).to receive(:create).and_return(stack)
58
+ end
59
+
60
+ it 'creates the stack with the correct parameters' do
61
+ expect(stack_collection).to receive(:create).with(
62
+ 'aws-stack-name',
63
+ '{}',
64
+ parameters: {
65
+ 'some_parameter' => 'some-answer',
66
+ },
67
+ capabilities: ['CAPABILITY_IAM']
68
+ )
69
+ ami_manager.prepare_environment(cloudformation_template_file.path)
70
+ end
71
+
72
+ it 'waits for the stack to finish creating' do
73
+ expect(stack).to receive(:status).and_return('CREATE_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'CREATE_COMPLETE')
74
+
75
+ ami_manager.prepare_environment(cloudformation_template_file.path)
76
+ end
77
+
78
+ it 'stops retrying after 360 times' do
79
+ expect(stack).to receive(:status).and_return('CREATE_IN_PROGRESS').
80
+ exactly(360).times
81
+
82
+ expect { ami_manager.prepare_environment(cloudformation_template_file.path) }.to raise_error(AwsManager::RetryLimitExceeded)
83
+ end
84
+
85
+ it 'aborts if stack fails to create' do
86
+ expect(stack).to receive(:status).and_return('CREATE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_COMPLETE').ordered
87
+ expect(stack).to receive(:delete)
88
+ expect {
89
+ ami_manager.prepare_environment(cloudformation_template_file.path)
90
+ }.to raise_error('Unexpected status for stack aws-stack-name : ROLLBACK_COMPLETE')
91
+ end
35
92
  end
36
93
 
37
94
  describe '#deploy' do
38
95
  let(:instance) { instance_double(AWS::EC2::Instance, status: :running, associate_elastic_ip: nil, add_tag: nil) }
96
+ let(:elastic_ip) { instance_double(AWS::EC2::ElasticIp, allocation_id: 'allocation-id') }
39
97
  let(:instances) { instance_double(AWS::EC2::InstanceCollection, create: instance) }
98
+ let(:elastic_ips) { instance_double(AWS::EC2::ElasticIpCollection, create: elastic_ip) }
40
99
 
41
100
  before do
42
101
  allow(ec2).to receive(:instances).and_return(instances)
43
- allow(ami_manager).to receive(:sleep) # speed up retry logic
102
+ allow(ec2).to receive(:elastic_ips).and_return(elastic_ips)
44
103
  end
45
104
 
46
105
  it 'creates an instance using AWS SDK v1' do
@@ -48,10 +107,10 @@ module VmShepherd
48
107
  image_id: ami_id,
49
108
  key_name: 'ssh-key-name',
50
109
  security_group_ids: ['security-group-id'],
51
- subnet: aws_options.fetch(:public_subnet_id),
110
+ subnet: 'public-subnet-id',
52
111
  instance_type: 'm3.medium').and_return(instance)
53
112
 
54
- ami_manager.deploy(ami_file_path)
113
+ ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config)
55
114
  end
56
115
 
57
116
  context 'when the ip address is in use' do
@@ -59,21 +118,21 @@ module VmShepherd
59
118
  expect(instances).to receive(:create).and_raise(AWS::EC2::Errors::InvalidIPAddress::InUse).once
60
119
  expect(instances).to receive(:create).and_return(instance).once
61
120
 
62
- ami_manager.deploy(ami_file_path)
121
+ ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config)
63
122
  end
64
123
 
65
124
  it 'stops retrying after 60 times' do
66
125
  expect(instances).to receive(:create).and_raise(AWS::EC2::Errors::InvalidIPAddress::InUse).
67
126
  exactly(AwsManager::RETRY_LIMIT).times
68
127
 
69
- expect { ami_manager.deploy(ami_file_path) }.to raise_error(AwsManager::RetryLimitExceeded)
128
+ expect { ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config) }.to raise_error(AwsManager::RetryLimitExceeded)
70
129
  end
71
130
  end
72
131
 
73
132
  it 'does not return until the instance is running' do
74
133
  expect(instance).to receive(:status).and_return(:pending, :pending, :pending, :running)
75
134
 
76
- ami_manager.deploy(ami_file_path)
135
+ ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config)
77
136
  end
78
137
 
79
138
  it 'handles API endpoints not knowing (right away) about the instance created' do
@@ -81,26 +140,29 @@ module VmShepherd
81
140
  exactly(AwsManager::RETRY_LIMIT - 1).times
82
141
  expect(instance).to receive(:status).and_return(:running).once
83
142
 
84
- ami_manager.deploy(ami_file_path)
143
+ ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config)
85
144
  end
86
145
 
87
146
  it 'stops retrying after 60 times' do
88
147
  expect(instance).to receive(:status).and_return(:pending).
89
148
  exactly(AwsManager::RETRY_LIMIT).times
90
149
 
91
- expect { ami_manager.deploy(ami_file_path) }.to raise_error(AwsManager::RetryLimitExceeded)
150
+ expect { ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config) }.to raise_error(AwsManager::RetryLimitExceeded)
92
151
  end
93
152
 
94
- it 'attaches the elastic IP' do
95
- expect(instance).to receive(:associate_elastic_ip).with(aws_options.fetch(:elastic_ip_id))
153
+ it 'creates and attaches an elastic IP' do
154
+ expect(ec2).to receive_message_chain(:elastic_ips, :create).with(
155
+ vpc: true).and_return(elastic_ip)
96
156
 
97
- ami_manager.deploy(ami_file_path)
157
+ expect(instance).to receive(:associate_elastic_ip).with(elastic_ip.allocation_id)
158
+
159
+ ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config)
98
160
  end
99
161
 
100
162
  it 'tags the instance with a name' do
101
- expect(instance).to receive(:add_tag).with('Name', value: aws_options.fetch(:vm_name))
163
+ expect(instance).to receive(:add_tag).with('Name', value: 'some-vm-name')
102
164
 
103
- ami_manager.deploy(ami_file_path)
165
+ ami_manager.deploy(ami_file_path: ami_file_path, vm_config: vm_config)
104
166
  end
105
167
  end
106
168
 
@@ -112,19 +174,28 @@ module VmShepherd
112
174
  let(:instance2) { instance_double(AWS::EC2::Instance, tags: {}) }
113
175
  let(:subnet1_instances) { [instance1] }
114
176
  let(:subnet2_instances) { [instance2] }
177
+ let(:cfm) { instance_double(AWS::CloudFormation, stacks: stack_collection) }
178
+ let(:stack) { instance_double(AWS::CloudFormation::Stack, status: 'DELETE_COMPLETE', delete: nil) }
179
+ let(:stack_collection) { instance_double(AWS::CloudFormation::StackCollection) }
115
180
 
116
- let(:instance1_volume) { instance_double(AWS::EC2::Volume)}
181
+ let(:instance1_volume) { instance_double(AWS::EC2::Volume) }
117
182
  let(:instance1_attachment) do
118
183
  instance_double(AWS::EC2::Attachment, volume: instance1_volume, delete_on_termination: true)
119
184
  end
120
185
 
121
186
  before do
187
+ allow(AWS::CloudFormation).to receive(:new).and_return(cfm)
188
+ allow(stack_collection).to receive(:[]).and_return(stack)
189
+
122
190
  allow(ec2).to receive(:subnets).and_return(subnets)
123
191
  allow(subnets).to receive(:[]).with('public-subnet-id').and_return(subnet1)
124
192
  allow(subnets).to receive(:[]).with('private-subnet-id').and_return(subnet2)
125
193
 
126
194
  allow(instance1).to receive(:attachments).and_return({'/dev/test' => instance1_attachment})
127
195
  allow(instance2).to receive(:attachments).and_return({})
196
+
197
+ allow(instance1).to receive(:terminate)
198
+ allow(instance2).to receive(:terminate)
128
199
  end
129
200
 
130
201
  it 'terminates all VMs in the subnet' do
@@ -134,20 +205,46 @@ module VmShepherd
134
205
  ami_manager.clean_environment
135
206
  end
136
207
 
137
- context 'when an instance has the magical tag' do
138
- let(:persistent_instance) { instance_double(AWS::EC2::Instance, tags: persist_tag) }
139
- let(:instances) { [instance1, instance2, persistent_instance] }
208
+ it 'deletes the stack' do
209
+ expect(stack_collection).to receive(:[]).with('aws-stack-name').and_return(stack)
210
+ expect(stack).to receive(:delete)
211
+ ami_manager.clean_environment
212
+ end
140
213
 
141
- context 'when the do not terminate tag is present' do
142
- let(:persist_tag) { { AwsManager::DO_NOT_TERMINATE_TAG_KEY => 'any value' } }
143
- it 'does not attempt to terminate this instance' do
144
- expect(instance1).to receive(:terminate)
145
- expect(instance2).to receive(:terminate)
146
- expect(persistent_instance).not_to receive(:terminate)
214
+ it 'waits for stack deletion to complete' do
215
+ expect(stack).to receive(:status).and_return('DELETE_IN_PROGRESS', 'DELETE_IN_PROGRESS', 'DELETE_IN_PROGRESS', 'DELETE_COMPLETE')
147
216
 
148
- ami_manager.clean_environment
149
- end
150
- end
217
+ ami_manager.clean_environment
218
+ end
219
+
220
+ it 'stops retrying after 360 times' do
221
+ expect(stack).to receive(:status).and_return('DELETE_IN_PROGRESS').
222
+ exactly(360).times
223
+
224
+ expect { ami_manager.clean_environment }.to raise_error(AwsManager::RetryLimitExceeded)
225
+ end
226
+
227
+ it 'aborts if stack reports unexpected status' do
228
+ expect(stack).to receive(:status).and_return('DELETE_IN_PROGRESS', 'UNEXPECTED_STATUS').ordered
229
+ expect {
230
+ ami_manager.clean_environment
231
+ }.to raise_error('Unexpected status for stack aws-stack-name : UNEXPECTED_STATUS')
232
+ end
233
+
234
+ it 'aborts if stack throws error' do
235
+ expect(stack).to receive(:status).and_raise(AWS::CloudFormation::Errors::ValidationError)
236
+ allow(stack).to receive(:exists?).and_return(true)
237
+ expect {
238
+ ami_manager.clean_environment
239
+ }.to raise_error(AWS::CloudFormation::Errors::ValidationError)
240
+ end
241
+
242
+ it 'succeeds if stack throws error and stack deletion has completed' do
243
+ expect(stack).to receive(:status).and_raise(AWS::CloudFormation::Errors::ValidationError)
244
+ allow(stack).to receive(:exists?).and_return(false)
245
+ expect {
246
+ ami_manager.clean_environment
247
+ }.not_to raise_error
151
248
  end
152
249
 
153
250
  context 'when the instance has volumes that are NOT delete_on_termination' do
@@ -181,33 +278,43 @@ module VmShepherd
181
278
  end
182
279
 
183
280
  describe '#destroy' do
184
- let(:elastic_ips) { instance_double(AWS::EC2::ElasticIpCollection) }
185
- let(:elastic_ip) { instance_double(AWS::EC2::ElasticIp, instance: instance, allocation_id: elastic_ip_id) }
186
- let(:instance) { instance_double(AWS::EC2::Instance, tags: {}) }
281
+ let(:elastic_ip) { nil }
282
+ let(:instance) { instance_double(AWS::EC2::Instance, tags: {'Name' => 'some-vm-name'}, elastic_ip: elastic_ip) }
283
+ let(:non_terminated_instance) { instance_double(AWS::EC2::Instance, tags: {}) }
284
+ let(:instances) { [non_terminated_instance, instance] }
187
285
 
188
286
  before do
189
- allow(ec2).to receive(:elastic_ips).and_return(elastic_ips)
190
- allow(elastic_ips).to receive(:each).and_yield(elastic_ip)
287
+ allow(ec2).to receive(:instances).and_return(instances)
288
+ allow(instance).to receive(:terminate)
191
289
  end
192
290
 
193
- it 'terminates the VM that matches the IP' do
291
+ it 'terminates the VM with the specified name' do
292
+ expect(non_terminated_instance).not_to receive(:terminate)
194
293
  expect(instance).to receive(:terminate)
195
294
 
196
- ami_manager.destroy
295
+ ami_manager.destroy(vm_config)
197
296
  end
198
297
 
199
- context 'when an instance has the magical tag' do
200
- let(:persistent_instance) { instance_double(AWS::EC2::Instance, tags: persist_tag) }
201
- let(:instances) { [instance1, instance2, persistent_instance] }
298
+ context 'when there is an elastic ip' do
299
+ let(:elastic_ip) { instance_double(AWS::EC2::ElasticIp) }
202
300
 
203
- context 'when there is no instance attached' do
204
- before do
205
- allow(elastic_ip).to receive(:instance).and_return(nil)
206
- end
301
+ before do
302
+ allow(elastic_ip).to receive(:delete)
303
+ allow(elastic_ip).to receive(:disassociate)
304
+ end
207
305
 
208
- it 'does not explode' do
209
- ami_manager.destroy
210
- end
306
+ it 'terminates the VM with the specified name' do
307
+ expect(non_terminated_instance).not_to receive(:terminate)
308
+ expect(instance).to receive(:terminate)
309
+
310
+ ami_manager.destroy(vm_config)
311
+ end
312
+
313
+ it 'disassociates and deletes the ip associated with the terminated vm' do
314
+ expect(elastic_ip).to receive(:disassociate).ordered
315
+ expect(elastic_ip).to receive(:delete).ordered
316
+
317
+ ami_manager.destroy(vm_config)
211
318
  end
212
319
  end
213
320
  end