Xenapi: Allow volume backed instances to migrate

The migrate process had no provisions for a non image backed instance.
This remedies that by doing a few things:

Don't snapshot the root disk on the source host.
Don't try to import the root disk on the destination host.
Connect the volume to the destination host as the boot device.

Change-Id: Ia453cf33a36803b1b5447c6240988a3dd603f328
Closes-bug: #1375480
This commit is contained in:
Andrew Laski 2014-09-29 18:04:08 -04:00
parent 3ad52a79d8
commit a234e16ca6
8 changed files with 183 additions and 13 deletions

View File

@ -314,7 +314,7 @@ class FakeSessionForVolumeFailedTests(FakeSessionForVolumeTests):
def stub_out_migration_methods(stubs):
fakesr = fake.create_sr()
def fake_import_all_migrated_disks(session, instance):
def fake_import_all_migrated_disks(session, instance, import_root=True):
vdi_ref = fake.create_vdi(instance['name'], fakesr)
vdi_rec = fake.get_record('VDI', vdi_ref)
vdi_rec['other_config']['nova_disk_type'] = 'root'

View File

@ -1944,6 +1944,23 @@ class ImportMigratedDisksTestCase(VMUtilsTestBase):
mock_root.assert_called_once_with(session, instance)
mock_ephemeral.assert_called_once_with(session, instance)
@mock.patch.object(vm_utils, '_import_migrate_ephemeral_disks')
@mock.patch.object(vm_utils, '_import_migrated_root_disk')
def test_import_all_migrated_disks_import_root_false(self, mock_root,
mock_ephemeral):
session = "session"
instance = "instance"
mock_root.return_value = "root_vdi"
mock_ephemeral.return_value = ["a", "b"]
result = vm_utils.import_all_migrated_disks(session, instance,
import_root=False)
expected = {'root': None, 'ephemerals': ["a", "b"]}
self.assertEqual(expected, result)
self.assertEqual(0, mock_root.call_count)
mock_ephemeral.assert_called_once_with(session, instance)
@mock.patch.object(vm_utils, '_import_migrated_vhds')
def test_import_migrated_root_disk(self, mock_migrate):
mock_migrate.return_value = "foo"

View File

@ -391,19 +391,32 @@ class SpawnTestCase(VMOpsTestBase):
throw_exception=test.TestingException())
def _test_finish_migration(self, power_on=True, resize_instance=True,
throw_exception=None):
throw_exception=None, booted_from_volume=False):
self._stub_out_common()
self.mox.StubOutWithMock(volumeops.VolumeOps, "connect_volume")
self.mox.StubOutWithMock(self.vmops._session, 'call_xenapi')
self.mox.StubOutWithMock(vm_utils, "import_all_migrated_disks")
self.mox.StubOutWithMock(self.vmops, "_attach_mapped_block_devices")
context = "context"
migration = {}
name_label = "dummy"
instance = {"name": name_label, "uuid": "fake_uuid"}
instance = {"name": name_label, "uuid": "fake_uuid",
"root_device_name": "/dev/xvda"}
disk_info = "disk_info"
network_info = "net_info"
image_meta = {"id": "image_id"}
block_device_info = "bdi"
block_device_info = {}
import_root = True
if booted_from_volume:
block_device_info = {'block_device_mapping': [
{'mount_device': '/dev/xvda',
'connection_info': {'data': 'fake-data'}}]}
import_root = False
volumeops.VolumeOps.connect_volume(
{'data': 'fake-data'}).AndReturn(('sr', 'vol-vdi-uuid'))
self.vmops._session.call_xenapi('VDI.get_by_uuid',
'vol-vdi-uuid').AndReturn('vol-vdi-ref')
session = self.vmops._session
self.vmops._ensure_instance_name_unique(name_label)
@ -415,8 +428,8 @@ class SpawnTestCase(VMOpsTestBase):
root_vdi = {"ref": "fake_ref"}
ephemeral_vdi = {"ref": "fake_ref_e"}
vdis = {"root": root_vdi, "ephemerals": {4: ephemeral_vdi}}
vm_utils.import_all_migrated_disks(self.vmops._session,
instance).AndReturn(vdis)
vm_utils.import_all_migrated_disks(self.vmops._session, instance,
import_root=import_root).AndReturn(vdis)
kernel_file = "kernel"
ramdisk_file = "ramdisk"
@ -475,6 +488,9 @@ class SpawnTestCase(VMOpsTestBase):
def test_finish_migration_no_power_on(self):
self._test_finish_migration(power_on=False, resize_instance=False)
def test_finish_migration_booted_from_volume(self):
self._test_finish_migration(booted_from_volume=True)
def test_finish_migrate_performs_rollback_on_error(self):
self.assertRaises(test.TestingException, self._test_finish_migration,
power_on=False, resize_instance=False,
@ -712,7 +728,10 @@ class MigrateDiskResizingUpTestCase(VMOpsTestBase):
parent = userdevice + "-parent"
yield [leaf, parent]
@mock.patch.object(volume_utils, 'is_booted_from_volume',
return_value=False)
def test_migrate_disk_resizing_up_works_no_ephemeral(self,
mock_is_booted_from_volume,
mock_apply_orig, mock_update_progress, mock_get_all_vdi_uuids,
mock_shutdown, mock_migrate_vhd):
context = "ctxt"
@ -750,7 +769,10 @@ class MigrateDiskResizingUpTestCase(VMOpsTestBase):
]
self.assertEqual(prog_expected, mock_update_progress.call_args_list)
@mock.patch.object(volume_utils, 'is_booted_from_volume',
return_value=False)
def test_migrate_disk_resizing_up_works_with_two_ephemeral(self,
mock_is_booted_from_volume,
mock_apply_orig, mock_update_progress, mock_get_all_vdi_uuids,
mock_shutdown, mock_migrate_vhd):
context = "ctxt"
@ -796,8 +818,54 @@ class MigrateDiskResizingUpTestCase(VMOpsTestBase):
]
self.assertEqual(prog_expected, mock_update_progress.call_args_list)
@mock.patch.object(volume_utils, 'is_booted_from_volume',
return_value=True)
def test_migrate_disk_resizing_up_booted_from_volume(self,
mock_is_booted_from_volume,
mock_apply_orig, mock_update_progress, mock_get_all_vdi_uuids,
mock_shutdown, mock_migrate_vhd):
context = "ctxt"
instance = {"name": "fake", "uuid": "uuid"}
dest = "dest"
vm_ref = "vm_ref"
sr_path = "sr_path"
mock_get_all_vdi_uuids.return_value = ["vdi-eph1", "vdi-eph2"]
with mock.patch.object(vm_utils, '_snapshot_attached_here_impl',
self._fake_snapshot_attached_here):
self.vmops._migrate_disk_resizing_up(context, instance, dest,
vm_ref, sr_path)
mock_get_all_vdi_uuids.assert_called_once_with(self.vmops._session,
vm_ref, min_userdevice=4)
mock_apply_orig.assert_called_once_with(instance, vm_ref)
mock_shutdown.assert_called_once_with(instance, vm_ref)
m_vhd_expected = [mock.call(self.vmops._session, instance,
"4-parent", dest, sr_path, 1, 1),
mock.call(self.vmops._session, instance,
"5-parent", dest, sr_path, 1, 2),
mock.call(self.vmops._session, instance,
"4-leaf", dest, sr_path, 0, 1),
mock.call(self.vmops._session, instance,
"5-leaf", dest, sr_path, 0, 2)]
self.assertEqual(m_vhd_expected, mock_migrate_vhd.call_args_list)
prog_expected = [
mock.call(context, instance, 1, 5),
mock.call(context, instance, 2, 5),
mock.call(context, instance, 3, 5),
mock.call(context, instance, 4, 5)
# 5/5: step to be executed by finish migration.
]
self.assertEqual(prog_expected, mock_update_progress.call_args_list)
@mock.patch.object(vmops.VMOps, '_restore_orig_vm_and_cleanup_orphan')
@mock.patch.object(volume_utils, 'is_booted_from_volume',
return_value=False)
def test_migrate_disk_resizing_up_rollback(self,
mock_is_booted_from_volume,
mock_restore,
mock_apply_orig, mock_update_progress, mock_get_all_vdi_uuids,
mock_shutdown, mock_migrate_vhd):

View File

@ -230,3 +230,23 @@ class FindVBDTestCase(stubs.XenAPITestBaseNoDB):
self.assertIsNone(result)
session.VM.get_VBDs.assert_called_once_with("vm_ref")
session.VBD.get_userdevice.assert_called_once_with("a")
class BootedFromVolumeTestCase(stubs.XenAPITestBaseNoDB):
def test_booted_from_volume(self):
session = mock.Mock()
session.VM.get_VBDs.return_value = ['vbd_ref']
session.VBD.get_userdevice.return_value = '0'
session.VBD.get_other_config.return_value = {'osvol': True}
booted_from_volume = volume_utils.is_booted_from_volume(session,
'vm_ref')
self.assertTrue(booted_from_volume)
def test_not_booted_from_volume(self):
session = mock.Mock()
session.VM.get_VBDs.return_value = ['vbd_ref']
session.VBD.get_userdevice.return_value = '0'
session.VBD.get_other_config.return_value = {}
booted_from_volume = volume_utils.is_booted_from_volume(session,
'vm_ref')
self.assertFalse(booted_from_volume)

View File

@ -1663,6 +1663,10 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase):
xenapi_fake.create_vm(instance['name'], 'Running')
flavor = {"root_gb": 80, 'ephemeral_gb': 0}
conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False)
vm_ref = vm_utils.lookup(conn._session, instance['name'])
self.mox.StubOutWithMock(volume_utils, 'is_booted_from_volume')
volume_utils.is_booted_from_volume(conn._session, vm_ref)
self.mox.ReplayAll()
conn.migrate_disk_and_power_off(self.context, instance,
'127.0.0.1', flavor, None)
@ -1698,6 +1702,10 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase):
instance = db.instance_create(self.context, values)
xenapi_fake.create_vm(instance['name'], 'Running')
conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False)
vm_ref = vm_utils.lookup(conn._session, instance['name'])
self.mox.StubOutWithMock(volume_utils, 'is_booted_from_volume')
volume_utils.is_booted_from_volume(conn._session, vm_ref)
self.mox.ReplayAll()
conn.migrate_disk_and_power_off(self.context, instance,
'127.0.0.1', flavor, None)
@ -1727,6 +1735,7 @@ class XenAPIMigrateInstance(stubs.XenAPITestBase):
stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests,
product_version=(4, 0, 0),
product_brand='XenServer')
self.mox.StubOutWithMock(volume_utils, 'is_booted_from_volume')
conn = xenapi_conn.XenAPIDriver(fake.FakeVirtAPI(), False)
network_info = fake_network.fake_get_instance_nw_info(self.stubs)

View File

@ -2492,8 +2492,10 @@ def ensure_correct_host(session):
'specified by connection_url'))
def import_all_migrated_disks(session, instance):
root_vdi = _import_migrated_root_disk(session, instance)
def import_all_migrated_disks(session, instance, import_root=True):
root_vdi = None
if import_root:
root_vdi = _import_migrated_root_disk(session, instance)
eph_vdis = _import_migrate_ephemeral_disks(session, instance)
return {'root': root_vdi, 'ephemerals': eph_vdis}

View File

