From 72e75dbcea66e6e4e2b0f5a931dcd647b2032ad6 Mon Sep 17 00:00:00 2001 From: John Garbutt Date: Thu, 14 Mar 2013 15:41:47 +0000 Subject: [PATCH] xenapi: ensure vdi is not too big when resizing down Fix for bug 1155066 This change adds some rollback into the resize disk down code within xenapi, and reports a better error message when the disk is too big to resize down. On a successful rollback, the user is notified of the error by the instance actions, rather than leaving the server in the error state. The user is then able to free up some disk space such that the resize can work correctly. Change-Id: Ibad568ab3cfb9caaf4fe002572c8cda973d501a7 --- nova/compute/manager.py | 7 + nova/exception.py | 7 + nova/tests/compute/test_compute.py | 37 ++++++ nova/tests/test_xenapi.py | 116 +++++++++++++++++ nova/tests/virt/xenapi/test_vm_utils.py | 86 +++++++++++++ nova/tests/virt/xenapi/test_volumeops.py | 4 +- nova/virt/xenapi/driver.py | 13 +- nova/virt/xenapi/vm_utils.py | 32 ++++- nova/virt/xenapi/vmops.py | 155 +++++++++++++++-------- nova/virt/xenapi/volumeops.py | 4 +- 10 files changed, 385 insertions(+), 76 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 79903f8542..7bc8281d40 100755 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -4117,6 +4117,13 @@ class ComputeManager(manager.SchedulerDependentManager): reservations=None): try: yield + except exception.InstanceFaultRollback, error: + self._quota_rollback(context, reservations) + msg = _("Setting instance back to ACTIVE after: %s") + LOG.info(msg % error, instance_uuid=instance_uuid) + self._instance_update(context, instance_uuid, + vm_state=vm_states.ACTIVE) + raise error.inner_exception except Exception, error: with excutils.save_and_reraise_exception(): self._quota_rollback(context, reservations) diff --git a/nova/exception.py b/nova/exception.py index a9afe37a7e..4398911539 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1196,3 +1196,10 @@ class BuildAbortException(NovaException): class RescheduledException(NovaException): message = _("Build of instance %(instance_uuid)s was re-scheduled: " "%(reason)s") + + +class InstanceFaultRollback(NovaException): + def __init__(self, inner_exception=None): + message = _("Instance rollback performed due to: %s") + self.inner_exception = inner_exception + super(InstanceFaultRollback, self).__init__(message % inner_exception) diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index 9560fc78cd..2adb07bdb7 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -3089,6 +3089,43 @@ class ComputeTestCase(BaseTestCase): self.compute.terminate_instance(self.context, instance=jsonutils.to_primitive(instance)) + def test_resize_instance_driver_rollback(self): + # Ensure instance status set to Running after rollback. + + def throw_up(*args, **kwargs): + raise exception.InstanceFaultRollback(test.TestingException()) + + self.stubs.Set(self.compute.driver, 'migrate_disk_and_power_off', + throw_up) + + instance = jsonutils.to_primitive(self._create_fake_instance()) + instance_type = flavors.get_default_instance_type() + reservations = self._ensure_quota_reservations_rolledback() + self.compute.run_instance(self.context, instance=instance) + new_instance = db.instance_update(self.context, instance['uuid'], + {'host': 'foo'}) + new_instance = jsonutils.to_primitive(new_instance) + self.compute.prep_resize(self.context, instance=new_instance, + instance_type=instance_type, image={}, + reservations=reservations) + migration_ref = db.migration_get_by_instance_and_status( + self.context.elevated(), new_instance['uuid'], 'pre-migrating') + db.instance_update(self.context, new_instance['uuid'], + {"task_state": task_states.RESIZE_PREP}) + + self.assertRaises(test.TestingException, self.compute.resize_instance, + self.context, instance=new_instance, + migration=migration_ref, image={}, + reservations=reservations, + instance_type=jsonutils.to_primitive(instance_type)) + + instance = db.instance_get_by_uuid(self.context, new_instance['uuid']) + self.assertEqual(instance['vm_state'], vm_states.ACTIVE) + self.assertEqual(instance['task_state'], None) + + self.compute.terminate_instance(self.context, + instance=jsonutils.to_primitive(instance)) + def test_resize_instance(self): # Ensure instance can be migrated/resized. instance = jsonutils.to_primitive(self._create_fake_instance()) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 9a366d7bf9..45ebfaab37 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -1513,6 +1513,122 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase): self.context, instance, '127.0.0.1', instance_type, None) + def test_migrate_rollback_when_resize_down_fs_fails(self): + conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False) + vmops = conn._vmops + virtapi = vmops._virtapi + + self.mox.StubOutWithMock(vmops, '_resize_ensure_vm_is_shutdown') + self.mox.StubOutWithMock(vmops, '_apply_orig_vm_name_label') + self.mox.StubOutWithMock(vm_utils, 'resize_disk') + self.mox.StubOutWithMock(vmops, '_migrate_vhd') + self.mox.StubOutWithMock(vm_utils, 'destroy_vdi') + self.mox.StubOutWithMock(vm_utils, 'get_vdi_for_vm_safely') + self.mox.StubOutWithMock(vmops, '_restore_orig_vm_and_cleanup_orphan') + self.mox.StubOutWithMock(virtapi, 'instance_update') + + instance = {'auto_disk_config': True, 'uuid': 'uuid'} + vm_ref = "vm_ref" + dest = "dest" + instance_type = "type" + sr_path = "sr_path" + + virtapi.instance_update(self.context, 'uuid', {'progress': 20.0}) + vmops._resize_ensure_vm_is_shutdown(instance, vm_ref) + vmops._apply_orig_vm_name_label(instance, vm_ref) + old_vdi_ref = "old_ref" + vm_utils.get_vdi_for_vm_safely(vmops._session, vm_ref).AndReturn( + (old_vdi_ref, None)) + virtapi.instance_update(self.context, 'uuid', {'progress': 40.0}) + new_vdi_ref = "new_ref" + new_vdi_uuid = "new_uuid" + vm_utils.resize_disk(vmops._session, instance, old_vdi_ref, + instance_type).AndReturn((new_vdi_ref, new_vdi_uuid)) + virtapi.instance_update(self.context, 'uuid', {'progress': 60.0}) + vmops._migrate_vhd(instance, new_vdi_uuid, dest, + sr_path, 0).AndRaise( + exception.ResizeError(reason="asdf")) + + vm_utils.destroy_vdi(vmops._session, new_vdi_ref) + vmops._restore_orig_vm_and_cleanup_orphan(instance, None) + + self.mox.ReplayAll() + + self.assertRaises(exception.InstanceFaultRollback, + vmops._migrate_disk_resizing_down, self.context, + instance, dest, instance_type, vm_ref, sr_path) + + def test_resize_ensure_vm_is_shutdown_cleanly(self): + conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False) + vmops = conn._vmops + fake_instance = {'uuid': 'uuid'} + + self.mox.StubOutWithMock(vm_utils, 'is_vm_shutdown') + self.mox.StubOutWithMock(vm_utils, 'clean_shutdown_vm') + self.mox.StubOutWithMock(vm_utils, 'hard_shutdown_vm') + + vm_utils.is_vm_shutdown(vmops._session, "ref").AndReturn(False) + vm_utils.clean_shutdown_vm(vmops._session, fake_instance, + "ref").AndReturn(True) + + self.mox.ReplayAll() + + vmops._resize_ensure_vm_is_shutdown(fake_instance, "ref") + + def test_resize_ensure_vm_is_shutdown_forced(self): + conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False) + vmops = conn._vmops + fake_instance = {'uuid': 'uuid'} + + self.mox.StubOutWithMock(vm_utils, 'is_vm_shutdown') + self.mox.StubOutWithMock(vm_utils, 'clean_shutdown_vm') + self.mox.StubOutWithMock(vm_utils, 'hard_shutdown_vm') + + vm_utils.is_vm_shutdown(vmops._session, "ref").AndReturn(False) + vm_utils.clean_shutdown_vm(vmops._session, fake_instance, + "ref").AndReturn(False) + vm_utils.hard_shutdown_vm(vmops._session, fake_instance, + "ref").AndReturn(True) + + self.mox.ReplayAll() + + vmops._resize_ensure_vm_is_shutdown(fake_instance, "ref") + + def test_resize_ensure_vm_is_shutdown_fails(self): + conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False) + vmops = conn._vmops + fake_instance = {'uuid': 'uuid'} + + self.mox.StubOutWithMock(vm_utils, 'is_vm_shutdown') + self.mox.StubOutWithMock(vm_utils, 'clean_shutdown_vm') + self.mox.StubOutWithMock(vm_utils, 'hard_shutdown_vm') + + vm_utils.is_vm_shutdown(vmops._session, "ref").AndReturn(False) + vm_utils.clean_shutdown_vm(vmops._session, fake_instance, + "ref").AndReturn(False) + vm_utils.hard_shutdown_vm(vmops._session, fake_instance, + "ref").AndReturn(False) + + self.mox.ReplayAll() + + self.assertRaises(exception.ResizeError, + vmops._resize_ensure_vm_is_shutdown, fake_instance, "ref") + + def test_resize_ensure_vm_is_shutdown_already_shutdown(self): + conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False) + vmops = conn._vmops + fake_instance = {'uuid': 'uuid'} + + self.mox.StubOutWithMock(vm_utils, 'is_vm_shutdown') + self.mox.StubOutWithMock(vm_utils, 'clean_shutdown_vm') + self.mox.StubOutWithMock(vm_utils, 'hard_shutdown_vm') + + vm_utils.is_vm_shutdown(vmops._session, "ref").AndReturn(True) + + self.mox.ReplayAll() + + vmops._resize_ensure_vm_is_shutdown(fake_instance, "ref") + class XenAPIImageTypeTestCase(test.TestCase): """Test ImageType class.""" diff --git a/nova/tests/virt/xenapi/test_vm_utils.py b/nova/tests/virt/xenapi/test_vm_utils.py index 3e532a1c74..2180f75f90 100644 --- a/nova/tests/virt/xenapi/test_vm_utils.py +++ b/nova/tests/virt/xenapi/test_vm_utils.py @@ -278,3 +278,89 @@ class FetchVhdImageTestCase(test.TestCase): self.context, self.session, self.instance, self.image_id) self.mox.VerifyAll() + + +class ResizeHelpersTestCase(test.TestCase): + def test_get_min_sectors(self): + self.mox.StubOutWithMock(utils, 'execute') + + utils.execute('resize2fs', '-P', "fakepath", + run_as_root=True).AndReturn(("size is: 42", "")) + + self.mox.ReplayAll() + + result = vm_utils._get_min_sectors("fakepath") + self.assertEquals(42 * 4096 / 512, result) + + def test_repair_filesystem(self): + self.mox.StubOutWithMock(utils, 'execute') + + utils.execute('e2fsck', '-f', "-y", "fakepath", + run_as_root=True, check_exit_code=[0, 1, 2]).AndReturn( + ("size is: 42", "")) + + self.mox.ReplayAll() + + vm_utils._repair_filesystem("fakepath") + + def _call_tune2fs_remove_journal(self, path): + utils.execute("tune2fs", "-O ^has_journal", path, run_as_root=True) + + def _call_tune2fs_add_journal(self, path): + utils.execute("tune2fs", "-j", path, run_as_root=True) + + def _call_parted(self, path, start, end): + utils.execute('parted', '--script', path, 'rm', '1', + run_as_root=True) + utils.execute('parted', '--script', path, 'mkpart', + 'primary', '%ds' % start, '%ds' % end, run_as_root=True) + + def test_resize_part_and_fs_down_succeeds(self): + self.mox.StubOutWithMock(vm_utils, "_repair_filesystem") + self.mox.StubOutWithMock(utils, 'execute') + self.mox.StubOutWithMock(vm_utils, "_get_min_sectors") + + dev_path = "/dev/fake" + partition_path = "%s1" % dev_path + vm_utils._repair_filesystem(partition_path) + self._call_tune2fs_remove_journal(partition_path) + vm_utils._get_min_sectors(partition_path).AndReturn(9) + utils.execute("resize2fs", partition_path, "10s", run_as_root=True) + self._call_parted(dev_path, 0, 9) + self._call_tune2fs_add_journal(partition_path) + + self.mox.ReplayAll() + + vm_utils._resize_part_and_fs("fake", 0, 20, 10) + + def test_resize_part_and_fs_down_fails_disk_too_big(self): + self.mox.StubOutWithMock(vm_utils, "_repair_filesystem") + self.mox.StubOutWithMock(utils, 'execute') + self.mox.StubOutWithMock(vm_utils, "_get_min_sectors") + + dev_path = "/dev/fake" + partition_path = "%s1" % dev_path + vm_utils._repair_filesystem(partition_path) + self._call_tune2fs_remove_journal(partition_path) + vm_utils._get_min_sectors(partition_path).AndReturn(10) + + self.mox.ReplayAll() + + self.assertRaises(exception.ResizeError, + vm_utils._resize_part_and_fs, "fake", 0, 20, 10) + + def test_resize_part_and_fs_up_succeeds(self): + self.mox.StubOutWithMock(vm_utils, "_repair_filesystem") + self.mox.StubOutWithMock(utils, 'execute') + + dev_path = "/dev/fake" + partition_path = "%s1" % dev_path + vm_utils._repair_filesystem(partition_path) + self._call_tune2fs_remove_journal(partition_path) + self._call_parted(dev_path, 0, 29) + utils.execute("resize2fs", partition_path, run_as_root=True) + self._call_tune2fs_add_journal(partition_path) + + self.mox.ReplayAll() + + vm_utils._resize_part_and_fs("fake", 0, 20, 30) diff --git a/nova/tests/virt/xenapi/test_volumeops.py b/nova/tests/virt/xenapi/test_volumeops.py index 7ec9eb1eab..5d4344bb06 100644 --- a/nova/tests/virt/xenapi/test_volumeops.py +++ b/nova/tests/virt/xenapi/test_volumeops.py @@ -33,7 +33,7 @@ class VolumeAttachTestCase(test.TestCase): ops = volumeops.VolumeOps('session') self.mox.StubOutWithMock(volumeops.vm_utils, 'lookup') self.mox.StubOutWithMock(volumeops.vm_utils, 'find_vbd_by_number') - self.mox.StubOutWithMock(volumeops.vm_utils, '_is_vm_shutdown') + self.mox.StubOutWithMock(volumeops.vm_utils, 'is_vm_shutdown') self.mox.StubOutWithMock(volumeops.vm_utils, 'unplug_vbd') self.mox.StubOutWithMock(volumeops.vm_utils, 'destroy_vbd') self.mox.StubOutWithMock(volumeops.volume_utils, 'get_device_number') @@ -49,7 +49,7 @@ class VolumeAttachTestCase(test.TestCase): volumeops.vm_utils.find_vbd_by_number( 'session', 'vmref', 'devnumber').AndReturn('vbdref') - volumeops.vm_utils._is_vm_shutdown('session', 'vmref').AndReturn( + volumeops.vm_utils.is_vm_shutdown('session', 'vmref').AndReturn( False) volumeops.vm_utils.unplug_vbd('session', 'vbdref') diff --git a/nova/virt/xenapi/driver.py b/nova/virt/xenapi/driver.py index 02b849a99d..5bc1a30499 100755 --- a/nova/virt/xenapi/driver.py +++ b/nova/virt/xenapi/driver.py @@ -239,17 +239,8 @@ class XenAPIDriver(driver.ComputeDriver): """Transfers the VHD of a running instance to another host, then shuts off the instance copies over the COW disk""" # NOTE(vish): Xen currently does not use network info. - rv = self._vmops.migrate_disk_and_power_off(context, instance, - dest, instance_type) - block_device_mapping = driver.block_device_info_get_mapping( - block_device_info) - name_label = self._vmops._get_orig_vm_name_label(instance) - for vol in block_device_mapping: - connection_info = vol['connection_info'] - mount_device = vol['mount_device'].rpartition("/")[2] - self._volumeops.detach_volume(connection_info, - name_label, mount_device) - return rv + return self._vmops.migrate_disk_and_power_off(context, instance, + dest, instance_type, block_device_info) def suspend(self, instance): """suspend the specified instance.""" diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 4cd2b2ab54..3f2c6835f6 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -286,7 +286,7 @@ def destroy_vm(session, instance, vm_ref): def clean_shutdown_vm(session, instance, vm_ref): - if _is_vm_shutdown(session, vm_ref): + if is_vm_shutdown(session, vm_ref): LOG.warn(_("VM already halted, skipping shutdown..."), instance=instance) return False @@ -301,7 +301,7 @@ def clean_shutdown_vm(session, instance, vm_ref): def hard_shutdown_vm(session, instance, vm_ref): - if _is_vm_shutdown(session, vm_ref): + if is_vm_shutdown(session, vm_ref): LOG.warn(_("VM already halted, skipping shutdown..."), instance=instance) return False @@ -315,7 +315,7 @@ def hard_shutdown_vm(session, instance, vm_ref): return True -def _is_vm_shutdown(session, vm_ref): +def is_vm_shutdown(session, vm_ref): vm_rec = session.call_xenapi("VM.get_record", vm_ref) state = compile_info(vm_rec)['state'] if state == power_state.SHUTDOWN: @@ -2078,6 +2078,21 @@ def _write_partition(virtual_size, dev): LOG.debug(_('Writing partition table %s done.'), dev_path) +def _get_min_sectors(partition_path, block_size=4096): + stdout, _err = utils.execute('resize2fs', '-P', partition_path, + run_as_root=True) + min_size_blocks = long(re.sub('[^0-9]', '', stdout)) + min_size_bytes = min_size_blocks * block_size + return min_size_bytes / SECTOR_SIZE + + +def _repair_filesystem(partition_path): + # Exit Code 1 = File system errors corrected + # 2 = File system errors corrected, system needs a reboot + utils.execute('e2fsck', '-f', '-y', partition_path, run_as_root=True, + check_exit_code=[0, 1, 2]) + + def _resize_part_and_fs(dev, start, old_sectors, new_sectors): """Resize partition and fileystem. @@ -2091,10 +2106,7 @@ def _resize_part_and_fs(dev, start, old_sectors, new_sectors): partition_path = utils.make_dev_path(dev, partition=1) # Replay journal if FS wasn't cleanly unmounted - # Exit Code 1 = File system errors corrected - # 2 = File system errors corrected, system needs a reboot - utils.execute('e2fsck', '-f', '-y', partition_path, run_as_root=True, - check_exit_code=[0, 1, 2]) + _repair_filesystem(partition_path) # Remove ext3 journal (making it ext2) utils.execute('tune2fs', '-O ^has_journal', partition_path, @@ -2102,6 +2114,12 @@ def _resize_part_and_fs(dev, start, old_sectors, new_sectors): if new_sectors < old_sectors: # Resizing down, resize filesystem before partition resize + min_sectors = _get_min_sectors(partition_path) + if min_sectors >= new_sectors: + reason = _('Resize down not allowed because minimum ' + 'filesystem sectors %(min_sectors)d is too big ' + 'for target sectors %(new_sectors)d') + raise exception.ResizeError(reason=(reason % locals())) utils.execute('resize2fs', partition_path, '%ds' % size, run_as_root=True) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 1dabe271d1..0345568097 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -209,6 +209,9 @@ class VMOps(object): return nova_uuids def confirm_migration(self, migration, instance, network_info): + self._destroy_orig_vm(instance, network_info) + + def _destroy_orig_vm(self, instance, network_info): name_label = self._get_orig_vm_name_label(instance) vm_ref = vm_utils.lookup(self._session, name_label) return self._destroy(instance, vm_ref, network_info=network_info) @@ -227,6 +230,9 @@ class VMOps(object): hotplug=False) def finish_revert_migration(self, instance, block_device_info=None): + self._restore_orig_vm_and_cleanup_orphan(instance, block_device_info) + + def _restore_orig_vm_and_cleanup_orphan(self, instance, block_device_info): # NOTE(sirp): the original vm was suffixed with '-orig'; find it using # the old suffix, remove the suffix, then power it back on. name_label = self._get_orig_vm_name_label(instance) @@ -797,53 +803,84 @@ class VMOps(object): self._virtapi.instance_update(context, instance['uuid'], {'progress': progress}) - def _migrate_disk_resizing_down(self, context, instance, dest, - instance_type, vm_ref, sr_path): - # 1. NOOP since we're not transmitting the base-copy separately - self._update_instance_progress(context, instance, - step=1, - total_steps=RESIZE_TOTAL_STEPS) + def _resize_ensure_vm_is_shutdown(self, instance, vm_ref): + if vm_utils.is_vm_shutdown(self._session, vm_ref): + LOG.debug(_("VM was already shutdown."), instance=instance) + return - vdi_ref, vm_vdi_rec = vm_utils.get_vdi_for_vm_safely( - self._session, vm_ref) - vdi_uuid = vm_vdi_rec['uuid'] - - old_gb = instance['root_gb'] - new_gb = instance_type['root_gb'] - LOG.debug(_("Resizing down VDI %(vdi_uuid)s from " - "%(old_gb)dGB to %(new_gb)dGB"), locals(), - instance=instance) - - # 2. Power down the instance before resizing if not vm_utils.clean_shutdown_vm(self._session, instance, vm_ref): LOG.debug(_("Clean shutdown did not complete successfully, " "trying hard shutdown."), instance=instance) - vm_utils.hard_shutdown_vm(self._session, instance, vm_ref) - self._update_instance_progress(context, instance, - step=2, - total_steps=RESIZE_TOTAL_STEPS) + if not vm_utils.hard_shutdown_vm(self._session, instance, vm_ref): + raise exception.ResizeError( + reason=_("Unable to terminate instance.")) - # 3. Copy VDI, resize partition and filesystem, forget VDI, - # truncate VHD - new_ref, new_uuid = vm_utils.resize_disk(self._session, - instance, - vdi_ref, - instance_type) - self._update_instance_progress(context, instance, - step=3, - total_steps=RESIZE_TOTAL_STEPS) + def _migrate_disk_resizing_down(self, context, instance, dest, + instance_type, vm_ref, sr_path): + if not instance['auto_disk_config']: + reason = _('Resize down not allowed without auto_disk_config') + raise exception.ResizeError(reason=reason) - # 4. Transfer the new VHD - self._migrate_vhd(instance, new_uuid, dest, sr_path, 0) - self._update_instance_progress(context, instance, - step=4, - total_steps=RESIZE_TOTAL_STEPS) + step = make_step_decorator(context, instance, + self._virtapi.instance_update) - # Clean up VDI now that it's been copied - vm_utils.destroy_vdi(self._session, new_ref) + @step + def fake_step_to_match_resizing_up(): + pass + + @step + def rename_and_power_off_vm(undo_mgr): + self._resize_ensure_vm_is_shutdown(instance, vm_ref) + self._apply_orig_vm_name_label(instance, vm_ref) + + def restore_orig_vm(): + # Do not need to restore block devices, not yet been removed + self._restore_orig_vm_and_cleanup_orphan(instance, None) + + undo_mgr.undo_with(restore_orig_vm) + + @step + def create_copy_vdi_and_resize(undo_mgr, old_vdi_ref): + new_vdi_ref, new_vdi_uuid = vm_utils.resize_disk(self._session, + instance, old_vdi_ref, instance_type) + + def cleanup_vdi_copy(): + vm_utils.destroy_vdi(self._session, new_vdi_ref) + + undo_mgr.undo_with(cleanup_vdi_copy) + + return new_vdi_ref, new_vdi_uuid + + @step + def transfer_vhd_to_dest(new_vdi_ref, new_vdi_uuid): + self._migrate_vhd(instance, new_vdi_uuid, dest, sr_path, 0) + # Clean up VDI now that it's been copied + vm_utils.destroy_vdi(self._session, new_vdi_ref) + + @step + def fake_step_to_be_executed_by_finish_migration(): + pass + + undo_mgr = utils.UndoManager() + try: + fake_step_to_match_resizing_up() + rename_and_power_off_vm(undo_mgr) + old_vdi_ref, _ignore = vm_utils.get_vdi_for_vm_safely( + self._session, vm_ref) + new_vdi_ref, new_vdi_uuid = create_copy_vdi_and_resize( + undo_mgr, old_vdi_ref) + transfer_vhd_to_dest(new_vdi_ref, new_vdi_uuid) + except Exception, error: + msg = _("_migrate_disk_resizing_down failed. " + "Restoring orig vm due_to: %{exception}.") + LOG.exception(msg, instance=instance) + undo_mgr._rollback() + raise exception.InstanceFaultRollback(error) def _migrate_disk_resizing_up(self, context, instance, dest, vm_ref, sr_path): + self._apply_orig_vm_name_label(instance, vm_ref) + # 1. Create Snapshot label = "%s-snapshot" % instance['name'] with vm_utils.snapshot_attached_here( @@ -865,10 +902,7 @@ class VMOps(object): total_steps=RESIZE_TOTAL_STEPS) # 3. Now power down the instance - if not vm_utils.clean_shutdown_vm(self._session, instance, vm_ref): - LOG.debug(_("Clean shutdown did not complete successfully, " - "trying hard shutdown."), instance=instance) - vm_utils.hard_shutdown_vm(self._session, instance, vm_ref) + self._resize_ensure_vm_is_shutdown(instance, vm_ref) self._update_instance_progress(context, instance, step=3, total_steps=RESIZE_TOTAL_STEPS) @@ -882,8 +916,15 @@ class VMOps(object): step=4, total_steps=RESIZE_TOTAL_STEPS) + def _apply_orig_vm_name_label(self, instance, vm_ref): + # NOTE(sirp): in case we're resizing to the same host (for dev + # purposes), apply a suffix to name-label so the two VM records + # extant until a confirm_resize don't collide. + name_label = self._get_orig_vm_name_label(instance) + vm_utils.set_vm_name_label(self._session, vm_ref, name_label) + def migrate_disk_and_power_off(self, context, instance, dest, - instance_type): + instance_type, block_device_info): """Copies a VHD from one host machine to another, possibly resizing filesystem before hand. @@ -891,23 +932,17 @@ class VMOps(object): :param dest: the destination host machine. :param instance_type: instance_type to resize to """ - vm_ref = self._get_vm_opaque_ref(instance) - sr_path = vm_utils.get_sr_path(self._session) - resize_down = instance['root_gb'] > instance_type['root_gb'] - if resize_down and not instance['auto_disk_config']: - reason = _('Resize down not allowed without auto_disk_config') - raise exception.ResizeError(reason=reason) - # 0. Zero out the progress to begin self._update_instance_progress(context, instance, step=0, total_steps=RESIZE_TOTAL_STEPS) - # NOTE(sirp): in case we're resizing to the same host (for dev - # purposes), apply a suffix to name-label so the two VM records - # extant until a confirm_resize don't collide. - name_label = self._get_orig_vm_name_label(instance) - vm_utils.set_vm_name_label(self._session, vm_ref, name_label) + vm_ref = self._get_vm_opaque_ref(instance) + sr_path = vm_utils.get_sr_path(self._session) + + old_gb = instance['root_gb'] + new_gb = instance_type['root_gb'] + resize_down = old_gb > new_gb if resize_down: self._migrate_disk_resizing_down( @@ -916,12 +951,24 @@ class VMOps(object): self._migrate_disk_resizing_up( context, instance, dest, vm_ref, sr_path) + self._detach_block_devices_from_orig_vm(instance, block_device_info) + # NOTE(sirp): disk_info isn't used by the xenapi driver, instead it # uses a staging-area (/images/instance) and sequence-numbered # VHDs to figure out how to reconstruct the VDI chain after syncing disk_info = {} return disk_info + def _detach_block_devices_from_orig_vm(self, instance, block_device_info): + block_device_mapping = virt_driver.block_device_info_get_mapping( + block_device_info) + name_label = self._get_orig_vm_name_label(instance) + for vol in block_device_mapping: + connection_info = vol['connection_info'] + mount_device = vol['mount_device'].rpartition("/")[2] + self._volumeops.detach_volume(connection_info, name_label, + mount_device) + def _resize_instance(self, instance, root_vdi): """Resize an instances root disk.""" diff --git a/nova/virt/xenapi/volumeops.py b/nova/virt/xenapi/volumeops.py index 5e650f55d0..3560edbc1e 100644 --- a/nova/virt/xenapi/volumeops.py +++ b/nova/virt/xenapi/volumeops.py @@ -141,7 +141,7 @@ class VolumeOps(object): return # Unplug VBD if we're NOT shutdown - unplug = not vm_utils._is_vm_shutdown(self._session, vm_ref) + unplug = not vm_utils.is_vm_shutdown(self._session, vm_ref) self._detach_vbd(vbd_ref, unplug=unplug) LOG.info(_('Mountpoint %(mountpoint)s detached from instance' @@ -171,7 +171,7 @@ class VolumeOps(object): # Generally speaking, detach_all will be called with VM already # shutdown; however if it's still running, we can still perform the # operation by unplugging the VBD first. - unplug = not vm_utils._is_vm_shutdown(self._session, vm_ref) + unplug = not vm_utils.is_vm_shutdown(self._session, vm_ref) vbd_refs = self._get_all_volume_vbd_refs(vm_ref) for vbd_ref in vbd_refs: