vm_shepherd 1.0.3 → 1.1.0

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