diff --git a/nova/tests/virt/vmwareapi/test_vmops.py b/nova/tests/virt/vmwareapi/test_vmops.py index fff33c117ee..6b43be1e80c 100644 --- a/nova/tests/virt/vmwareapi/test_vmops.py +++ b/nova/tests/virt/vmwareapi/test_vmops.py @@ -565,6 +565,155 @@ def test_get_ds_browser(self): self.assertIs(ds_browser, ret) self.assertIs(ds_browser, cache.get(moref.value)) + @mock.patch.object( + vmops.VMwareVMOps, '_sized_image_exists', return_value=False) + @mock.patch.object(vmops.VMwareVMOps, '_extend_virtual_disk') + @mock.patch.object(vm_util, 'copy_virtual_disk') + def _test_use_disk_image_as_linked_clone(self, + mock_copy_virtual_disk, + mock_extend_virtual_disk, + mock_sized_image_exists, + flavor_fits_image=False): + file_size = 10 * units.Gi if flavor_fits_image else 5 * units.Gi + image_info = vmware_images.VMwareImage( + image_id=self._image_id, + file_size=file_size, + linked_clone=False) + + cache_root_folder = self._ds.build_path("vmware_base", self._image_id) + mock_imagecache = mock.Mock() + mock_imagecache.get_image_cache_folder.return_value = cache_root_folder + vi = vmops.VirtualMachineInstanceConfigInfo( + self._instance, "fake_uuid", image_info, + self._ds, self._dc_info, mock_imagecache) + + sized_cached_image_ds_loc = cache_root_folder.join( + "%s.%s.vmdk" % (self._image_id, vi.root_gb)) + + copy_spec = None + + self._vmops._volumeops = mock.Mock() + mock_attach_disk_to_vm = self._vmops._volumeops.attach_disk_to_vm + + self._vmops._use_disk_image_as_linked_clone("fake_vm_ref", vi) + + mock_copy_virtual_disk.assert_called_once_with( + self._session, self._dc_info.ref, + str(vi.cache_image_path), + str(sized_cached_image_ds_loc), + copy_spec) + + if not flavor_fits_image: + mock_extend_virtual_disk.assert_called_once_with( + self._instance, vi.root_gb * units.Mi, + str(sized_cached_image_ds_loc), + self._dc_info.ref) + + mock_attach_disk_to_vm.assert_called_once_with( + "fake_vm_ref", self._instance, vi.ii.adapter_type, + vi.ii.disk_type, + str(sized_cached_image_ds_loc), + vi.root_gb * units.Mi, False) + + def test_use_disk_image_as_linked_clone(self): + self._test_use_disk_image_as_linked_clone() + + def test_use_disk_image_as_linked_clone_flavor_fits_image(self): + self._test_use_disk_image_as_linked_clone(flavor_fits_image=True) + + @mock.patch.object(vmops.VMwareVMOps, '_extend_virtual_disk') + @mock.patch.object(vm_util, 'copy_virtual_disk') + def _test_use_disk_image_as_full_clone(self, + mock_copy_virtual_disk, + mock_extend_virtual_disk, + flavor_fits_image=False): + file_size = 10 * units.Gi if flavor_fits_image else 5 * units.Gi + image_info = vmware_images.VMwareImage( + image_id=self._image_id, + file_size=file_size, + linked_clone=False) + + cache_root_folder = self._ds.build_path("vmware_base", self._image_id) + mock_imagecache = mock.Mock() + mock_imagecache.get_image_cache_folder.return_value = cache_root_folder + vi = vmops.VirtualMachineInstanceConfigInfo( + self._instance, "fake_uuid", image_info, + self._ds, self._dc_info, mock_imagecache) + + copy_spec = None + + self._vmops._volumeops = mock.Mock() + mock_attach_disk_to_vm = self._vmops._volumeops.attach_disk_to_vm + + self._vmops._use_disk_image_as_full_clone("fake_vm_ref", vi) + + mock_copy_virtual_disk.assert_called_once_with( + self._session, self._dc_info.ref, + str(vi.cache_image_path), + '[fake_ds] fake_uuid/fake_uuid.vmdk', copy_spec) + + if not flavor_fits_image: + mock_extend_virtual_disk.assert_called_once_with( + self._instance, vi.root_gb * units.Mi, + '[fake_ds] fake_uuid/fake_uuid.vmdk', self._dc_info.ref) + + mock_attach_disk_to_vm.assert_called_once_with( + "fake_vm_ref", self._instance, vi.ii.adapter_type, + vi.ii.disk_type, '[fake_ds] fake_uuid/fake_uuid.vmdk', + vi.root_gb * units.Mi, False) + + def test_use_disk_image_as_full_clone(self): + self._test_use_disk_image_as_full_clone() + + def test_use_disk_image_as_full_clone_image_too_big(self): + self._test_use_disk_image_as_full_clone(flavor_fits_image=True) + + @mock.patch.object(vmops.VMwareVMOps, '_attach_cdrom_to_vm') + @mock.patch.object(vm_util, 'create_virtual_disk') + def _test_use_iso_image(self, + mock_create_virtual_disk, + mock_attach_cdrom, + with_root_disk): + image_info = vmware_images.VMwareImage( + image_id=self._image_id, + file_size=10 * units.Mi, + linked_clone=True) + + cache_root_folder = self._ds.build_path("vmware_base", self._image_id) + mock_imagecache = mock.Mock() + mock_imagecache.get_image_cache_folder.return_value = cache_root_folder + vi = vmops.VirtualMachineInstanceConfigInfo( + self._instance, "fake_uuid", image_info, + self._ds, self._dc_info, mock_imagecache) + + self._vmops._volumeops = mock.Mock() + mock_attach_disk_to_vm = self._vmops._volumeops.attach_disk_to_vm + + self._vmops._use_iso_image("fake_vm_ref", vi) + + mock_attach_cdrom.assert_called_once_with( + "fake_vm_ref", self._instance, self._ds.ref, + str(vi.cache_image_path)) + + if with_root_disk: + mock_create_virtual_disk.assert_called_once_with( + self._session, self._dc_info.ref, + vi.ii.adapter_type, vi.ii.disk_type, + '[fake_ds] fake_uuid/fake_uuid.vmdk', + vi.root_gb * units.Mi) + linked_clone = False + mock_attach_disk_to_vm.assert_called_once_with( + "fake_vm_ref", self._instance, + vi.ii.adapter_type, vi.ii.disk_type, + '[fake_ds] fake_uuid/fake_uuid.vmdk', + vi.root_gb * units.Mi, linked_clone) + + def test_use_iso_image_with_root_disk(self): + self._test_use_iso_image(with_root_disk=True) + + def test_use_iso_image_without_root_disk(self): + self._test_use_iso_image(with_root_disk=False) + def _verify_spawn_method_calls(self, mock_call_method): # TODO(vui): More explicit assertions of spawn() behavior # are waiting on additional refactoring pertaining to image @@ -654,7 +803,7 @@ def _test_spawn(self, mock_is_neutron.assert_called_once_with() - expected_mkdir_calls = 3 + expected_mkdir_calls = 2 if block_device_info and len(block_device_info.get( 'block_device_mapping', [])) > 0: # if block_device_info contains key 'block_device_mapping' diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 0586da19dd7..d553278fbf2 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -372,11 +372,6 @@ def spawn(self, context, instance, image_meta, injected_files, upload_name = instance.image_ref upload_folder = '%s/%s' % (self._base_folder, upload_name) - # The vmdk meta-data file - uploaded_file_path = str(datastore.build_path( - upload_folder, - "%s.%s" % (upload_name, image_info.file_type))) - session_vim = self._session._get_vim() cookies = session_vim.client.options.transport.cookiejar @@ -493,129 +488,11 @@ def spawn(self, context, instance, image_meta, injected_files, dc_info.ref) if image_info.is_iso: - if instance.root_gb != 0: - dest_vmdk_path = self._get_vmdk_path(datastore.name, - instance.uuid, - instance_name) - # Create the blank virtual disk for the VM - LOG.debug("Create blank virtual disk on %s", - datastore.name, instance=instance) - vm_util.create_virtual_disk(self._session, - dc_info.ref, - image_info.adapter_type, - image_info.disk_type, - dest_vmdk_path, - image_info.file_size_in_kb) - LOG.debug("Blank virtual disk created on %s.", - datastore.name, instance=instance) - root_vmdk_path = dest_vmdk_path - else: - root_vmdk_path = None + self._use_iso_image(vm_ref, vi) + elif image_info.linked_clone: + self._use_disk_image_as_linked_clone(vm_ref, vi) else: - # Extend the disk size if necessary - if not image_info.linked_clone: - # If we are not using linked_clone, copy the image from - # the cache into the instance directory. If we are using - # linked clone it is references from the cache directory - dest_vmdk_path = self._get_vmdk_path(datastore.name, - instance_name, instance_name) - copy_spec = self.get_copy_virtual_disk_spec( - client_factory, - image_info.adapter_type, - image_info.disk_type) - vm_util.copy_virtual_disk(self._session, - dc_info.ref, - uploaded_file_path, - dest_vmdk_path, copy_spec) - - root_vmdk_path = dest_vmdk_path - self._extend_if_required(dc_info, image_info, instance, - root_vmdk_path) - else: - upload_folder = '%s/%s' % (self._base_folder, upload_name) - if instance.root_gb: - root_vmdk_name = "%s.%s.vmdk" % (upload_name, - instance.root_gb) - else: - root_vmdk_name = "%s.vmdk" % upload_name - root_vmdk_path = str(datastore.build_path( - upload_folder, root_vmdk_name)) - - # Ensure only a single thread extends the image at once. - # We do this by taking a lock on the name of the extended - # image. This allows multiple threads to create resized - # copies simultaneously, as long as they are different - # sizes. Threads attempting to create the same resized copy - # will be serialized, with only the first actually creating - # the copy. - # - # Note that the object is in a per-nova cache directory, - # so inter-nova locking is not a concern. Consequently we - # can safely use simple thread locks. - - with lockutils.lock(root_vmdk_path, - lock_file_prefix='nova-vmware-image'): - if not self._check_if_folder_file_exists( - ds_browser, - datastore.ref, datastore.name, - upload_folder, - root_vmdk_name): - LOG.debug("Copying root disk of size %sGb", - instance.root_gb) - - copy_spec = self.get_copy_virtual_disk_spec( - client_factory, - image_info.adapter_type, - image_info.disk_type) - - # Create a copy of the base image, ensuring we - # clean up on failure - try: - vm_util.copy_virtual_disk(self._session, - dc_info.ref, - uploaded_file_path, - root_vmdk_path, - copy_spec) - except Exception as e: - with excutils.save_and_reraise_exception(): - LOG.error(_LE('Failed to copy cached ' - 'image %(source)s to ' - '%(dest)s for resize: ' - '%(error)s'), - {'source': uploaded_file_path, - 'dest': root_vmdk_path, - 'error': e.message}) - try: - ds_util.file_delete(self._session, - root_vmdk_path, - dc_info.ref) - except vexc.FileNotFoundException: - # File was never created: cleanup not - # required - pass - - # Resize the copy to the appropriate size. No need - # for cleanup up here, as _extend_virtual_disk - # already does it - self._extend_if_required(dc_info, image_info, - instance, root_vmdk_path) - - # Attach the root disk to the VM. - if root_vmdk_path is not None: - self._volumeops.attach_disk_to_vm( - vm_ref, - instance, - image_info.adapter_type, - image_info.disk_type, - root_vmdk_path, - instance.root_gb * units.Mi, - image_info.linked_clone) - - if image_info.is_iso: - self._attach_cdrom_to_vm( - vm_ref, instance, - datastore.ref, - uploaded_file_path) + self._use_disk_image_as_full_clone(vm_ref, vi) if configdrive.required_by(instance): self._configure_config_drive( @@ -1481,9 +1358,8 @@ def _check_if_folder_file_exists(self, ds_browser, ds_ref, ds_name, self.check_cache_folder(ds_name, ds_ref) # Check if the file exists or not. folder_ds_path = ds_util.DatastorePath(ds_name, folder_name) - file_exists = ds_util.file_exists(self._session, ds_browser, - folder_ds_path, file_name) - return file_exists + return ds_util.file_exists( + self._session, ds_browser, folder_ds_path, file_name) def inject_network_info(self, instance, network_info): """inject network info for specified instance.""" @@ -1606,6 +1482,142 @@ def detach_interface(self, instance, vif): instance_uuid=instance['uuid']) LOG.debug("Reconfigured VM to detach interface", instance=instance) + def _use_disk_image_as_full_clone(self, vm_ref, vi): + """Uses cached image disk by copying it into the VM directory.""" + + instance_folder = vi.instance_name + root_disk_name = "%s.vmdk" % vi.instance_name + root_disk_ds_loc = vi.datastore.build_path(instance_folder, + root_disk_name) + + client_factory = self._session._get_vim().client.factory + copy_spec = self.get_copy_virtual_disk_spec( + client_factory, vi.ii.adapter_type, vi.ii.disk_type) + + vm_util.copy_virtual_disk( + self._session, + vi.dc_info.ref, + str(vi.cache_image_path), + str(root_disk_ds_loc), + copy_spec) + + self._extend_if_required( + vi.dc_info, vi.ii, vi.instance, str(root_disk_ds_loc)) + + self._volumeops.attach_disk_to_vm( + vm_ref, vi.instance, + vi.ii.adapter_type, vi.ii.disk_type, + str(root_disk_ds_loc), + vi.root_gb * units.Mi, False) + + def _sized_image_exists(self, sized_disk_ds_loc, ds_ref): + ds_browser = self._get_ds_browser(ds_ref) + return ds_util.file_exists( + self._session, ds_browser, sized_disk_ds_loc.parent, + sized_disk_ds_loc.basename) + + def _use_disk_image_as_linked_clone(self, vm_ref, vi): + """Uses cached image as parent of a COW child in the VM directory.""" + + sized_image_disk_name = "%s.vmdk" % vi.ii.image_id + if vi.root_gb > 0: + sized_image_disk_name = "%s.%s.vmdk" % (vi.ii.image_id, vi.root_gb) + sized_disk_ds_loc = vi.cache_image_folder.join(sized_image_disk_name) + + # Ensure only a single thread extends the image at once. + # We do this by taking a lock on the name of the extended + # image. This allows multiple threads to create resized + # copies simultaneously, as long as they are different + # sizes. Threads attempting to create the same resized copy + # will be serialized, with only the first actually creating + # the copy. + # + # Note that the object is in a per-nova cache directory, + # so inter-nova locking is not a concern. Consequently we + # can safely use simple thread locks. + + with lockutils.lock(str(sized_disk_ds_loc), + lock_file_prefix='nova-vmware-image'): + + if not self._sized_image_exists(sized_disk_ds_loc, + vi.datastore.ref): + LOG.debug("Copying root disk of size %sGb", vi.root_gb) + try: + client_factory = self._session._get_vim().client.factory + copy_spec = self.get_copy_virtual_disk_spec( + client_factory, vi.ii.adapter_type, + vi.ii.disk_type) + + vm_util.copy_virtual_disk( + self._session, + vi.dc_info.ref, + str(vi.cache_image_path), + str(sized_disk_ds_loc), + copy_spec) + except Exception as e: + LOG.warning(_("Root disk file creation " + "failed - %s"), e) + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to copy cached ' + 'image %(source)s to ' + '%(dest)s for resize: ' + '%(error)s'), + {'source': vi.cache_image_path, + 'dest': sized_disk_ds_loc, + 'error': e.message}) + try: + ds_util.file_delete(self._session, + sized_disk_ds_loc, + vi.dc_info.ref) + except vexc.FileNotFoundException: + # File was never created: cleanup not + # required + pass + + # Resize the copy to the appropriate size. No need + # for cleanup up here, as _extend_virtual_disk + # already does it + self._extend_if_required( + vi.dc_info, vi.ii, vi.instance, str(sized_disk_ds_loc)) + + # Associate the sized image disk to the VM by attaching to the VM a + # COW child of said disk. + self._volumeops.attach_disk_to_vm( + vm_ref, vi.instance, + vi.ii.adapter_type, vi.ii.disk_type, + str(sized_disk_ds_loc), + vi.root_gb * units.Mi, vi.ii.linked_clone) + + def _use_iso_image(self, vm_ref, vi): + """Uses cached image as a bootable virtual cdrom.""" + + self._attach_cdrom_to_vm( + vm_ref, vi.instance, vi.datastore.ref, + str(vi.cache_image_path)) + + # Optionally create and attach blank disk + if vi.root_gb > 0: + instance_folder = vi.instance_name + root_disk_name = "%s.vmdk" % vi.instance_name + root_disk_ds_loc = vi.datastore.build_path(instance_folder, + root_disk_name) + + # It is pointless to COW a blank disk + linked_clone = False + + vm_util.create_virtual_disk( + self._session, vi.dc_info.ref, + vi.ii.adapter_type, + vi.ii.disk_type, + str(root_disk_ds_loc), + vi.root_gb * units.Mi) + + self._volumeops.attach_disk_to_vm( + vm_ref, vi.instance, + vi.ii.adapter_type, vi.ii.disk_type, + str(root_disk_ds_loc), + vi.root_gb * units.Mi, linked_clone) + class VMwareVCVMOps(VMwareVMOps): """Management class for VM-related tasks.