vm_shepherd 0.0.1 → 0.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,3 +1,3 @@
1
1
  module VmShepherd
2
- VERSION = '0.0.1'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
@@ -0,0 +1,293 @@
1
+ require 'logger'
2
+ require 'rbvmomi'
3
+
4
+ module VmShepherd
5
+ class VsphereManager
6
+ TEMPLATE_PREFIX = 'tpl'.freeze
7
+
8
+ def initialize(host, username, password, datacenter_name)
9
+ @host = host
10
+ @username = username
11
+ @password = password
12
+ @datacenter_name = datacenter_name
13
+ @logger = Logger.new(STDERR)
14
+ end
15
+
16
+ def deploy(ova_path, vm_config, vsphere_config)
17
+ raise ArgumentError unless folder_name_is_valid?(vsphere_config[:folder])
18
+
19
+ ova_path = File.expand_path(ova_path.strip)
20
+ ensure_no_running_vm(vm_config)
21
+
22
+ tmp_dir = untar_vbox_ova(ova_path)
23
+ ovf_file_path = ovf_file_path_from_dir(tmp_dir)
24
+
25
+ template = deploy_ovf_template(ovf_file_path, vsphere_config)
26
+ vm = create_vm_from_template(template, vsphere_config)
27
+
28
+ reconfigure_vm(vm, vm_config)
29
+ power_on_vm(vm)
30
+ ensure
31
+ FileUtils.remove_entry_secure(ovf_file_path, force: true) unless ovf_file_path.nil?
32
+ end
33
+
34
+ def destroy(folder_name)
35
+ fail("#{folder_name.inspect} is not a valid folder name") unless folder_name_is_valid?(folder_name)
36
+
37
+ delete_folder_and_vms(folder_name)
38
+
39
+ fail("#{folder_name.inspect} already exists") unless datacenter.vmFolder.traverse(folder_name).nil?
40
+
41
+ datacenter.vmFolder.traverse(folder_name, RbVmomi::VIM::Folder, true)
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :host, :username, :password, :datacenter_name, :logger
47
+
48
+ def delete_folder_and_vms(folder_name)
49
+ return unless (folder = datacenter.vmFolder.traverse(folder_name))
50
+
51
+ find_vms(folder).each { |vm| power_off(vm) }
52
+
53
+ logger.info("BEGIN folder.destroy_task folder=#{folder_name}")
54
+ folder.Destroy_Task.wait_for_completion
55
+ logger.info("END folder.destroy_task folder=#{folder_name}")
56
+ rescue RbVmomi::Fault => e
57
+ logger.info("ERROR folder.destroy_task folder=#{folder_name}", e)
58
+ raise
59
+ end
60
+
61
+ def find_vms(folder)
62
+ vms = folder.childEntity.grep(RbVmomi::VIM::VirtualMachine)
63
+ vms << folder.childEntity.grep(RbVmomi::VIM::Folder).map { |child| find_vms(child) }
64
+ vms.flatten
65
+ end
66
+
67
+ def power_off(vm)
68
+ 2.times do
69
+ break if vm.runtime.powerState == 'poweredOff'
70
+
71
+ begin
72
+ logger.info("BEGIN vm.power_off_task vm=#{vm.name}, power_state=#{vm.runtime.powerState}")
73
+ vm.PowerOffVM_Task.wait_for_completion
74
+ logger.info("END vm.power_off_task vm=#{vm.name}")
75
+ rescue StandardError => e
76
+ logger.info("ERROR vm.power_off_task vm=#{vm.name}")
77
+ raise unless e.message.start_with?('InvalidPowerState')
78
+ end
79
+ end
80
+ end
81
+
82
+ def folder_name_is_valid?(folder_name)
83
+ /\A([\w-]{1,80}\/)*[\w-]{1,80}\/?\z/.match(folder_name)
84
+ end
85
+
86
+ def ensure_no_running_vm(ova_config)
87
+ logger.info('--- Running: Checking for existing VM')
88
+ ip = ova_config[:external_ip] || ova_config[:ip]
89
+ port = ova_config[:external_port] || 443
90
+ fail("VM exists at #{ip}") if system("nc -z -w 5 #{ip} #{port}")
91
+ end
92
+
93
+ def untar_vbox_ova(ova_path)
94
+ logger.info("--- Running: Untarring #{ova_path}")
95
+ Dir.mktmpdir.tap do |dir|
96
+ system_or_exit("cd #{dir} && tar xfv '#{ova_path}'")
97
+ end
98
+ end
99
+
100
+ def ovf_file_path_from_dir(dir)
101
+ Dir["#{dir}/*.ovf"].first || fail('Failed to find ovf')
102
+ end
103
+
104
+ def deploy_ovf_template(ovf_file_path, vsphere_config)
105
+ template_name = [TEMPLATE_PREFIX, Time.new.strftime('%F-%H-%M'), cluster(vsphere_config).name].join('-')
106
+ logger.info("BEGIN deploy_ovf ovf_file=#{ovf_file_path} template_name=#{template_name}")
107
+ connection.serviceContent.ovfManager.deployOVF(
108
+ uri: ovf_file_path,
109
+ vmName: template_name,
110
+ vmFolder: target_folder(vsphere_config),
111
+ host: find_deploy_host(vsphere_config),
112
+ resourcePool: resource_pool(vsphere_config),
113
+ datastore: datastore(vsphere_config),
114
+ networkMappings: create_network_mappings(ovf_file_path, vsphere_config),
115
+ propertyMappings: {},
116
+ ).tap do |ovf_template|
117
+ ovf_template.add_delta_disk_layer_on_all_disks
118
+ ovf_template.MarkAsTemplate
119
+ end
120
+ end
121
+
122
+ def find_deploy_host(vsphere_config)
123
+ property_collector = connection.serviceContent.propertyCollector
124
+
125
+ hosts = cluster(vsphere_config).host
126
+ host_properties_by_host =
127
+ property_collector.collectMultiple(
128
+ hosts,
129
+ 'datastore',
130
+ 'runtime.connectionState',
131
+ 'runtime.inMaintenanceMode',
132
+ 'name',
133
+ )
134
+
135
+ hosts.shuffle.find do |host|
136
+ (host_properties_by_host[host]['runtime.connectionState'] == 'connected') && # connected
137
+ host_properties_by_host[host]['datastore'].member?(datastore(vsphere_config)) && # must have the destination datastore
138
+ !host_properties_by_host[host]['runtime.inMaintenanceMode'] #not be in maintenance mode
139
+ end || fail('ERROR finding host to upload OVF to')
140
+ end
141
+
142
+ def create_network_mappings(ovf_file_path, vsphere_config)
143
+ ovf = Nokogiri::XML(File.read(ovf_file_path))
144
+ ovf.remove_namespaces!
145
+ networks = ovf.xpath('//NetworkSection/Network').map { |x| x['name'] }
146
+ Hash[networks.map { |ovf_network| [ovf_network, network(vsphere_config)] }]
147
+ end
148
+
149
+ def create_vm_from_template(template, vsphere_config)
150
+ logger.info("BEGIN clone_vm_task tempalte=#{template.name}")
151
+ template.CloneVM_Task(
152
+ folder: target_folder(vsphere_config),
153
+ name: "#{template.name}-vm",
154
+ spec: {
155
+ location: {
156
+ pool: resource_pool(vsphere_config),
157
+ datastore: datastore(vsphere_config),
158
+ diskMoveType: :moveChildMostDiskBacking,
159
+ },
160
+ powerOn: false,
161
+ template: false,
162
+ config: {numCPUs: 2, memoryMB: 2048},
163
+ }
164
+ ).wait_for_completion
165
+ logger.info("END clone_vm_task tempalte=#{template.name}")
166
+ end
167
+
168
+ def reconfigure_vm(vm, vm_config)
169
+ virtual_machine_config_spec = create_virtual_machine_config_spec(vm_config)
170
+ logger.info("BEGIN reconfigure_vm_task virtual_machine_cofig_spec=#{virtual_machine_config_spec.inspect}")
171
+ vm.ReconfigVM_Task(spec: virtual_machine_config_spec).wait_for_completion
172
+ logger.info("END reconfigure_vm_task virtual_machine_cofig_spec=#{virtual_machine_config_spec.inspect}")
173
+ end
174
+
175
+ def create_virtual_machine_config_spec(vm_config)
176
+ logger.info('BEGIN VmConfigSpec creation')
177
+ vm_config_spec = RbVmomi::VIM::VmConfigSpec.new
178
+ vm_config_spec.ovfEnvironmentTransport = ['com.vmware.guestInfo']
179
+ vm_config_spec.property = create_vapp_property_specs(vm_config)
180
+ logger.info("END VmConfigSpec creation: #{vm_config_spec.inspect}")
181
+
182
+ logger.info('BEGIN VirtualMachineConfigSpec creation')
183
+ virtual_machine_config_spec = RbVmomi::VIM::VirtualMachineConfigSpec.new
184
+ virtual_machine_config_spec.vAppConfig = vm_config_spec
185
+ logger.info("END VirtualMachineConfigSpec creation #{virtual_machine_config_spec.inspect}")
186
+ virtual_machine_config_spec
187
+ end
188
+
189
+ def create_vapp_property_specs(vm_config)
190
+ ip_configuration = {
191
+ 'ip0' => vm_config[:ip],
192
+ 'netmask0' => vm_config[:netmask],
193
+ 'gateway' => vm_config[:gateway],
194
+ 'DNS' => vm_config[:dns],
195
+ 'ntp_servers' => vm_config[:ntp_servers],
196
+ }
197
+
198
+ vapp_property_specs = []
199
+
200
+ logger.info("BEGIN VAppPropertySpec creation configuration=#{ip_configuration.inspect}")
201
+ # IP Configuration key order must match OVF template property order
202
+ ip_configuration.each_with_index do |(key, value), i|
203
+ vapp_property_specs << RbVmomi::VIM::VAppPropertySpec.new.tap do |spec|
204
+ spec.operation = 'edit'
205
+ spec.info = RbVmomi::VIM::VAppPropertyInfo.new.tap do |p|
206
+ p.key = i
207
+ p.label = key
208
+ p.value = value
209
+ end
210
+ end
211
+ end
212
+
213
+ vapp_property_specs << RbVmomi::VIM::VAppPropertySpec.new.tap do |spec|
214
+ spec.operation = 'edit'
215
+ spec.info = RbVmomi::VIM::VAppPropertyInfo.new.tap do |p|
216
+ p.key = ip_configuration.length
217
+ p.label = 'admin_password'
218
+ p.value = vm_config[:vm_password]
219
+ end
220
+ end
221
+ logger.info("END VAppPropertySpec creation vapp_property_specs=#{vapp_property_specs.inspect}")
222
+ vapp_property_specs
223
+ end
224
+
225
+ def power_on_vm(vm)
226
+ logger.info('BEGIN power_on_vm_task')
227
+ vm.PowerOnVM_Task.wait_for_completion
228
+ logger.info('END power_on_vm_task')
229
+
230
+ Timeout.timeout(7*60) do
231
+ until vm.guest_ip
232
+ logger.info('BEGIN polling for VM IP address')
233
+ sleep 30
234
+ end
235
+ logger.info("END polling for VM IP address #{vm.guest_ip.inspect}")
236
+ end
237
+ end
238
+
239
+ def connection
240
+ RbVmomi::VIM.connect(
241
+ host: host,
242
+ user: username,
243
+ password: password,
244
+ ssl: true,
245
+ insecure: true,
246
+ )
247
+ end
248
+
249
+ def datacenter
250
+ connection.searchIndex.FindByInventoryPath(inventoryPath: datacenter_name).tap do |dc|
251
+ fail("ERROR finding datacenter #{datacenter_name.inspect}") unless dc.is_a?(RbVmomi::VIM::Datacenter)
252
+ end
253
+ end
254
+
255
+ def target_folder(vsphere_config)
256
+ datacenter.vmFolder.traverse(vsphere_config[:folder], RbVmomi::VIM::Folder, true)
257
+ end
258
+
259
+ def cluster(vsphere_config)
260
+ datacenter.find_compute_resource(vsphere_config[:cluster]) ||
261
+ fail("ERROR finding cluster #{vsphere_config[:cluster].inspect}")
262
+ end
263
+
264
+ def network(vsphere_config)
265
+ datacenter.networkFolder.traverse(vsphere_config[:network]) ||
266
+ fail("ERROR finding network #{vsphere_config[:network].inspect}")
267
+ end
268
+
269
+ def resource_pool(vsphere_config)
270
+ find_resource_pool(cluster(vsphere_config), vsphere_config[:resource_pool]) ||
271
+ fail("ERROR finding resource_pool #{vsphere_config[:resource_pool].inspect}")
272
+ end
273
+
274
+ def datastore(vsphere_config)
275
+ datacenter.find_datastore(vsphere_config[:datastore]) ||
276
+ fail("ERROR finding datastore #{vsphere_config[:datastore].inspect}")
277
+ end
278
+
279
+ def find_resource_pool(cluster, resource_pool_name)
280
+ if resource_pool_name
281
+ cluster.resourcePool.resourcePool.find { |rp| rp.name == resource_pool_name }
282
+ else
283
+ cluster.resourcePool
284
+ end
285
+ end
286
+
287
+ def system_or_exit(command)
288
+ logger.info("BEGIN running #{command.inspect}")
289
+ system(command) || fail("ERROR running #{command.inspect}")
290
+ logger.info("END running #{command.inspect}")
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,19 @@
1
+ iaas_type: openstack
2
+ vm_deployer:
3
+ creds:
4
+ auth_url: 'http://example.com/version/tokens'
5
+ username: 'username'
6
+ api_key: 'api-key'
7
+ tenant: 'tenant'
8
+ vm:
9
+ name: 'some-vm-name'
10
+ flavor_parameters:
11
+ min_disk_size: 150
12
+ network_name: 'some-network'
13
+ key_name: 'some-key'
14
+ security_group_names:
15
+ - 'security-group-A'
16
+ - 'security-group-B'
17
+ - 'security-group-C'
18
+ public_ip: 198.11.195.5
19
+ private_ip: 192.168.100.100
@@ -0,0 +1,20 @@
1
+ require 'fog/openstack/requests/image/create_image'
2
+ require 'fog/openstack/requests/image/delete_image'
3
+ require 'fog/openstack/requests/image/list_public_images_detailed'
4
+
5
+ module PatchedFog
6
+ def self.included(spec)
7
+ spec.before do
8
+ stub_const('::Fog::Image::OpenStack::Mock', PatchedFog::ImageMock)
9
+ stub_const('::Fog::Time', ::Time)
10
+ end
11
+ end
12
+
13
+ class ImageMock < Fog::Image::OpenStack::Mock
14
+ def delete_image(image_id)
15
+ # Temporarily do what Fog's mock should have done–keep state of images up to date.
16
+ self.data[:images].delete(image_id)
17
+ super(image_id)
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,7 @@
1
- require 'vm_shepherd/ami_manager'
1
+ require 'vm_shepherd/aws_manager'
2
2
 