@ -216,6 +216,9 @@ class VMOps(object):
block_device_mapping = virt_driver.block_device_info_get_mapping(
block_device_info)
for vol in block_device_mapping:
if vol['mount_device'] == instance['root_device_name']:
# NOTE(alaski): The root device should be attached already
continue
connection_info = vol['connection_info']
mount_device = vol['mount_device'].rpartition("/")[2]
self._volumeops.attach_volume(connection_info,
@ -266,15 +269,39 @@ class VMOps(object):
def create_disks_step(undo_mgr, disk_image_type, image_meta,
name_label):
import_root = True
root_vol_vdi = None
if block_device_info:
LOG.debug("Block device information present: %s",
block_device_info, instance=instance)
# NOTE(alaski): Follows the basic procedure of
# vm_utils.get_vdis_for_instance() used by spawn()
for bdm in block_device_info['block_device_mapping']:
if bdm['mount_device'] == instance['root_device_name']:
connection_info = bdm['connection_info']
_sr, root_vol_vdi = self._volumeops.connect_volume(
connection_info)
import_root = False
break
# TODO(johngarbutt) clean up if this is not run
vdis = vm_utils.import_all_migrated_disks(self._session,
instance)
vdis = vm_utils.import_all_migrated_disks(self._session, instance,
import_root=import_root)
if root_vol_vdi:
vol_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid',
root_vol_vdi)
vdis['root'] = dict(uuid=root_vol_vdi, file=None,
ref=vol_vdi_ref, osvol=True)
def undo_create_disks():
eph_vdis = vdis['ephemerals']
root_vdi = vdis['root']
vdi_refs = [vdi['ref'] for vdi in eph_vdis.values()]
vdi_refs.append(root_vdi['ref'])
if not root_vdi.get('osvol', False):
vdi_refs.append(root_vdi['ref'])
else:
self._volumeops.safe_cleanup_from_vdis(root_vdi['ref'])
vm_utils.safe_destroy_vdis(self._session, vdi_refs)
undo_mgr.undo_with(undo_create_disks)
@ -1048,8 +1075,9 @@ class VMOps(object):
def power_down_and_transfer_leaf_vhds(root_vdi_uuid,
ephemeral_vdi_uuids=None):
self._resize_ensure_vm_is_shutdown(instance, vm_ref)
vm_utils.migrate_vhd(self._session, instance, root_vdi_uuid,
dest, sr_path, 0)
if root_vdi_uuid is not None:
vm_utils.migrate_vhd(self._session, instance, root_vdi_uuid,
dest, sr_path, 0)
if ephemeral_vdi_uuids:
for ephemeral_disk_number, ephemeral_vdi_uuid in enumerate(
ephemeral_vdi_uuids, start=1):
@ -1060,6 +1088,23 @@ class VMOps(object):
self._apply_orig_vm_name_label(instance, vm_ref)
try:
label = "%s-snapshot" % instance['name']
if volume_utils.is_booted_from_volume(self._session, vm_ref):
LOG.debug('Not snapshotting root disk since it is a volume',
instance=instance)
# NOTE(alaski): This is done twice to match the number of
# defined steps.
fake_step_to_show_snapshot_complete()
fake_step_to_show_snapshot_complete()
# NOTE(alaski): This is set to None to avoid transferring the
# VHD in power_down_and_transfer_leaf_vhds.
active_root_vdi_uuid = None
# snapshot and transfer all ephemeral disks
# then power down and transfer any diffs since
# the snapshots were taken
transfer_ephemeral_disks_then_all_leaf_vdis()
return
with vm_utils.snapshot_attached_here(
self._session, instance, vm_ref, label) as root_vdi_uuids:
# NOTE(johngarbutt) snapshot attached here will delete

View File

@ -325,3 +325,12 @@ def find_vbd_by_number(session, vm_ref, dev_number):
except session.XenAPI.Failure:
msg = "Error looking up VBD %s for %s" % (vbd_ref, vm_ref)
LOG.debug(msg, exc_info=True)
def is_booted_from_volume(session, vm_ref):
"""Determine if the root device is a volume."""
vbd_ref = find_vbd_by_number(session, vm_ref, 0)
vbd_other_config = session.VBD.get_other_config(vbd_ref)
if vbd_other_config.get('osvol', False):
return True
return False