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
This commit is contained in:
parent
8dd4b85093
commit
3f96f3039a
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user