3
3
  module VmShepherd
4
- RSpec.describe AmiManager do
4
+ RSpec.describe AwsManager do
5
5
  let(:access_key) { 'access-key' }
6
6
  let(:secret_key) { 'secret-key' }
7
7
  let(:ami_id) { 'ami-deadbeef' }
@@ -21,7 +21,7 @@ module VmShepherd
21
21
  }
22
22
  end
23
23
 
24
- subject(:ami_manager) { AmiManager.new(aws_options) }
24
+ subject(:ami_manager) { AwsManager.new(aws_options) }
25
25
 
26
26
  before do
27
27
  expect(AWS).to receive(:config).with(
@@ -48,7 +48,6 @@ module VmShepherd
48
48
  key_name: 'ssh-key-name',
49
49
  security_group_ids: ['security-group-id'],
50
50
  subnet: aws_options.fetch(:public_subnet_id),
51
- private_ip_address: AmiManager::OPS_MANAGER_PRIVATE_IP,
52
51
  instance_type: 'm3.medium').and_return(instance)
53
52
 
54
53
  ami_manager.deploy(ami_file_path)
@@ -64,9 +63,9 @@ module VmShepherd
64
63
 
65
64
  it 'stops retrying after 60 times' do
66
65
  expect(instances).to receive(:create).and_raise(AWS::EC2::Errors::InvalidIPAddress::InUse).
