From 3f96f3039a17e2cad15c05c4fe2d61ee16a14a80 Mon Sep 17 00:00:00 2001 From: Lucian Petrut Date: Fri, 6 Feb 2015 19:02:23 +0200 Subject: [PATCH] Hyper-V: Implement nova rescue The root disk image is moved to a separate disk slot while the rescue image will take it's place. If the instance requires it, a temporary config drive is created as well. Unrescuing the instance will move the root disk image back in place, removing temporary images. DocImpact Implements: blueprint hyper-v-rescue Change-Id: I6059ae35a77d675f54b98b2b43b5762e1d24365b --- doc/source/support-matrix.ini | 2 +- .../tests/unit/virt/hyperv/test_imagecache.py | 27 +- nova/tests/unit/virt/hyperv/test_pathutils.py | 25 +- nova/tests/unit/virt/hyperv/test_vmops.py | 255 ++++++++++++++++-- nova/virt/hyperv/driver.py | 8 + nova/virt/hyperv/imagecache.py | 29 +- nova/virt/hyperv/pathutils.py | 32 ++- nova/virt/hyperv/vmops.py | 140 +++++++++- 8 files changed, 466 insertions(+), 52 deletions(-) diff --git a/doc/source/support-matrix.ini b/doc/source/support-matrix.ini index 483bf0b8f5fd..5db9abe97b63 100644 --- a/doc/source/support-matrix.ini +++ b/doc/source/support-matrix.ini @@ -357,7 +357,7 @@ driver-impl-libvirt-qemu-x86=complete driver-impl-libvirt-lxc=missing driver-impl-libvirt-xen=complete driver-impl-vmware=complete -driver-impl-hyperv=missing +driver-impl-hyperv=complete driver-impl-ironic=missing driver-impl-libvirt-vz-vm=missing driver-impl-libvirt-vz-ct=missing diff --git a/nova/tests/unit/virt/hyperv/test_imagecache.py b/nova/tests/unit/virt/hyperv/test_imagecache.py index 8e60ea7bfb88..8a27b5531b95 100644 --- a/nova/tests/unit/virt/hyperv/test_imagecache.py +++ b/nova/tests/unit/virt/hyperv/test_imagecache.py @@ -97,7 +97,8 @@ class ImageCacheTestCase(test_base.HyperVBaseTestCase): mock_internal_vhd_size.assert_called_once_with( mock.sentinel.vhd_path, self.FAKE_VHD_SIZE_GB * units.Gi) - def _prepare_get_cached_image(self, path_exists, use_cow): + def _prepare_get_cached_image(self, path_exists=False, use_cow=False, + rescue_image_id=None): self.instance.image_ref = self.FAKE_IMAGE_REF self.imagecache._pathutils.get_base_vhd_dir.return_value = ( self.FAKE_BASE_DIR) @@ -107,8 +108,9 @@ class ImageCacheTestCase(test_base.HyperVBaseTestCase): CONF.set_override('use_cow_images', use_cow) + image_file_name = rescue_image_id or self.FAKE_IMAGE_REF expected_path = os.path.join(self.FAKE_BASE_DIR, - self.FAKE_IMAGE_REF) + image_file_name) expected_vhd_path = "%s.%s" % (expected_path, constants.DISK_FORMAT_VHD.lower()) return (expected_path, expected_vhd_path) @@ -157,3 +159,24 @@ class ImageCacheTestCase(test_base.HyperVBaseTestCase): self.assertEqual(expected_resized_vhd_path, result) mock_resize.assert_called_once_with(self.instance, expected_vhd_path) + + @mock.patch.object(imagecache.images, 'fetch') + def test_cache_rescue_image_bigger_than_flavor(self, mock_fetch): + fake_rescue_image_id = 'fake_rescue_image_id' + + self.imagecache._vhdutils.get_vhd_info.return_value = { + 'VirtualSize': self.instance.root_gb + 1} + (expected_path, + expected_vhd_path) = self._prepare_get_cached_image( + rescue_image_id=fake_rescue_image_id) + + self.assertRaises(exception.ImageUnacceptable, + self.imagecache.get_cached_image, + self.context, self.instance, + fake_rescue_image_id) + + mock_fetch.assert_called_once_with(self.context, + fake_rescue_image_id, + expected_path) + self.imagecache._vhdutils.get_vhd_info.assert_called_once_with( + expected_vhd_path) diff --git a/nova/tests/unit/virt/hyperv/test_pathutils.py b/nova/tests/unit/virt/hyperv/test_pathutils.py index 5f2fb877c8b4..96dca680c048 100644 --- a/nova/tests/unit/virt/hyperv/test_pathutils.py +++ b/nova/tests/unit/virt/hyperv/test_pathutils.py @@ -33,7 +33,7 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase): self._pathutils = pathutils.PathUtils() - def _mock_lookup_configdrive_path(self, ext): + def _mock_lookup_configdrive_path(self, ext, rescue=False): self._pathutils.get_instance_dir = mock.MagicMock( return_value=self.fake_instance_dir) @@ -42,15 +42,26 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase): return True if path[(path.rfind('.') + 1):] == ext else False self._pathutils.exists = mock_exists configdrive_path = self._pathutils.lookup_configdrive_path( - self.fake_instance_name) + self.fake_instance_name, rescue) return configdrive_path - def test_lookup_configdrive_path(self): + def _test_lookup_configdrive_path(self, rescue=False): + configdrive_name = 'configdrive' + if rescue: + configdrive_name += '-rescue' + for format_ext in constants.DISK_FORMAT_MAP: - configdrive_path = self._mock_lookup_configdrive_path(format_ext) - fake_path = os.path.join(self.fake_instance_dir, - 'configdrive.' + format_ext) - self.assertEqual(configdrive_path, fake_path) + configdrive_path = self._mock_lookup_configdrive_path(format_ext, + rescue) + expected_path = os.path.join(self.fake_instance_dir, + configdrive_name + '.' + format_ext) + self.assertEqual(expected_path, configdrive_path) + + def test_lookup_configdrive_path(self): + self._test_lookup_configdrive_path() + + def test_lookup_rescue_configdrive_path(self): + self._test_lookup_configdrive_path(rescue=True) def test_lookup_configdrive_path_non_exist(self): self._pathutils.get_instance_dir = mock.MagicMock( diff --git a/nova/tests/unit/virt/hyperv/test_vmops.py b/nova/tests/unit/virt/hyperv/test_vmops.py index d3ce2f440b02..9427c3ca3261 100644 --- a/nova/tests/unit/virt/hyperv/test_vmops.py +++ b/nova/tests/unit/virt/hyperv/test_vmops.py @@ -20,8 +20,10 @@ from os_win import constants as os_win_const from os_win import exceptions as os_win_exc from oslo_concurrency import processutils from oslo_config import cfg +from oslo_utils import fileutils from oslo_utils import units +from nova.compute import vm_states from nova import exception from nova import objects from nova.tests.unit import fake_instance @@ -189,7 +191,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self.assertEqual(fake_root_path, response) self._vmops._pathutils.get_root_vhd_path.assert_called_with( - mock_instance.name, vhd_format) + mock_instance.name, vhd_format, False) differencing_vhd = self._vmops._vhdutils.create_differencing_vhd differencing_vhd.assert_called_with(fake_root_path, fake_vhd_path) self._vmops._vhdutils.get_vhd_info.assert_called_once_with( @@ -205,29 +207,41 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): fake_root_path, root_vhd_internal_size, is_file_max_size=False) @mock.patch('nova.virt.hyperv.imagecache.ImageCache.get_cached_image') - def _test_create_root_vhd(self, mock_get_cached_image, vhd_format): + def _test_create_root_vhd(self, mock_get_cached_image, vhd_format, + is_rescue_vhd=False): mock_instance = self._prepare_create_root_vhd_mocks( use_cow_images=False, vhd_format=vhd_format, vhd_size=(self.FAKE_SIZE - 1)) fake_vhd_path = self.FAKE_ROOT_PATH % vhd_format mock_get_cached_image.return_value = fake_vhd_path + rescue_image_id = ( + mock.sentinel.rescue_image_id if is_rescue_vhd else None) fake_root_path = self._vmops._pathutils.get_root_vhd_path.return_value root_vhd_internal_size = mock_instance.root_gb * units.Gi get_size = self._vmops._vhdutils.get_internal_vhd_size_by_file_size - response = self._vmops._create_root_vhd(context=self.context, - instance=mock_instance) + response = self._vmops._create_root_vhd( + context=self.context, + instance=mock_instance, + rescue_image_id=rescue_image_id) self.assertEqual(fake_root_path, response) + mock_get_cached_image.assert_called_once_with(self.context, + mock_instance, + rescue_image_id) self._vmops._pathutils.get_root_vhd_path.assert_called_with( - mock_instance.name, vhd_format) + mock_instance.name, vhd_format, is_rescue_vhd) self._vmops._pathutils.copyfile.assert_called_once_with( fake_vhd_path, fake_root_path) get_size.assert_called_once_with(fake_vhd_path, root_vhd_internal_size) - self._vmops._vhdutils.resize_vhd.assert_called_once_with( - fake_root_path, root_vhd_internal_size, is_file_max_size=False) + if is_rescue_vhd: + self.assertFalse(self._vmops._vhdutils.resize_vhd.called) + else: + self._vmops._vhdutils.resize_vhd.assert_called_once_with( + fake_root_path, root_vhd_internal_size, + is_file_max_size=False) def test_create_root_vhd(self): self._test_create_root_vhd(vhd_format=constants.DISK_FORMAT_VHD) @@ -241,6 +255,10 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): def test_create_root_vhdx_use_cow_images_true(self): self._test_create_root_vhd_qcow(vhd_format=constants.DISK_FORMAT_VHDX) + def test_create_rescue_vhd(self): + self._test_create_root_vhd(vhd_format=constants.DISK_FORMAT_VHD, + is_rescue_vhd=True) + def test_create_root_vhdx_size_less_than_internal(self): self._test_create_root_vhd_exception( vhd_format=constants.DISK_FORMAT_VHD) @@ -546,44 +564,63 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): @mock.patch('nova.utils.execute') def _test_create_config_drive(self, mock_execute, mock_ConfigDriveBuilder, mock_InstanceMetadata, config_drive_format, - config_drive_cdrom, side_effect): + config_drive_cdrom, side_effect, + rescue=False): mock_instance = fake_instance.fake_instance_obj(self.context) self.flags(config_drive_format=config_drive_format) self.flags(config_drive_cdrom=config_drive_cdrom, group='hyperv') self.flags(config_drive_inject_password=True, group='hyperv') - self._vmops._pathutils.get_instance_dir.return_value = ( - self.FAKE_DIR) mock_ConfigDriveBuilder().__enter__().make_drive.side_effect = [ side_effect] + path_iso = os.path.join(self.FAKE_DIR, self.FAKE_CONFIG_DRIVE_ISO) + path_vhd = os.path.join(self.FAKE_DIR, self.FAKE_CONFIG_DRIVE_VHD) + + def fake_get_configdrive_path(instance_name, disk_format, + rescue=False): + return (path_iso + if disk_format == constants.DVD_FORMAT else path_vhd) + + mock_get_configdrive_path = self._vmops._pathutils.get_configdrive_path + mock_get_configdrive_path.side_effect = fake_get_configdrive_path + expected_get_configdrive_path_calls = [mock.call(mock_instance.name, + constants.DVD_FORMAT, + rescue=rescue)] + if not config_drive_cdrom: + expected_call = mock.call(mock_instance.name, + constants.DISK_FORMAT_VHD, + rescue=rescue) + expected_get_configdrive_path_calls.append(expected_call) + if config_drive_format != self.ISO9660: self.assertRaises(exception.ConfigDriveUnsupportedFormat, self._vmops._create_config_drive, mock_instance, [mock.sentinel.FILE], mock.sentinel.PASSWORD, - mock.sentinel.NET_INFO) + mock.sentinel.NET_INFO, + rescue) elif side_effect is processutils.ProcessExecutionError: self.assertRaises(processutils.ProcessExecutionError, self._vmops._create_config_drive, mock_instance, [mock.sentinel.FILE], mock.sentinel.PASSWORD, - mock.sentinel.NET_INFO) + mock.sentinel.NET_INFO, + rescue) else: path = self._vmops._create_config_drive(mock_instance, [mock.sentinel.FILE], mock.sentinel.PASSWORD, - mock.sentinel.NET_INFO) + mock.sentinel.NET_INFO, + rescue) mock_InstanceMetadata.assert_called_once_with( mock_instance, content=[mock.sentinel.FILE], extra_md={'admin_pass': mock.sentinel.PASSWORD}, network_info=mock.sentinel.NET_INFO) - self._vmops._pathutils.get_instance_dir.assert_called_once_with( - mock_instance.name) + mock_get_configdrive_path.assert_has_calls( + expected_get_configdrive_path_calls) mock_ConfigDriveBuilder.assert_called_with( instance_md=mock_InstanceMetadata()) mock_make_drive = mock_ConfigDriveBuilder().__enter__().make_drive - path_iso = os.path.join(self.FAKE_DIR, self.FAKE_CONFIG_DRIVE_ISO) - path_vhd = os.path.join(self.FAKE_DIR, self.FAKE_CONFIG_DRIVE_VHD) mock_make_drive.assert_called_once_with(path_iso) if not CONF.hyperv.config_drive_cdrom: expected = path_vhd @@ -608,6 +645,12 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): config_drive_cdrom=False, side_effect=None) + def test_create_rescue_config_drive_vhd(self): + self._test_create_config_drive(config_drive_format=self.ISO9660, + config_drive_cdrom=False, + side_effect=None, + rescue=True) + def test_create_config_drive_other_drive_format(self): self._test_create_config_drive(config_drive_format=mock.sentinel.OTHER, config_drive_cdrom=False, @@ -646,6 +689,25 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): instance.name, self._FAKE_CONFIGDRIVE_PATH, 1, 0, constants.CTRL_TYPE_SCSI, constants.DISK) + def test_detach_config_drive(self): + is_rescue_configdrive = True + mock_lookup_configdrive = ( + self._vmops._pathutils.lookup_configdrive_path) + mock_lookup_configdrive.return_value = mock.sentinel.configdrive_path + + self._vmops._detach_config_drive(mock.sentinel.instance_name, + rescue=is_rescue_configdrive, + delete=True) + + mock_lookup_configdrive.assert_called_once_with( + mock.sentinel.instance_name, + rescue=is_rescue_configdrive) + self._vmops._vmutils.detach_vm_disk.assert_called_once_with( + mock.sentinel.instance_name, mock.sentinel.configdrive_path, + is_physical=False) + self._vmops._pathutils.remove.assert_called_once_with( + mock.sentinel.configdrive_path) + def test_delete_disk_files(self): mock_instance = fake_instance.fake_instance_obj(self.context) self._vmops._delete_disk_files(mock_instance.name) @@ -1073,3 +1135,162 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self.assertRaises(exception.InterfaceDetachFailed, self._vmops.detach_interface, mock.MagicMock(), mock.sentinel.fake_vif) + + @mock.patch('nova.virt.configdrive.required_by') + @mock.patch.object(vmops.VMOps, '_create_root_vhd') + @mock.patch.object(vmops.VMOps, 'get_image_vm_generation') + @mock.patch.object(vmops.VMOps, '_attach_drive') + @mock.patch.object(vmops.VMOps, '_create_config_drive') + @mock.patch.object(vmops.VMOps, 'attach_config_drive') + @mock.patch.object(vmops.VMOps, '_detach_config_drive') + @mock.patch.object(vmops.VMOps, 'power_on') + def test_rescue_instance(self, mock_power_on, + mock_detach_config_drive, + mock_attach_config_drive, + mock_create_config_drive, + mock_attach_drive, + mock_get_image_vm_gen, + mock_create_root_vhd, + mock_configdrive_required): + mock_image_meta = mock.MagicMock() + mock_vm_gen = constants.VM_GEN_2 + mock_instance = fake_instance.fake_instance_obj(self.context) + + mock_configdrive_required.return_value = True + mock_create_root_vhd.return_value = mock.sentinel.rescue_vhd_path + mock_get_image_vm_gen.return_value = mock_vm_gen + self._vmops._vmutils.get_vm_generation.return_value = mock_vm_gen + self._vmops._pathutils.lookup_root_vhd_path.return_value = ( + mock.sentinel.root_vhd_path) + mock_create_config_drive.return_value = ( + mock.sentinel.rescue_configdrive_path) + + self._vmops.rescue_instance(self.context, + mock_instance, + mock.sentinel.network_info, + mock_image_meta, + mock.sentinel.rescue_password) + + mock_get_image_vm_gen.assert_called_once_with( + mock_instance.uuid, mock.sentinel.rescue_vhd_path, + mock_image_meta) + self._vmops._vmutils.detach_vm_disk.assert_called_once_with( + mock_instance.name, mock.sentinel.root_vhd_path, + is_physical=False) + mock_attach_drive.assert_called_once_with( + mock_instance.name, mock.sentinel.rescue_vhd_path, 0, + self._vmops._ROOT_DISK_CTRL_ADDR, + vmops.VM_GENERATIONS_CONTROLLER_TYPES[mock_vm_gen]) + self._vmops._vmutils.attach_scsi_drive.assert_called_once_with( + mock_instance.name, mock.sentinel.root_vhd_path, + drive_type=constants.DISK) + mock_detach_config_drive.assert_called_once_with(mock_instance.name) + mock_create_config_drive.assert_called_once_with( + mock_instance, + injected_files=None, + admin_password=mock.sentinel.rescue_password, + network_info=mock.sentinel.network_info, + rescue=True) + mock_attach_config_drive.assert_called_once_with( + mock_instance, mock.sentinel.rescue_configdrive_path, + mock_vm_gen) + + @mock.patch.object(vmops.VMOps, '_create_root_vhd') + @mock.patch.object(vmops.VMOps, 'get_image_vm_generation') + @mock.patch.object(vmops.VMOps, 'unrescue_instance') + def _test_rescue_instance_exception(self, mock_unrescue, + mock_get_image_vm_gen, + mock_create_root_vhd, + wrong_vm_gen=False, + boot_from_volume=False, + expected_exc=None): + mock_vm_gen = constants.VM_GEN_1 + image_vm_gen = (mock_vm_gen + if not wrong_vm_gen else constants.VM_GEN_2) + mock_image_meta = mock.MagicMock() + + mock_instance = fake_instance.fake_instance_obj(self.context) + mock_get_image_vm_gen.return_value = image_vm_gen + self._vmops._vmutils.get_vm_generation.return_value = mock_vm_gen + self._vmops._pathutils.lookup_root_vhd_path.return_value = ( + mock.sentinel.root_vhd_path if not boot_from_volume else None) + + self.assertRaises(expected_exc, + self._vmops.rescue_instance, + self.context, mock_instance, + mock.sentinel.network_info, + mock_image_meta, + mock.sentinel.rescue_password) + mock_unrescue.assert_called_once_with(mock_instance) + + def test_rescue_instance_wrong_vm_gen(self): + # Test the case when the rescue image requires a different + # vm generation than the actual rescued instance. + self._test_rescue_instance_exception( + wrong_vm_gen=True, + expected_exc=exception.ImageUnacceptable) + + def test_rescue_instance_boot_from_volume(self): + # Rescuing instances booted from volume is not supported. + self._test_rescue_instance_exception( + boot_from_volume=True, + expected_exc=exception.InstanceNotRescuable) + + @mock.patch.object(fileutils, 'delete_if_exists') + @mock.patch.object(vmops.VMOps, '_attach_drive') + @mock.patch.object(vmops.VMOps, 'attach_config_drive') + @mock.patch.object(vmops.VMOps, '_detach_config_drive') + @mock.patch.object(vmops.VMOps, 'power_on') + @mock.patch.object(vmops.VMOps, 'power_off') + def test_unrescue_instance(self, mock_power_on, mock_power_off, + mock_detach_config_drive, + mock_attach_configdrive, + mock_attach_drive, + mock_delete_if_exists): + mock_instance = fake_instance.fake_instance_obj(self.context) + mock_vm_gen = constants.VM_GEN_2 + + self._vmops._vmutils.get_vm_generation.return_value = mock_vm_gen + self._vmops._vmutils.is_disk_attached.return_value = False + self._vmops._pathutils.lookup_root_vhd_path.side_effect = ( + mock.sentinel.root_vhd_path, mock.sentinel.rescue_vhd_path) + self._vmops._pathutils.lookup_configdrive_path.return_value = ( + mock.sentinel.configdrive_path) + + self._vmops.unrescue_instance(mock_instance) + + self._vmops._pathutils.lookup_root_vhd_path.assert_has_calls( + [mock.call(mock_instance.name), + mock.call(mock_instance.name, rescue=True)]) + self._vmops._vmutils.detach_vm_disk.assert_has_calls( + [mock.call(mock_instance.name, + mock.sentinel.root_vhd_path, + is_physical=False), + mock.call(mock_instance.name, + mock.sentinel.rescue_vhd_path, + is_physical=False)]) + mock_attach_drive.assert_called_once_with( + mock_instance.name, mock.sentinel.root_vhd_path, 0, + self._vmops._ROOT_DISK_CTRL_ADDR, + vmops.VM_GENERATIONS_CONTROLLER_TYPES[mock_vm_gen]) + mock_detach_config_drive.assert_called_once_with(mock_instance.name, + rescue=True, + delete=True) + mock_delete_if_exists.assert_called_once_with( + mock.sentinel.rescue_vhd_path) + self._vmops._vmutils.is_disk_attached.assert_called_once_with( + mock.sentinel.configdrive_path, + is_physical=False) + mock_attach_configdrive.assert_called_once_with( + mock_instance, mock.sentinel.configdrive_path, mock_vm_gen) + mock_power_on.assert_called_once_with(mock_instance) + + @mock.patch.object(vmops.VMOps, 'power_off') + def test_unrescue_instance_missing_root_image(self, mock_power_off): + mock_instance = fake_instance.fake_instance_obj(self.context) + mock_instance.vm_state = vm_states.RESCUED + self._vmops._pathutils.lookup_root_vhd_path.return_value = None + + self.assertRaises(exception.InstanceNotRescuable, + self._vmops.unrescue_instance, + mock_instance) diff --git a/nova/virt/hyperv/driver.py b/nova/virt/hyperv/driver.py index 349e1ee4a87f..bebcfb7b196d 100644 --- a/nova/virt/hyperv/driver.py +++ b/nova/virt/hyperv/driver.py @@ -333,3 +333,11 @@ class HyperVDriver(driver.ComputeDriver): def detach_interface(self, instance, vif): return self._vmops.detach_interface(instance, vif) + + def rescue(self, context, instance, network_info, image_meta, + rescue_password): + self._vmops.rescue_instance(context, instance, network_info, + image_meta, rescue_password) + + def unrescue(self, instance, network_info): + self._vmops.unrescue_instance(instance) diff --git a/nova/virt/hyperv/imagecache.py b/nova/virt/hyperv/imagecache.py index f700e68836d5..b99b04431916 100644 --- a/nova/virt/hyperv/imagecache.py +++ b/nova/virt/hyperv/imagecache.py @@ -24,6 +24,7 @@ from oslo_utils import units import nova.conf from nova import exception +from nova.i18n import _ from nova import utils from nova.virt.hyperv import pathutils from nova.virt import images @@ -87,8 +88,8 @@ class ImageCache(object): copy_and_resize_vhd() return resized_vhd_path - def get_cached_image(self, context, instance): - image_id = instance.image_ref + def get_cached_image(self, context, instance, rescue_image_id=None): + image_id = rescue_image_id or instance.image_ref base_vhd_dir = self._pathutils.get_base_vhd_dir() base_vhd_path = os.path.join(base_vhd_dir, image_id) @@ -118,11 +119,33 @@ class ImageCache(object): vhd_path = fetch_image_if_not_existing() - if CONF.use_cow_images and vhd_path.split('.')[-1].lower() == 'vhd': + # Note: rescue images are not resized. + is_vhd = vhd_path.split('.')[-1].lower() == 'vhd' + if CONF.use_cow_images and is_vhd and not rescue_image_id: # Resize the base VHD image as it's not possible to resize a # differencing VHD. This does not apply to VHDX images. resized_vhd_path = self._resize_and_cache_vhd(instance, vhd_path) if resized_vhd_path: return resized_vhd_path + if rescue_image_id: + self._verify_rescue_image(instance, rescue_image_id, + vhd_path) + return vhd_path + + def _verify_rescue_image(self, instance, rescue_image_id, + rescue_image_path): + rescue_image_info = self._vhdutils.get_vhd_info(rescue_image_path) + rescue_image_size = rescue_image_info['VirtualSize'] + flavor_disk_size = instance.root_gb * units.Gi + + if rescue_image_size> flavor_disk_size: + err_msg = _('Using a rescue image bigger than the instance ' + 'flavor disk size is not allowed. ' + 'Rescue image size: %(rescue_image_size)s. ' + 'Flavor disk size:%(flavor_disk_size)s.') % dict( + rescue_image_size=rescue_image_size, + flavor_disk_size=flavor_disk_size) + raise exception.ImageUnacceptable(reason=err_msg, + image_id=rescue_image_id) diff --git a/nova/virt/hyperv/pathutils.py b/nova/virt/hyperv/pathutils.py index b17c8d69dee4..c65b6ad8f38e 100644 --- a/nova/virt/hyperv/pathutils.py +++ b/nova/virt/hyperv/pathutils.py @@ -80,22 +80,26 @@ class PathUtils(pathutils.PathUtils): return self._get_instances_sub_dir(instance_name, remote_server, create_dir, remove_dir) - def _lookup_vhd_path(self, instance_name, vhd_path_func): + def _lookup_vhd_path(self, instance_name, vhd_path_func, + *args, **kwargs): vhd_path = None for format_ext in ['vhd', 'vhdx']: - test_path = vhd_path_func(instance_name, format_ext) + test_path = vhd_path_func(instance_name, format_ext, + *args, **kwargs) if self.exists(test_path): vhd_path = test_path break return vhd_path - def lookup_root_vhd_path(self, instance_name): - return self._lookup_vhd_path(instance_name, self.get_root_vhd_path) + def lookup_root_vhd_path(self, instance_name, rescue=False): + return self._lookup_vhd_path(instance_name, self.get_root_vhd_path, + rescue) - def lookup_configdrive_path(self, instance_name): + def lookup_configdrive_path(self, instance_name, rescue=False): configdrive_path = None for format_ext in constants.DISK_FORMAT_MAP: - test_path = self.get_configdrive_path(instance_name, format_ext) + test_path = self.get_configdrive_path(instance_name, format_ext, + rescue=rescue) if self.exists(test_path): configdrive_path = test_path break @@ -105,14 +109,22 @@ class PathUtils(pathutils.PathUtils): return self._lookup_vhd_path(instance_name, self.get_ephemeral_vhd_path) - def get_root_vhd_path(self, instance_name, format_ext): + def get_root_vhd_path(self, instance_name, format_ext, rescue=False): instance_path = self.get_instance_dir(instance_name) - return os.path.join(instance_path, 'root.' + format_ext.lower()) + image_name = 'root' + if rescue: + image_name += '-rescue' + return os.path.join(instance_path, + image_name + '.' + format_ext.lower()) def get_configdrive_path(self, instance_name, format_ext, - remote_server=None): + remote_server=None, rescue=False): instance_path = self.get_instance_dir(instance_name, remote_server) - return os.path.join(instance_path, 'configdrive.' + format_ext.lower()) + configdrive_image_name = 'configdrive' + if rescue: + configdrive_image_name += '-rescue' + return os.path.join(instance_path, + configdrive_image_name + '.' + format_ext.lower()) def get_ephemeral_vhd_path(self, instance_name, format_ext): instance_path = self.get_instance_dir(instance_name) diff --git a/nova/virt/hyperv/vmops.py b/nova/virt/hyperv/vmops.py index 85cdab7fc43f..9ca5b908ddc9 100644 --- a/nova/virt/hyperv/vmops.py +++ b/nova/virt/hyperv/vmops.py @@ -29,11 +29,13 @@ from oslo_concurrency import processutils from oslo_log import log as logging from oslo_service import loopingcall from oslo_utils import excutils +from oslo_utils import fileutils from oslo_utils import importutils from oslo_utils import units from oslo_utils import uuidutils from nova.api.metadata import base as instance_metadata +from nova.compute import vm_states import nova.conf from nova import exception from nova.i18n import _, _LI, _LE, _LW @@ -94,6 +96,7 @@ class VMOps(object): # The console log is stored in two files, each should have at most half of # the maximum console log size. _MAX_CONSOLE_LOG_FILE_SIZE = units.Mi / 2 + _ROOT_DISK_CTRL_ADDR = 0 def __init__(self): self._vmutils = utilsfactory.get_vmutils() @@ -146,13 +149,17 @@ class VMOps(object): num_cpu=info['NumberOfProcessors'], cpu_time_ns=info['UpTime']) - def _create_root_vhd(self, context, instance): - base_vhd_path = self._imagecache.get_cached_image(context, instance) + def _create_root_vhd(self, context, instance, rescue_image_id=None): + is_rescue_vhd = rescue_image_id is not None + + base_vhd_path = self._imagecache.get_cached_image(context, instance, + rescue_image_id) base_vhd_info = self._vhdutils.get_vhd_info(base_vhd_path) base_vhd_size = base_vhd_info['VirtualSize'] format_ext = base_vhd_path.split('.')[-1] root_vhd_path = self._pathutils.get_root_vhd_path(instance.name, - format_ext) + format_ext, + is_rescue_vhd) root_vhd_size = instance.root_gb * units.Gi try: @@ -182,9 +189,9 @@ class VMOps(object): self._vhdutils.get_internal_vhd_size_by_file_size( base_vhd_path, root_vhd_size)) - if self._is_resize_needed(root_vhd_path, base_vhd_size, - root_vhd_internal_size, - instance): + if not is_rescue_vhd and self._is_resize_needed( + root_vhd_path, base_vhd_size, + root_vhd_internal_size, instance): self._vhdutils.resize_vhd(root_vhd_path, root_vhd_internal_size, is_file_max_size=False) @@ -343,7 +350,7 @@ class VMOps(object): return vm_gen def _create_config_drive(self, instance, injected_files, admin_password, - network_info): + network_info, rescue=False): if CONF.config_drive_format != 'iso9660': raise exception.ConfigDriveUnsupportedFormat( format=CONF.config_drive_format) @@ -359,9 +366,8 @@ class VMOps(object): extra_md=extra_md, network_info=network_info) - instance_path = self._pathutils.get_instance_dir( - instance.name) - configdrive_path_iso = os.path.join(instance_path, 'configdrive.iso') + configdrive_path_iso = self._pathutils.get_configdrive_path( + instance.name, constants.DVD_FORMAT, rescue=rescue) LOG.info(_LI('Creating config drive at %(path)s'), {'path': configdrive_path_iso}, instance=instance) @@ -375,8 +381,8 @@ class VMOps(object): e, instance=instance) if not CONF.hyperv.config_drive_cdrom: - configdrive_path = os.path.join(instance_path, - 'configdrive.vhd') + configdrive_path = self._pathutils.get_configdrive_path( + instance.name, constants.DISK_FORMAT_VHD, rescue=rescue) utils.execute(CONF.hyperv.qemu_img_cmd, 'convert', '-f', @@ -404,6 +410,17 @@ class VMOps(object): except KeyError: raise exception.InvalidDiskFormat(disk_format=configdrive_ext) + def _detach_config_drive(self, instance_name, rescue=False, delete=False): + configdrive_path = self._pathutils.lookup_configdrive_path( + instance_name, rescue=rescue) + + if configdrive_path: + self._vmutils.detach_vm_disk(instance_name, + configdrive_path, + is_physical=False) + if delete: + self._pathutils.remove(configdrive_path) + def _delete_disk_files(self, instance_name): self._pathutils.get_instance_dir(instance_name, create_dir=False, @@ -662,3 +679,102 @@ class VMOps(object): "might have been destroyed beforehand.", instance=instance) raise exception.InterfaceDetachFailed(instance_uuid=instance.uuid) + + def rescue_instance(self, context, instance, network_info, image_meta, + rescue_password): + try: + self._rescue_instance(context, instance, network_info, + image_meta, rescue_password) + except Exception as exc: + with excutils.save_and_reraise_exception(): + err_msg = _LE("Instance rescue failed. Exception: %(exc)s. " + "Attempting to unrescue the instance.") + LOG.error(err_msg, {'exc': exc}, instance=instance) + self.unrescue_instance(instance) + + def _rescue_instance(self, context, instance, network_info, image_meta, + rescue_password): + rescue_image_id = image_meta.id or instance.image_ref + rescue_vhd_path = self._create_root_vhd( + context, instance, rescue_image_id=rescue_image_id) + + rescue_vm_gen = self.get_image_vm_generation(instance.uuid, + rescue_vhd_path, + image_meta) + vm_gen = self._vmutils.get_vm_generation(instance.name) + if rescue_vm_gen != vm_gen: + err_msg = _('The requested rescue image requires a different VM ' + 'generation than the actual rescued instance. ' + 'Rescue image VM generation: %(rescue_vm_gen)s. ' + 'Rescued instance VM generation: %(vm_gen)s.') % dict( + rescue_vm_gen=rescue_vm_gen, + vm_gen=vm_gen) + raise exception.ImageUnacceptable(reason=err_msg, + image_id=rescue_image_id) + + root_vhd_path = self._pathutils.lookup_root_vhd_path(instance.name) + if not root_vhd_path: + err_msg = _('Instance root disk image could not be found. ' + 'Rescuing instances booted from volume is ' + 'not supported.') + raise exception.InstanceNotRescuable(reason=err_msg, + instance_id=instance.uuid) + + controller_type = VM_GENERATIONS_CONTROLLER_TYPES[vm_gen] + + self._vmutils.detach_vm_disk(instance.name, root_vhd_path, + is_physical=False) + self._attach_drive(instance.name, rescue_vhd_path, 0, + self._ROOT_DISK_CTRL_ADDR, controller_type) + self._vmutils.attach_scsi_drive(instance.name, root_vhd_path, + drive_type=constants.DISK) + + if configdrive.required_by(instance): + self._detach_config_drive(instance.name) + rescue_configdrive_path = self._create_config_drive( + instance, + injected_files=None, + admin_password=rescue_password, + network_info=network_info, + rescue=True) + self.attach_config_drive(instance, rescue_configdrive_path, + vm_gen) + + self.power_on(instance) + + def unrescue_instance(self, instance): + self.power_off(instance) + + root_vhd_path = self._pathutils.lookup_root_vhd_path(instance.name) + rescue_vhd_path = self._pathutils.lookup_root_vhd_path(instance.name, + rescue=True) + + if (instance.vm_state == vm_states.RESCUED and + not (rescue_vhd_path and root_vhd_path)): + err_msg = _('Missing instance root and/or rescue image. ' + 'The instance cannot be unrescued.') + raise exception.InstanceNotRescuable(reason=err_msg, + instance_id=instance.uuid) + + vm_gen = self._vmutils.get_vm_generation(instance.name) + controller_type = VM_GENERATIONS_CONTROLLER_TYPES[vm_gen] + + self._vmutils.detach_vm_disk(instance.name, root_vhd_path, + is_physical=False) + if rescue_vhd_path: + self._vmutils.detach_vm_disk(instance.name, rescue_vhd_path, + is_physical=False) + fileutils.delete_if_exists(rescue_vhd_path) + self._attach_drive(instance.name, root_vhd_path, 0, + self._ROOT_DISK_CTRL_ADDR, controller_type) + + self._detach_config_drive(instance.name, rescue=True, delete=True) + + # Reattach the configdrive, if exists and not already attached. + configdrive_path = self._pathutils.lookup_configdrive_path( + instance.name) + if configdrive_path and not self._vmutils.is_disk_attached( + configdrive_path, is_physical=False): + self.attach_config_drive(instance, configdrive_path, vm_gen) + + self.power_on(instance)

AltStyle によって変換されたページ (->オリジナル) /