Hyper-V: properly handle shared storage during migrations
In case of migrations, we attempt to move the instance files without checking whether shared storage is used. Note that the block_migration flag is ignored. After a live migration is performed, we always try to delete the instance files. Also, if the migration fails, the destination node is not cleaned up, as the HyperVLiveMigrateData object is not used at the moment. This change addresses those issues. Partial-Bug: #1565895 Change-Id: I0ac0a2d2e7a8771024a486dd5931bd05b1ecd074
This commit is contained in:
@@ -5345,8 +5345,8 @@ class ComputeManager(manager.Manager):
|
|||||||
"""
|
"""
|
||||||
# NOTE(pkoniszewski): block migration specific params are set inside
|
# NOTE(pkoniszewski): block migration specific params are set inside
|
||||||
# migrate_data objects for drivers that expose block live migration
|
# migrate_data objects for drivers that expose block live migration
|
||||||
# information (i.e. Libvirt and Xenapi). For other drivers cleanup is
|
# information (i.e. Libvirt, Xenapi and HyperV). For other drivers
|
||||||
# not needed.
|
# cleanup is not needed.
|
||||||
is_shared_block_storage = True
|
is_shared_block_storage = True
|
||||||
is_shared_instance_path = True
|
is_shared_instance_path = True
|
||||||
if isinstance(migrate_data, migrate_data_obj.LibvirtLiveMigrateData):
|
if isinstance(migrate_data, migrate_data_obj.LibvirtLiveMigrateData):
|
||||||
@@ -5355,6 +5355,9 @@ class ComputeManager(manager.Manager):
|
|||||||
elif isinstance(migrate_data, migrate_data_obj.XenapiLiveMigrateData):
|
elif isinstance(migrate_data, migrate_data_obj.XenapiLiveMigrateData):
|
||||||
is_shared_block_storage = not migrate_data.block_migration
|
is_shared_block_storage = not migrate_data.block_migration
|
||||||
is_shared_instance_path = not migrate_data.block_migration
|
is_shared_instance_path = not migrate_data.block_migration
|
||||||
|
elif isinstance(migrate_data, migrate_data_obj.HyperVLiveMigrateData):
|
||||||
|
is_shared_instance_path = migrate_data.is_shared_instance_path
|
||||||
|
is_shared_block_storage = migrate_data.is_shared_instance_path
|
||||||
|
|
||||||
# No instance booting at source host, but instance dir
|
# No instance booting at source host, but instance dir
|
||||||
# must be deleted for preparing next block migration
|
# must be deleted for preparing next block migration
|
||||||
|
|||||||
@@ -5030,6 +5030,22 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase):
|
|||||||
self.assertFalse(do_cleanup)
|
self.assertFalse(do_cleanup)
|
||||||
self.assertFalse(destroy_disks)
|
self.assertFalse(destroy_disks)
|
||||||
|
|
||||||
|
def test_live_migration_cleanup_flags_block_migrate_hyperv(self):
|
||||||
|
migrate_data = objects.HyperVLiveMigrateData(
|
||||||
|
is_shared_instance_path=False)
|
||||||
|
do_cleanup, destroy_disks = self.compute._live_migration_cleanup_flags(
|
||||||
|
migrate_data)
|
||||||
|
self.assertTrue(do_cleanup)
|
||||||
|
self.assertTrue(destroy_disks)
|
||||||
|
|
||||||
|
def test_live_migration_cleanup_flags_shared_hyperv(self):
|
||||||
|
migrate_data = objects.HyperVLiveMigrateData(
|
||||||
|
is_shared_instance_path=True)
|
||||||
|
do_cleanup, destroy_disks = self.compute._live_migration_cleanup_flags(
|
||||||
|
migrate_data)
|
||||||
|
self.assertFalse(do_cleanup)
|
||||||
|
self.assertFalse(destroy_disks)
|
||||||
|
|
||||||
|
|
||||||
class ComputeManagerInstanceUsageAuditTestCase(test.TestCase):
|
class ComputeManagerInstanceUsageAuditTestCase(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -292,14 +292,16 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase):
|
|||||||
|
|
||||||
mock_destroy.assert_called_once_with(
|
mock_destroy.assert_called_once_with(
|
||||||
mock.sentinel.context, mock.sentinel.instance,
|
mock.sentinel.context, mock.sentinel.instance,
|
||||||
mock.sentinel.network_info, mock.sentinel.block_device_info)
|
mock.sentinel.network_info, mock.sentinel.block_device_info,
|
||||||
|
destroy_disks=mock.sentinel.destroy_disks)
|
||||||
|
|
||||||
def test_pre_live_migration(self):
|
def test_pre_live_migration(self):
|
||||||
self.driver.pre_live_migration(
|
migrate_data = self.driver.pre_live_migration(
|
||||||
mock.sentinel.context, mock.sentinel.instance,
|
mock.sentinel.context, mock.sentinel.instance,
|
||||||
mock.sentinel.block_device_info, mock.sentinel.network_info,
|
mock.sentinel.block_device_info, mock.sentinel.network_info,
|
||||||
mock.sentinel.disk_info, mock.sentinel.migrate_data)
|
mock.sentinel.disk_info, mock.sentinel.migrate_data)
|
||||||
|
|
||||||
|
self.assertEqual(mock.sentinel.migrate_data, migrate_data)
|
||||||
pre_live_migration = self.driver._livemigrationops.pre_live_migration
|
pre_live_migration = self.driver._livemigrationops.pre_live_migration
|
||||||
pre_live_migration.assert_called_once_with(
|
pre_live_migration.assert_called_once_with(
|
||||||
mock.sentinel.context, mock.sentinel.instance,
|
mock.sentinel.context, mock.sentinel.instance,
|
||||||
@@ -313,7 +315,8 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase):
|
|||||||
post_live_migration = self.driver._livemigrationops.post_live_migration
|
post_live_migration = self.driver._livemigrationops.post_live_migration
|
||||||
post_live_migration.assert_called_once_with(
|
post_live_migration.assert_called_once_with(
|
||||||
mock.sentinel.context, mock.sentinel.instance,
|
mock.sentinel.context, mock.sentinel.instance,
|
||||||
mock.sentinel.block_device_info)
|
mock.sentinel.block_device_info,
|
||||||
|
mock.sentinel.migrate_data)
|
||||||
|
|
||||||
def test_post_live_migration_at_destination(self):
|
def test_post_live_migration_at_destination(self):
|
||||||
self.driver.post_live_migration_at_destination(
|
self.driver.post_live_migration_at_destination(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import mock
|
|||||||
from os_win import exceptions as os_win_exc
|
from os_win import exceptions as os_win_exc
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from nova.objects import migrate_data as migrate_data_obj
|
||||||
from nova.tests.unit import fake_instance
|
from nova.tests.unit import fake_instance
|
||||||
from nova.tests.unit.virt.hyperv import test_base
|
from nova.tests.unit.virt.hyperv import test_base
|
||||||
from nova.virt.hyperv import livemigrationops
|
from nova.virt.hyperv import livemigrationops
|
||||||
@@ -35,49 +36,103 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
|||||||
self._livemigrops._livemigrutils = mock.MagicMock()
|
self._livemigrops._livemigrutils = mock.MagicMock()
|
||||||
self._livemigrops._pathutils = mock.MagicMock()
|
self._livemigrops._pathutils = mock.MagicMock()
|
||||||
self._livemigrops._block_dev_man = mock.MagicMock()
|
self._livemigrops._block_dev_man = mock.MagicMock()
|
||||||
|
self._pathutils = self._livemigrops._pathutils
|
||||||
|
|
||||||
@mock.patch.object(serialconsoleops.SerialConsoleOps,
|
@mock.patch.object(serialconsoleops.SerialConsoleOps,
|
||||||
'stop_console_handler')
|
'stop_console_handler')
|
||||||
@mock.patch('nova.virt.hyperv.vmops.VMOps.copy_vm_dvd_disks')
|
@mock.patch('nova.virt.hyperv.vmops.VMOps.copy_vm_dvd_disks')
|
||||||
def _test_live_migration(self, mock_get_vm_dvd_paths,
|
def _test_live_migration(self, mock_copy_dvd_disk,
|
||||||
mock_stop_console_handler, side_effect):
|
mock_stop_console_handler,
|
||||||
|
side_effect=None,
|
||||||
|
shared_storage=False,
|
||||||
|
migrate_data_received=True,
|
||||||
|
migrate_data_version='1.1'):
|
||||||
mock_instance = fake_instance.fake_instance_obj(self.context)
|
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||||
mock_post = mock.MagicMock()
|
mock_post = mock.MagicMock()
|
||||||
mock_recover = mock.MagicMock()
|
mock_recover = mock.MagicMock()
|
||||||
mock_copy_logs = self._livemigrops._pathutils.copy_vm_console_logs
|
mock_copy_logs = self._livemigrops._pathutils.copy_vm_console_logs
|
||||||
fake_dest = mock.sentinel.DESTINATION
|
fake_dest = mock.sentinel.DESTINATION
|
||||||
|
mock_check_shared_inst_dir = (
|
||||||
|
self._pathutils.check_remote_instances_dir_shared)
|
||||||
|
mock_check_shared_inst_dir.return_value = shared_storage
|
||||||
self._livemigrops._livemigrutils.live_migrate_vm.side_effect = [
|
self._livemigrops._livemigrutils.live_migrate_vm.side_effect = [
|
||||||
side_effect]
|
side_effect]
|
||||||
|
|
||||||
|
if migrate_data_received:
|
||||||
|
migrate_data = migrate_data_obj.HyperVLiveMigrateData()
|
||||||
|
if migrate_data_version != '1.0':
|
||||||
|
migrate_data.is_shared_instance_path = shared_storage
|
||||||
|
else:
|
||||||
|
migrate_data = None
|
||||||
|
|
||||||
if side_effect is os_win_exc.HyperVException:
|
if side_effect is os_win_exc.HyperVException:
|
||||||
self.assertRaises(os_win_exc.HyperVException,
|
self.assertRaises(os_win_exc.HyperVException,
|
||||||
self._livemigrops.live_migration,
|
self._livemigrops.live_migration,
|
||||||
self.context, mock_instance, fake_dest,
|
self.context, mock_instance, fake_dest,
|
||||||
mock_post, mock_recover, False, None)
|
mock_post, mock_recover,
|
||||||
|
mock.sentinel.block_migr,
|
||||||
|
migrate_data)
|
||||||
mock_recover.assert_called_once_with(self.context, mock_instance,
|
mock_recover.assert_called_once_with(self.context, mock_instance,
|
||||||
fake_dest, False)
|
fake_dest,
|
||||||
|
mock.sentinel.block_migr,
|
||||||
|
migrate_data)
|
||||||
else:
|
else:
|
||||||
self._livemigrops.live_migration(context=self.context,
|
self._livemigrops.live_migration(context=self.context,
|
||||||
instance_ref=mock_instance,
|
instance_ref=mock_instance,
|
||||||
dest=fake_dest,
|
dest=fake_dest,
|
||||||
post_method=mock_post,
|
post_method=mock_post,
|
||||||
recover_method=mock_recover)
|
recover_method=mock_recover,
|
||||||
|
block_migration=(
|
||||||
|
mock.sentinel.block_migr),
|
||||||
|
migrate_data=migrate_data)
|
||||||
|
post_call_args = mock_post.call_args_list
|
||||||
|
self.assertEqual(1, len(post_call_args))
|
||||||
|
|
||||||
mock_stop_console_handler.assert_called_once_with(
|
post_call_args_list = post_call_args[0][0]
|
||||||
mock_instance.name)
|
self.assertEqual((self.context, mock_instance,
|
||||||
|
fake_dest, mock.sentinel.block_migr),
|
||||||
|
post_call_args_list[:-1])
|
||||||
|
# The last argument, the migrate_data object, should be created
|
||||||
|
# by the callee if not received.
|
||||||
|
migrate_data_arg = post_call_args_list[-1]
|
||||||
|
self.assertIsInstance(
|
||||||
|
migrate_data_arg,
|
||||||
|
migrate_data_obj.HyperVLiveMigrateData)
|
||||||
|
self.assertEqual(shared_storage,
|
||||||
|
migrate_data_arg.is_shared_instance_path)
|
||||||
|
|
||||||
|
if not migrate_data_received or migrate_data_version == '1.0':
|
||||||
|
mock_check_shared_inst_dir.assert_called_once_with(fake_dest)
|
||||||
|
else:
|
||||||
|
self.assertFalse(mock_check_shared_inst_dir.called)
|
||||||
|
|
||||||
|
mock_stop_console_handler.assert_called_once_with(mock_instance.name)
|
||||||
|
|
||||||
|
if not shared_storage:
|
||||||
mock_copy_logs.assert_called_once_with(mock_instance.name,
|
mock_copy_logs.assert_called_once_with(mock_instance.name,
|
||||||
fake_dest)
|
fake_dest)
|
||||||
mock_live_migr = self._livemigrops._livemigrutils.live_migrate_vm
|
mock_copy_dvd_disk.assert_called_once_with(mock_instance.name,
|
||||||
mock_live_migr.assert_called_once_with(mock_instance.name,
|
fake_dest)
|
||||||
fake_dest)
|
else:
|
||||||
mock_post.assert_called_once_with(self.context, mock_instance,
|
self.assertFalse(mock_copy_logs.called)
|
||||||
fake_dest, False)
|
self.assertFalse(mock_copy_dvd_disk.called)
|
||||||
|
|
||||||
|
mock_live_migr = self._livemigrops._livemigrutils.live_migrate_vm
|
||||||
|
mock_live_migr.assert_called_once_with(mock_instance.name,
|
||||||
|
fake_dest)
|
||||||
|
|
||||||
def test_live_migration(self):
|
def test_live_migration(self):
|
||||||
self._test_live_migration(side_effect=None)
|
self._test_live_migration(migrate_data_received=False)
|
||||||
|
|
||||||
|
def test_live_migration_old_migrate_data_version(self):
|
||||||
|
self._test_live_migration(migrate_data_version='1.0')
|
||||||
|
|
||||||
def test_live_migration_exception(self):
|
def test_live_migration_exception(self):
|
||||||
self._test_live_migration(side_effect=os_win_exc.HyperVException)
|
self._test_live_migration(side_effect=os_win_exc.HyperVException)
|
||||||
|
|
||||||
|
def test_live_migration_shared_storage(self):
|
||||||
|
self._test_live_migration(shared_storage=True)
|
||||||
|
|
||||||
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps.get_disk_path_mapping')
|
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps.get_disk_path_mapping')
|
||||||
@mock.patch('nova.virt.hyperv.imagecache.ImageCache.get_cached_image')
|
@mock.patch('nova.virt.hyperv.imagecache.ImageCache.get_cached_image')
|
||||||
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps'
|
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps'
|
||||||
@@ -125,11 +180,43 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
|||||||
self._test_pre_live_migration(phys_disks_attached=False)
|
self._test_pre_live_migration(phys_disks_attached=False)
|
||||||
|
|
||||||
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps.disconnect_volumes')
|
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps.disconnect_volumes')
|
||||||
def test_post_live_migration(self, mock_disconnect_volumes):
|
def _test_post_live_migration(self, mock_disconnect_volumes,
|
||||||
|
shared_storage=False):
|
||||||
|
migrate_data = migrate_data_obj.HyperVLiveMigrateData(
|
||||||
|
is_shared_instance_path=shared_storage)
|
||||||
|
|
||||||
self._livemigrops.post_live_migration(
|
self._livemigrops.post_live_migration(
|
||||||
self.context, mock.sentinel.instance,
|
self.context, mock.sentinel.instance,
|
||||||
mock.sentinel.block_device_info)
|
mock.sentinel.block_device_info,
|
||||||
|
migrate_data)
|
||||||
mock_disconnect_volumes.assert_called_once_with(
|
mock_disconnect_volumes.assert_called_once_with(
|
||||||
mock.sentinel.block_device_info)
|
mock.sentinel.block_device_info)
|
||||||
self._livemigrops._pathutils.get_instance_dir.assert_called_once_with(
|
mock_get_inst_dir = self._pathutils.get_instance_dir
|
||||||
mock.sentinel.instance.name, create_dir=False, remove_dir=True)
|
|
||||||
|
if not shared_storage:
|
||||||
|
mock_get_inst_dir.assert_called_once_with(
|
||||||
|
mock.sentinel.instance.name,
|
||||||
|
create_dir=False, remove_dir=True)
|
||||||
|
else:
|
||||||
|
self.assertFalse(mock_get_inst_dir.called)
|
||||||
|
|
||||||
|
def test_post_block_migration(self):
|
||||||
|
self._test_post_live_migration()
|
||||||
|
|
||||||
|
def test_post_live_migration_shared_storage(self):
|
||||||
|
self._test_post_live_migration(shared_storage=True)
|
||||||
|
|
||||||
|
@mock.patch.object(migrate_data_obj, 'HyperVLiveMigrateData')
|
||||||
|
def test_check_can_live_migrate_destination(self, mock_migr_data_cls):
|
||||||
|
mock_instance = fake_instance.fake_instance_obj(self.context)
|
||||||
|
migr_data = self._livemigrops.check_can_live_migrate_destination(
|
||||||
|
mock.sentinel.context, mock_instance, mock.sentinel.src_comp_info,
|
||||||
|
mock.sentinel.dest_comp_info)
|
||||||
|
|
||||||
|
mock_check_shared_inst_dir = (
|
||||||
|
self._pathutils.check_remote_instances_dir_shared)
|
||||||
|
mock_check_shared_inst_dir.assert_called_once_with(mock_instance.host)
|
||||||
|
|
||||||
|
self.assertEqual(mock_migr_data_cls.return_value, migr_data)
|
||||||
|
self.assertEqual(mock_check_shared_inst_dir.return_value,
|
||||||
|
migr_data.is_shared_instance_path)
|
||||||
|
|||||||
@@ -49,16 +49,21 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
|||||||
self._migrationops._imagecache = mock.MagicMock()
|
self._migrationops._imagecache = mock.MagicMock()
|
||||||
self._migrationops._block_dev_man = mock.MagicMock()
|
self._migrationops._block_dev_man = mock.MagicMock()
|
||||||
|
|
||||||
def _check_migrate_disk_files(self, host):
|
def _check_migrate_disk_files(self, shared_storage=False):
|
||||||
instance_path = 'fake/instance/path'
|
instance_path = 'fake/instance/path'
|
||||||
self._migrationops._pathutils.get_instance_dir.return_value = (
|
dest_instance_path = 'remote/instance/path'
|
||||||
instance_path)
|
self._migrationops._pathutils.get_instance_dir.side_effect = (
|
||||||
|
instance_path, dest_instance_path)
|
||||||
get_revert_dir = (
|
get_revert_dir = (
|
||||||
self._migrationops._pathutils.get_instance_migr_revert_dir)
|
self._migrationops._pathutils.get_instance_migr_revert_dir)
|
||||||
self._migrationops._hostutils.get_local_ips.return_value = [host]
|
check_shared_storage = (
|
||||||
|
self._migrationops._pathutils.check_dirs_shared_storage)
|
||||||
|
check_shared_storage.return_value = shared_storage
|
||||||
self._migrationops._pathutils.exists.return_value = True
|
self._migrationops._pathutils.exists.return_value = True
|
||||||
|
|
||||||
expected_get_dir = [mock.call(mock.sentinel.instance_name)]
|
expected_get_dir = [mock.call(mock.sentinel.instance_name),
|
||||||
|
mock.call(mock.sentinel.instance_name,
|
||||||
|
mock.sentinel.dest_path)]
|
||||||
expected_move_calls = [mock.call(instance_path,
|
expected_move_calls = [mock.call(instance_path,
|
||||||
get_revert_dir.return_value)]
|
get_revert_dir.return_value)]
|
||||||
|
|
||||||
@@ -67,24 +72,26 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
|||||||
disk_files=[self._FAKE_DISK],
|
disk_files=[self._FAKE_DISK],
|
||||||
dest=mock.sentinel.dest_path)
|
dest=mock.sentinel.dest_path)
|
||||||
|
|
||||||
self._migrationops._hostutils.get_local_ips.assert_called_once_with()
|
self._migrationops._pathutils.exists.assert_called_once_with(
|
||||||
|
dest_instance_path)
|
||||||
|
check_shared_storage.assert_called_once_with(
|
||||||
|
instance_path, dest_instance_path)
|
||||||
get_revert_dir.assert_called_with(mock.sentinel.instance_name,
|
get_revert_dir.assert_called_with(mock.sentinel.instance_name,
|
||||||
remove_dir=True, create_dir=True)
|
remove_dir=True, create_dir=True)
|
||||||
if host == mock.sentinel.dest_path:
|
if shared_storage:
|
||||||
fake_dest_path = '%s_tmp' % instance_path
|
fake_dest_path = '%s_tmp' % instance_path
|
||||||
self._migrationops._pathutils.exists.assert_called_once_with(
|
|
||||||
fake_dest_path)
|
|
||||||
self._migrationops._pathutils.rmtree.assert_called_once_with(
|
|
||||||
fake_dest_path)
|
|
||||||
self._migrationops._pathutils.makedirs.assert_called_once_with(
|
|
||||||
fake_dest_path)
|
|
||||||
expected_move_calls.append(mock.call(fake_dest_path,
|
expected_move_calls.append(mock.call(fake_dest_path,
|
||||||
instance_path))
|
instance_path))
|
||||||
|
self._migrationops._pathutils.rmtree.assert_called_once_with(
|
||||||
|
fake_dest_path)
|
||||||
else:
|
else:
|
||||||
fake_dest_path = instance_path
|
fake_dest_path = dest_instance_path
|
||||||
expected_get_dir.append(mock.call(mock.sentinel.instance_name,
|
|
||||||
mock.sentinel.dest_path,
|
self._migrationops._pathutils.makedirs.assert_called_once_with(
|
||||||
remove_dir=True))
|
fake_dest_path)
|
||||||
|
check_remove_dir = self._migrationops._pathutils.check_remove_dir
|
||||||
|
check_remove_dir.assert_called_once_with(fake_dest_path)
|
||||||
|
|
||||||
self._migrationops._pathutils.get_instance_dir.assert_has_calls(
|
self._migrationops._pathutils.get_instance_dir.assert_has_calls(
|
||||||
expected_get_dir)
|
expected_get_dir)
|
||||||
self._migrationops._pathutils.copy.assert_called_once_with(
|
self._migrationops._pathutils.copy.assert_called_once_with(
|
||||||
@@ -93,10 +100,10 @@ class MigrationOpsTestCase(test_base.HyperVBaseTestCase):
|
|||||||
expected_move_calls)
|
expected_move_calls)
|
||||||
|
|
||||||
def test_migrate_disk_files(self):
|
def test_migrate_disk_files(self):
|
||||||
self._check_migrate_disk_files(host=mock.sentinel.other_dest_path)
|
self._check_migrate_disk_files()
|
||||||
|
|
||||||
def test_migrate_disk_files_same_host(self):
|
def test_migrate_disk_files_same_host(self):
|
||||||
self._check_migrate_disk_files(host=mock.sentinel.dest_path)
|
self._check_migrate_disk_files(shared_storage=True)
|
||||||
|
|
||||||
@mock.patch.object(migrationops.MigrationOps,
|
@mock.patch.object(migrationops.MigrationOps,
|
||||||
'_cleanup_failed_disk_migration')
|
'_cleanup_failed_disk_migration')
|
||||||
|
|||||||
@@ -176,3 +176,39 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase):
|
|||||||
self.assertEqual(42, actual_age)
|
self.assertEqual(42, actual_age)
|
||||||
mock_time.time.assert_called_once_with()
|
mock_time.time.assert_called_once_with()
|
||||||
mock_getmtime.assert_called_once_with(mock.sentinel.filename)
|
mock_getmtime.assert_called_once_with(mock.sentinel.filename)
|
||||||
|
|
||||||
|
@mock.patch('os.path.exists')
|
||||||
|
@mock.patch('tempfile.NamedTemporaryFile')
|
||||||
|
def test_check_dirs_shared_storage(self, mock_named_tempfile,
|
||||||
|
mock_exists):
|
||||||
|
fake_src_dir = 'fake_src_dir'
|
||||||
|
fake_dest_dir = 'fake_dest_dir'
|
||||||
|
|
||||||
|
mock_exists.return_value = True
|
||||||
|
mock_tmpfile = mock_named_tempfile.return_value.__enter__.return_value
|
||||||
|
mock_tmpfile.name = 'fake_tmp_fname'
|
||||||
|
expected_src_tmp_path = os.path.join(fake_src_dir,
|
||||||
|
mock_tmpfile.name)
|
||||||
|
|
||||||
|
self._pathutils.check_dirs_shared_storage(
|
||||||
|
fake_src_dir, fake_dest_dir)
|
||||||
|
|
||||||
|
mock_named_tempfile.assert_called_once_with(dir=fake_dest_dir)
|
||||||
|
mock_exists.assert_called_once_with(expected_src_tmp_path)
|
||||||
|
|
||||||
|
@mock.patch.object(pathutils.PathUtils, 'check_dirs_shared_storage')
|
||||||
|
@mock.patch.object(pathutils.PathUtils, 'get_instances_dir')
|
||||||
|
def test_check_remote_instances_shared(self, mock_get_instances_dir,
|
||||||
|
mock_check_dirs_shared_storage):
|
||||||
|
mock_get_instances_dir.side_effect = [mock.sentinel.local_inst_dir,
|
||||||
|
mock.sentinel.remote_inst_dir]
|
||||||
|
|
||||||
|
shared_storage = self._pathutils.check_remote_instances_dir_shared(
|
||||||
|
mock.sentinel.dest)
|
||||||
|
|
||||||
|
self.assertEqual(mock_check_dirs_shared_storage.return_value,
|
||||||
|
shared_storage)
|
||||||
|
mock_get_instances_dir.assert_has_calls(
|
||||||
|
[mock.call(), mock.call(mock.sentinel.dest)])
|
||||||
|
mock_check_dirs_shared_storage.assert_called_once_with(
|
||||||
|
mock.sentinel.local_inst_dir, mock.sentinel.remote_inst_dir)
|
||||||
|
|||||||
@@ -233,18 +233,21 @@ class HyperVDriver(driver.ComputeDriver):
|
|||||||
block_device_info,
|
block_device_info,
|
||||||
destroy_disks=True,
|
destroy_disks=True,
|
||||||
migrate_data=None):
|
migrate_data=None):
|
||||||
self.destroy(context, instance, network_info, block_device_info)
|
self.destroy(context, instance, network_info, block_device_info,
|
||||||
|
destroy_disks=destroy_disks)
|
||||||
|
|
||||||
def pre_live_migration(self, context, instance, block_device_info,
|
def pre_live_migration(self, context, instance, block_device_info,
|
||||||
network_info, disk_info, migrate_data=None):
|
network_info, disk_info, migrate_data=None):
|
||||||
self._livemigrationops.pre_live_migration(context, instance,
|
self._livemigrationops.pre_live_migration(context, instance,
|
||||||
block_device_info,
|
block_device_info,
|
||||||
network_info)
|
network_info)
|
||||||
|
return migrate_data
|
||||||
|
|
||||||
def post_live_migration(self, context, instance, block_device_info,
|
def post_live_migration(self, context, instance, block_device_info,
|
||||||
migrate_data=None):
|
migrate_data=None):
|
||||||
self._livemigrationops.post_live_migration(context, instance,
|
self._livemigrationops.post_live_migration(context, instance,
|
||||||
block_device_info)
|
block_device_info,
|
||||||
|
migrate_data)
|
||||||
|
|
||||||
def post_live_migration_at_destination(self, context, instance,
|
def post_live_migration_at_destination(self, context, instance,
|
||||||
network_info,
|
network_info,
|
||||||
|
|||||||
@@ -51,25 +51,39 @@ class LiveMigrationOps(object):
|
|||||||
LOG.debug("live_migration called", instance=instance_ref)
|
LOG.debug("live_migration called", instance=instance_ref)
|
||||||
instance_name = instance_ref["name"]
|
instance_name = instance_ref["name"]
|
||||||
|
|
||||||
try:
|
if migrate_data and 'is_shared_instance_path' in migrate_data:
|
||||||
self._vmops.copy_vm_dvd_disks(instance_name, dest)
|
shared_storage = migrate_data.is_shared_instance_path
|
||||||
|
else:
|
||||||
|
shared_storage = (
|
||||||
|
self._pathutils.check_remote_instances_dir_shared(dest))
|
||||||
|
if migrate_data:
|
||||||
|
migrate_data.is_shared_instance_path = shared_storage
|
||||||
|
else:
|
||||||
|
migrate_data = migrate_data_obj.HyperVLiveMigrateData(
|
||||||
|
is_shared_instance_path=shared_storage)
|
||||||
|
|
||||||
|
try:
|
||||||
# We must make sure that the console log workers are stopped,
|
# We must make sure that the console log workers are stopped,
|
||||||
# otherwise we won't be able to delete / move VM log files.
|
# otherwise we won't be able to delete / move VM log files.
|
||||||
self._serial_console_ops.stop_console_handler(instance_name)
|
self._serial_console_ops.stop_console_handler(instance_name)
|
||||||
|
|
||||||
self._pathutils.copy_vm_console_logs(instance_name, dest)
|
if not shared_storage:
|
||||||
|
self._pathutils.copy_vm_console_logs(instance_name, dest)
|
||||||
|
self._vmops.copy_vm_dvd_disks(instance_name, dest)
|
||||||
|
|
||||||
self._livemigrutils.live_migrate_vm(instance_name,
|
self._livemigrutils.live_migrate_vm(instance_name,
|
||||||
dest)
|
dest)
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.debug("Calling live migration recover_method "
|
LOG.debug("Calling live migration recover_method "
|
||||||
"for instance: %s", instance_name)
|
"for instance: %s", instance_name)
|
||||||
recover_method(context, instance_ref, dest, block_migration)
|
recover_method(context, instance_ref, dest, block_migration,
|
||||||
|
migrate_data)
|
||||||
|
|
||||||
LOG.debug("Calling live migration post_method for instance: %s",
|
LOG.debug("Calling live migration post_method for instance: %s",
|
||||||
instance_name)
|
instance_name)
|
||||||
post_method(context, instance_ref, dest, block_migration)
|
post_method(context, instance_ref, dest,
|
||||||
|
block_migration, migrate_data)
|
||||||
|
|
||||||
def pre_live_migration(self, context, instance, block_device_info,
|
def pre_live_migration(self, context, instance, block_device_info,
|
||||||
network_info):
|
network_info):
|
||||||
@@ -93,11 +107,14 @@ class LiveMigrationOps(object):
|
|||||||
instance.host,
|
instance.host,
|
||||||
disk_path_mapping)
|
disk_path_mapping)
|
||||||
|
|
||||||
def post_live_migration(self, context, instance, block_device_info):
|
def post_live_migration(self, context, instance, block_device_info,
|
||||||
|
migrate_data):
|
||||||
self._volumeops.disconnect_volumes(block_device_info)
|
self._volumeops.disconnect_volumes(block_device_info)
|
||||||
self._pathutils.get_instance_dir(instance.name,
|
|
||||||
create_dir=False,
|
if not migrate_data.is_shared_instance_path:
|
||||||
remove_dir=True)
|
self._pathutils.get_instance_dir(instance.name,
|
||||||
|
create_dir=False,
|
||||||
|
remove_dir=True)
|
||||||
|
|
||||||
def post_live_migration_at_destination(self, ctxt, instance_ref,
|
def post_live_migration_at_destination(self, ctxt, instance_ref,
|
||||||
network_info, block_migration):
|
network_info, block_migration):
|
||||||
@@ -108,8 +125,14 @@ class LiveMigrationOps(object):
|
|||||||
src_compute_info, dst_compute_info,
|
src_compute_info, dst_compute_info,
|
||||||
block_migration=False,
|
block_migration=False,
|
||||||
disk_over_commit=False):
|
disk_over_commit=False):
|
||||||
LOG.debug("check_can_live_migrate_destination called", instance_ref)
|
LOG.debug("check_can_live_migrate_destination called",
|
||||||
return migrate_data_obj.HyperVLiveMigrateData()
|
instance=instance_ref)
|
||||||
|
|
||||||
|
migrate_data = migrate_data_obj.HyperVLiveMigrateData()
|
||||||
|
migrate_data.is_shared_instance_path = (
|
||||||
|
self._pathutils.check_remote_instances_dir_shared(
|
||||||
|
instance_ref.host))
|
||||||
|
return migrate_data
|
||||||
|
|
||||||
def cleanup_live_migration_destination_check(self, ctxt, dest_check_data):
|
def cleanup_live_migration_destination_check(self, ctxt, dest_check_data):
|
||||||
LOG.debug("cleanup_live_migration_destination_check called")
|
LOG.debug("cleanup_live_migration_destination_check called")
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class MigrationOps(object):
|
class MigrationOps(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._hostutils = utilsfactory.get_hostutils()
|
|
||||||
self._vmutils = utilsfactory.get_vmutils()
|
self._vmutils = utilsfactory.get_vmutils()
|
||||||
self._vhdutils = utilsfactory.get_vhdutils()
|
self._vhdutils = utilsfactory.get_vhdutils()
|
||||||
self._pathutils = pathutils.PathUtils()
|
self._pathutils = pathutils.PathUtils()
|
||||||
@@ -51,29 +50,27 @@ class MigrationOps(object):
|
|||||||
def _migrate_disk_files(self, instance_name, disk_files, dest):
|
def _migrate_disk_files(self, instance_name, disk_files, dest):
|
||||||
# TODO(mikal): it would be nice if this method took a full instance,
|
# TODO(mikal): it would be nice if this method took a full instance,
|
||||||
# because it could then be passed to the log messages below.
|
# because it could then be passed to the log messages below.
|
||||||
same_host = False
|
|
||||||
if dest in self._hostutils.get_local_ips():
|
|
||||||
same_host = True
|
|
||||||
LOG.debug("Migration target is the source host")
|
|
||||||
else:
|
|
||||||
LOG.debug("Migration target host: %s", dest)
|
|
||||||
|
|
||||||
instance_path = self._pathutils.get_instance_dir(instance_name)
|
instance_path = self._pathutils.get_instance_dir(instance_name)
|
||||||
|
dest_path = self._pathutils.get_instance_dir(instance_name, dest)
|
||||||
revert_path = self._pathutils.get_instance_migr_revert_dir(
|
revert_path = self._pathutils.get_instance_migr_revert_dir(
|
||||||
instance_name, remove_dir=True, create_dir=True)
|
instance_name, remove_dir=True, create_dir=True)
|
||||||
dest_path = None
|
|
||||||
|
shared_storage = (self._pathutils.exists(dest_path) and
|
||||||
|
self._pathutils.check_dirs_shared_storage(
|
||||||
|
instance_path, dest_path))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if same_host:
|
if shared_storage:
|
||||||
# Since source and target are the same, we copy the files to
|
# Since source and target are the same, we copy the files to
|
||||||
# a temporary location before moving them into place
|
# a temporary location before moving them into place.
|
||||||
|
# This applies when the migration target is the source host or
|
||||||
|
# when shared storage is used for the instance files.
|
||||||
dest_path = '%s_tmp' % instance_path
|
dest_path = '%s_tmp' % instance_path
|
||||||
if self._pathutils.exists(dest_path):
|
|
||||||
self._pathutils.rmtree(dest_path)
|
self._pathutils.check_remove_dir(dest_path)
|
||||||
self._pathutils.makedirs(dest_path)
|
self._pathutils.makedirs(dest_path)
|
||||||
else:
|
|
||||||
dest_path = self._pathutils.get_instance_dir(
|
|
||||||
instance_name, dest, remove_dir=True)
|
|
||||||
for disk_file in disk_files:
|
for disk_file in disk_files:
|
||||||
# Skip the config drive as the instance is already configured
|
# Skip the config drive as the instance is already configured
|
||||||
if os.path.basename(disk_file).lower() != 'configdrive.vhd':
|
if os.path.basename(disk_file).lower() != 'configdrive.vhd':
|
||||||
@@ -84,8 +81,9 @@ class MigrationOps(object):
|
|||||||
|
|
||||||
self._pathutils.move_folder_files(instance_path, revert_path)
|
self._pathutils.move_folder_files(instance_path, revert_path)
|
||||||
|
|
||||||
if same_host:
|
if shared_storage:
|
||||||
self._pathutils.move_folder_files(dest_path, instance_path)
|
self._pathutils.move_folder_files(dest_path, instance_path)
|
||||||
|
self._pathutils.rmtree(dest_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
self._cleanup_failed_disk_migration(instance_path, revert_path,
|
self._cleanup_failed_disk_migration(instance_path, revert_path,
|
||||||
|
|||||||
@@ -14,15 +14,19 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from os_win.utils import pathutils
|
from os_win.utils import pathutils
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
import nova.conf
|
import nova.conf
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova.i18n import _
|
from nova.i18n import _
|
||||||
from nova.virt.hyperv import constants
|
from nova.virt.hyperv import constants
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF = nova.conf.CONF
|
CONF = nova.conf.CONF
|
||||||
|
|
||||||
ERROR_INVALID_NAME = 123
|
ERROR_INVALID_NAME = 123
|
||||||
@@ -169,3 +173,25 @@ class PathUtils(pathutils.PathUtils):
|
|||||||
|
|
||||||
def get_age_of_file(self, file_name):
|
def get_age_of_file(self, file_name):
|
||||||
return time.time() - os.path.getmtime(file_name)
|
return time.time() - os.path.getmtime(file_name)
|
||||||
|
|
||||||
|
def check_dirs_shared_storage(self, src_dir, dest_dir):
|
||||||
|
# Check if shared storage is being used by creating a temporary
|
||||||
|
# file at the destination path and checking if it exists at the
|
||||||
|
# source path.
|
||||||
|
LOG.debug("Checking if %(src_dir)s and %(dest_dir)s point "
|
||||||
|
"to the same location.",
|
||||||
|
dict(src_dir=src_dir, dest_dir=dest_dir))
|
||||||
|
with tempfile.NamedTemporaryFile(dir=dest_dir) as tmp_file:
|
||||||
|
src_path = os.path.join(src_dir,
|
||||||
|
os.path.basename(tmp_file.name))
|
||||||
|
|
||||||
|
shared_storage = os.path.exists(src_path)
|
||||||
|
return shared_storage
|
||||||
|
|
||||||
|
def check_remote_instances_dir_shared(self, dest):
|
||||||
|
# Checks if the instances dir from a remote host points
|
||||||
|
# to the same storage location as the local instances dir.
|
||||||
|
local_inst_dir = self.get_instances_dir()
|
||||||
|
remote_inst_dir = self.get_instances_dir(dest)
|
||||||
|
return self.check_dirs_shared_storage(local_inst_dir,
|
||||||
|
remote_inst_dir)
|
||||||
|
|||||||
Reference in New Issue
Block a user