67
- exactly(AmiManager::RETRY_LIMIT).times
66
+ exactly(AwsManager::RETRY_LIMIT).times
68
67
 
69
- expect { ami_manager.deploy(ami_file_path) }.to raise_error(AmiManager::RetryLimitExceeded)
68
+ expect { ami_manager.deploy(ami_file_path) }.to raise_error(AwsManager::RetryLimitExceeded)
70
69
  end
71
70
  end
72
71
 
@@ -78,7 +77,7 @@ module VmShepherd
78
77
 
79
78
  it 'handles API endpoints not knowing (right away) about the instance created' do
80
79
  expect(instance).to receive(:status).and_raise(AWS::EC2::Errors::InvalidInstanceID::NotFound).
81
- exactly(AmiManager::RETRY_LIMIT - 1).times
80
+ exactly(AwsManager::RETRY_LIMIT - 1).times
82
81
  expect(instance).to receive(:status).and_return(:running).once
83
82
 
84
83
  ami_manager.deploy(ami_file_path)
@@ -86,9 +85,9 @@ module VmShepherd
86
85
 
87
86
  it 'stops retrying after 60 times' do
88
87
  expect(instance).to receive(:status).and_return(:pending).
89
- exactly(AmiManager::RETRY_LIMIT).times
88
+ exactly(AwsManager::RETRY_LIMIT).times
90
89
 
91
- expect { ami_manager.deploy(ami_file_path) }.to raise_error(AmiManager::RetryLimitExceeded)
90
+ expect { ami_manager.deploy(ami_file_path) }.to raise_error(AwsManager::RetryLimitExceeded)
92
91
  end
93
92
 
94
93
  it 'attaches the elastic IP' do
@@ -131,7 +130,7 @@ module VmShepherd
131
130
  let(:instances) { [instance1, instance2, persistent_instance] }
132
131
 
133
132
  context 'when the do not terminate tag is present' do
134
- let(:persist_tag) { { AmiManager::DO_NOT_TERMINATE_TAG_KEY => 'any value' } }
133
+ let(:persist_tag) { { AwsManager::DO_NOT_TERMINATE_TAG_KEY => 'any value' } }
135
134
  it 'does not attempt to terminate this instance' do
136
135
  expect(instance1).to receive(:terminate)
137
136
  expect(instance2).to receive(:terminate)
@@ -0,0 +1,237 @@
1
+ require 'vm_shepherd/openstack_manager'
2
+ require 'support/patched_fog'
3
+
4
+ module VmShepherd
5
+ RSpec.describe OpenstackManager do
6
+ include PatchedFog
7
+
8
+ let(:openstack_options) do
9
+ {
10
+ auth_url: 'http://example.com/version/tokens',
11
+ username: 'username',
12
+ api_key: 'api-key',
13
+ tenant: 'tenant',
14
+ }
15
+ end
16
+ let(:openstack_vm_options) do
17
+ {
18
+ name: 'some-vm-name',
19
+ min_disk_size: 150,
20
+ network_name: 'Public',
21
+ key_name: 'some-key',
22
+ security_group_names: [
23
+ 'security-group-A',
24
+ 'security-group-B',
25
+ 'security-group-C',
26
+ ],
27
+ public_ip: '192.168.27.129', #magik ip to Fog::Mock
28
+ private_ip: '192.168.27.100',
29
+ }
30
+ end
31
+
32
+ subject(:openstack_vm_manager) { OpenstackManager.new(openstack_options) }
33
+
34
+ describe '#service' do
35
+ it 'creates a Fog::Compute connection' do
36
+ expect(Fog::Compute).to receive(:new).with(
37
+ {
38
+ provider: 'openstack',
39
+ openstack_auth_url: openstack_options[:auth_url],
40
+ openstack_username: openstack_options[:username],
41
+ openstack_tenant: openstack_options[:tenant],
42
+ openstack_api_key: openstack_options[:api_key],
43
+ }
44
+ )
45
+ openstack_vm_manager.service
46
+ end
47
+ end
48
+
49
+ describe '#image_service' do
50
+ it 'creates a Fog::Image connection' do
51
+ expect(Fog::Image).to receive(:new).with(
52
+ {
53
+ provider: 'openstack',
54
+ openstack_auth_url: openstack_options[:auth_url],
55
+ openstack_username: openstack_options[:username],
56
+ openstack_tenant: openstack_options[:tenant],
57
+ openstack_api_key: openstack_options[:api_key],
58
+ openstack_endpoint_type: 'publicURL',
59
+ }
60
+ )
61
+ openstack_vm_manager.image_service
62
+ end
63
+ end
64
+
65
+ describe '#network_service' do
66
+ it 'creates a Fog::Network connection' do
67
+ expect(Fog::Network).to receive(:new).with(
68
+ {
69
+ provider: 'openstack',
70
+ openstack_auth_url: openstack_options[:auth_url],
71
+ openstack_username: openstack_options[:username],
72
+ openstack_tenant: openstack_options[:tenant],
73
+ openstack_api_key: openstack_options[:api_key],
74
+ openstack_endpoint_type: 'publicURL',
75
+ }
76
+ )
77
+ openstack_vm_manager.network_service
78
+ end
79
+ end
80
+
81
+ describe '#deploy' do
82
+ let(:path) { 'path/to/qcow2/file' }
83
+ let(:file_size) { 42 }
84
+
85
+ let(:compute_service) { openstack_vm_manager.service }
86
+ let(:image_service) { openstack_vm_manager.image_service }
87
+ let(:network_service) { openstack_vm_manager.network_service }
88
+
89
+ let(:servers) { compute_service.servers }
90
+ let(:addresses) { compute_service.addresses }
91
+ let(:instance) { servers.find { |server| server.name == openstack_vm_options[:name] } }
92
+
93
+ before do
94
+ allow(File).to receive(:size).with(path).and_return(file_size)
95
+ allow(openstack_vm_manager).to receive(:say)
96
+
97
+ Fog.mock!
98
+ Fog::Mock.reset
99
+ Fog::Mock.delay = 0
100
+
101
+ allow(compute_service).to receive(:servers).and_return(servers)
102
+ allow(compute_service).to receive(:addresses).and_return(addresses)
103
+ end
104
+
105
+ it 'uploads the image' do
106
+ file_size = 2
107
+ expect(File).to receive(:size).with(path).and_return(file_size)
108
+
109
+ openstack_vm_manager.deploy(path, openstack_vm_options)
110
+
111
+ uploaded_image = image_service.images.find { |image| image.name == openstack_vm_options[:name] }
112
+ expect(uploaded_image).to be
113
+ expect(uploaded_image.size).to eq(file_size)
114
+ end
115
+
116
+ context 'when launching an instance' do
117
+ it 'launches an image instance' do
118
+ openstack_vm_manager.deploy(path, openstack_vm_options)
119
+
120
+ expect(instance).to be
121
+ end
122
+
123
+ it 'uses the correct flavor for the instance' do
124
+ openstack_vm_manager.deploy(path, openstack_vm_options)
125
+
126
+ instance_flavor = compute_service.flavors.find { |flavor| flavor.id == instance.flavor['id'] }
127
+ expect(instance_flavor.disk).to be >= 150
128
+ end
129
+
130
+ it 'uses the previously uploaded image' do
131
+ openstack_vm_manager.deploy(path, openstack_vm_options)
132
+
133
+ instance_image = image_service.images.get instance.image['id']
134
+ expect(instance_image.name).to eq(openstack_vm_options[:name])
135
+ end
136
+
137
+ it 'assigns the correct key_name to the instance' do
138
+ expect(servers).to receive(:create).with(
139
+ hash_including(:key_name => openstack_vm_options[:key_name])
140
+ ).and_call_original
141
+
142
+ openstack_vm_manager.deploy(path, openstack_vm_options)
143
+ end
144
+
145
+ it 'assigns the correct security groups' do
146
+ expect(servers).to receive(:create).with(
147
+ hash_including(:security_groups => openstack_vm_options[:security_group_names])
148
+ ).and_call_original
149
+
150
+ openstack_vm_manager.deploy(path, openstack_vm_options)
151
+ end
152
+
153
+ it 'assigns the correct private network information' do
154
+ assigned_network = network_service.networks.find { |network| network.name == openstack_vm_options[:network_name] }
155
+ expect(servers).to receive(:create).with(
156
+ hash_including(:nics => [
157
+ { net_id: assigned_network.id, v4_fixed_ip: openstack_vm_options[:private_ip]}
158
+ ]
159
+ )
160
+ ).and_call_original
161
+
162
+ openstack_vm_manager.deploy(path, openstack_vm_options)
163
+ end
164
+ end
165
+
166
+ it 'waits for the server to be ready' do
167
+ openstack_vm_manager.deploy(path, openstack_vm_options)
168
+ expect(instance.state).to eq('ACTIVE')
169
+ end
170
+
171
+ it 'assigns an IP to the instance' do
172
+ openstack_vm_manager.deploy(path, openstack_vm_options)
173
+ ip = addresses.find { |address| address.ip == openstack_vm_options[:public_ip] }
174
+
175
+ expect(ip.instance_id).to eq(instance.id)
176
+ end
177
+ end
178
+
179
+ describe '#destroy' do
180
+ let(:path) { 'path/to/qcow2/file' }
181
+ let(:file_size) { 42 }
182
+
183
+ let(:compute_service) { openstack_vm_manager.service }
184
+ let(:image_service) { openstack_vm_manager.image_service }
185
+ let(:network_service) { openstack_vm_manager.network_service }
186
+
187
+ let(:servers) { compute_service.servers }
188
+ let(:addresses) { compute_service.addresses }
189
+ let(:images) { image_service.images }
190
+ let(:image) { images.find { |image| image.name == openstack_vm_options[:name] } }
191
+ let(:instance) { servers.find { |server| server.name == openstack_vm_options[:name] } }
192
+
193
+ before do
194
+ allow(File).to receive(:size).with(path).and_return(file_size)
195
+ allow(openstack_vm_manager).to receive(:say)
196
+
197
+ Fog.mock!
198
+ Fog::Mock.reset
199
+ Fog::Mock.delay = 0
200
+
201
+ allow(compute_service).to receive(:servers).and_return(servers)
202
+ allow(compute_service).to receive(:addresses).and_return(addresses)
203
+ allow(image_service).to receive(:images).and_return(images)
204
+
205
+ openstack_vm_manager.deploy(path, openstack_vm_options)
206
+ end
207
+
208
+ it 'calls destroy on the correct instance' do
209
+ destroy_correct_server = change do
210
+ servers.reload
211
+ servers.find { |server| server.name == openstack_vm_options[:name] }
212
+ end.to(nil)
213
+
214
+ expect { openstack_vm_manager.destroy(openstack_vm_options) }.to(destroy_correct_server)
215
+ end
216
+
217
+ it 'calls destroy on the correct image' do
218
+ destroy_correct_image = change do
219
+ images.reload
220
+ images.find { |image| image.name == openstack_vm_options[:name] }
221
+ end.to(nil)
222
+
223
+ expect { openstack_vm_manager.destroy(openstack_vm_options) }.to(destroy_correct_image)
224
+ end
225
+
226
+ context 'when the server does not exist' do
227
+ before do
228
+ allow(servers).to receive(:get).and_return(nil)
229
+ end
230
+
231
+ it 'returns without error' do
232
+ expect { openstack_vm_manager.destroy(openstack_vm_options) }.not_to raise_error
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end