libvirt: make cross cell resize spawn from snapshot image

During a cross cell resize, we do an instance snapshot and
then we spawn() instance back on target cell.

Unfortunately, we mistakenly spawn back the instance to its original
image id, instead of using freshly created snapshot_id.

The change proposes to update instance.image_ref with snapshot_id
in order that spawn()->_create_image() uses it and set back
instance.image_ref after.

Note that for qcow2 backend case, we also need to rebase disk image
with its original backing file to avoid mismatch between
instance.image_ref and backing file, as we currently do in unshelve
context.

Change-Id: I0b81282eba8238d8b64a67e38cf9d6392de1f85c
Closes-Bug: #1906428
This commit is contained in:
Alexandre Arents 2020-12-04 15:19:08 +00:00
parent 048250a4b7
commit f02899418d
5 changed files with 116 additions and 39 deletions

View File

@ -3958,6 +3958,14 @@ class API(base.Base):
current_instance_type = instance.get_flavor()
# NOTE(aarents): Ensure image_base_image_ref is present as it will be
# needed during finish_resize/cross_cell_resize. Instances upgraded
# from an older nova release may not have this property because of
# a rebuild bug Bug/1893618.
instance.system_metadata.update(
{'image_base_image_ref': instance.image_ref}
)
# If flavor_id is not provided, only migrate the instance.
volume_backed = None
if not flavor_id:

View File

@ -5902,7 +5902,9 @@ class ComputeManager(manager.Manager):
other generic error handling.
"""
# Figure out the image metadata to use when spawning the guest.
origin_image_ref = instance.image_ref
if snapshot_id:
instance.image_ref = snapshot_id
image_meta = objects.ImageMeta.from_image_ref(
ctxt, self.image_api, snapshot_id)
else:
@ -5940,6 +5942,7 @@ class ComputeManager(manager.Manager):
# If we spawned from a temporary snapshot image we can delete that now,
# similar to how unshelve works.
if snapshot_id:
instance.image_ref = origin_image_ref
compute_utils.delete_image(
ctxt, instance, self.image_api, snapshot_id)

View File

@ -1240,6 +1240,7 @@ class ServersPolicyTest(base.BasePolicyTest):
self.project_member_context,
id=1, uuid=uuids.fake_id, project_id=self.project_id,
user_id='fake-user', vm_state=vm_states.ACTIVE,
expected_attrs=['system_metadata'],
launched_at=timeutils.utcnow())
mock_get.side_effect = fake_get

View File

@ -23164,22 +23164,25 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
@mock.patch.object(libvirt_driver.LibvirtDriver, '_try_fetch_image_cache')
@mock.patch.object(libvirt_driver.LibvirtDriver, '_rebase_with_qemu_img')
def _test_unshelve_qcow2_rebase_image_during_create(self,
mock_rebase, mock_fetch, original_image_in_glance=True):
def _test_qcow2_rebase_image_during_create(self,
mock_rebase, mock_fetch, image_ref, base_image_ref, vm_state=None,
task_state=None, original_image_in_glance=True,
rebase_expected=True):
self.flags(images_type='qcow2', group='libvirt')
# Original image ref from where instance was created, before SHELVE
# occurs, base_root_fname is related backing file name.
base_image_ref = 'base_image_ref'
base_root_fname = imagecache.get_cache_fname(base_image_ref)
# Snapshot image ref created during SHELVE.
shelved_image_ref = 'shelved_image_ref'
shelved_root_fname = imagecache.get_cache_fname(shelved_image_ref)
# base_image_ref: original image ref from where instance was created,
# stored in system_metadata at instance creation.
# image_ref: current instance.image_ref used in unshelve/resize)
# vm_state: current vm_state
# Instance state during unshelve spawn().
base_image_root_fname = imagecache.get_cache_fname(base_image_ref)
image_root_fname = imagecache.get_cache_fname(image_ref)
# Instance state during _create_and_inject_local_root call.
inst_params = {
'image_ref': shelved_image_ref,
'vm_state': vm_states.SHELVED_OFFLOADED,
'image_ref': image_ref,
'vm_state': vm_state,
'task_state': task_state,
'system_metadata': {'image_base_image_ref': base_image_ref}
}
@ -23193,7 +23196,7 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
# We expect final backing file is original image, not shelved one.
expected_backing_file = os.path.join(
imagecache.ImageCacheManager().cache_dir,
base_root_fname)
base_image_root_fname)
else:
# None means rebase will merge backing file into disk(flatten).
expected_backing_file = None
@ -23205,36 +23208,81 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
drvr._create_and_inject_local_root(
self.context, instance, False, '', disk_images, None, None)
mock_fetch.assert_has_calls([
mock_fetch_calls = [
mock.call(test.MatchType(nova.virt.libvirt.imagebackend.Qcow2),
libvirt_utils.fetch_image,
self.context, shelved_root_fname, shelved_image_ref,
instance, instance.root_gb * units.Gi, None),
self.context, image_root_fname, image_ref,
instance, instance.root_gb * units.Gi, None)
]
if rebase_expected:
# if we rebase we must expect a 2nd fetch call, to cache the
# original backing file.
mock_fetch_calls.append(
mock.call(test.MatchType(nova.virt.libvirt.imagebackend.Qcow2),
libvirt_utils.fetch_image,
self.context, base_root_fname, base_image_ref,
instance, None)])
mock_rebase.assert_called_once_with(disk_path, expected_backing_file)
self.context, base_image_root_fname, base_image_ref,
instance, None))
mock_rebase.assert_called_once_with(disk_path,
expected_backing_file)
else:
mock_rebase.assert_not_called()
mock_fetch.assert_has_calls(mock_fetch_calls)
def test_unshelve_qcow2_rebase_image_during_create(self):
# Original image is present in Glance. In that case the 2nd
# fetch succeeds and we rebase instance disk to original image backing
# file, instance is back to nominal state: after unshelve,
# instance.image_ref will match current backing file.
self._test_unshelve_qcow2_rebase_image_during_create()
self._test_qcow2_rebase_image_during_create(
image_ref='snapshot_id_of_shelved_instance',
base_image_ref='original_image_id',
vm_state=vm_states.SHELVED_OFFLOADED,
rebase_expected=True)
def test_unshelve_qcow2_rebase_image_during_create_notfound(self):
# Original image is no longer available in Glance, so 2nd fetch
# will failed (HTTP 404). In that case qemu-img rebase will merge
# backing file into disk, removing backing file dependency.
self._test_unshelve_qcow2_rebase_image_during_create(
original_image_in_glance=False)
self._test_qcow2_rebase_image_during_create(
image_ref='snapshot_id_of_shelved_instance',
base_image_ref='original_image_id',
vm_state=vm_states.SHELVED_OFFLOADED,
original_image_in_glance=False,
rebase_expected=True)
@mock.patch('nova.virt.libvirt.driver.imagebackend')
def test_cross_cell_resize_qcow2_rebase_image_during_create(self):
self._test_qcow2_rebase_image_during_create(
image_ref='snapshot_id_of_resized_instance',
base_image_ref='original_image_id',
task_state=task_states.RESIZE_FINISH,
rebase_expected=True)
def test_cross_cell_resize_qcow2_rebase_image_during_create_notfound(self):
self._test_qcow2_rebase_image_during_create(
image_ref='snapshot_id_of_resized_instance',
base_image_ref='original_image_id',
task_state=task_states.RESIZE_FINISH,
original_image_in_glance=False,
rebase_expected=True)
def test_local_cell_resize_qcow2_rebase_image_during_create(self):
# local cell resize does not go into a spawn from a snapshot,
# consequently, instance.image_ref remain the same and we must ensure
# that no rebase is done.
self._test_qcow2_rebase_image_during_create(
image_ref='original_image_id',
base_image_ref='original_image_id',
task_state=task_states.RESIZE_FINISH,
rebase_expected=False)
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver.'
'_try_fetch_image_cache', new=mock.Mock())
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._inject_data')
@mock.patch('nova.virt.libvirt.driver.imagecache')
def test_data_not_injects_with_configdrive(self, mock_image, mock_inject,
mock_backend):
@mock.patch('nova.virt.libvirt.driver.imagecache', new=mock.Mock())
def test_data_not_injects_with_configdrive(self, mock_inject):
self.flags(inject_partition=-1, group='libvirt')
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)

View File

@ -4184,13 +4184,11 @@ class LibvirtDriver(driver.ComputeDriver):
root_fname, disk_images['image_id'],
instance, size, fallback_from_host)
# During unshelve on Qcow2 backend, we spawn() using snapshot image
# created during shelve. Extra work is needed in order to rebase
# During unshelve or cross cell resize on Qcow2 backend, we spawn()
# using a snapshot image. Extra work is needed in order to rebase
# disk image to its original image_ref. Disk backing file will
# then represent back image_ref instead of shelved image.
if (instance.vm_state == vm_states.SHELVED_OFFLOADED and
isinstance(backend, imagebackend.Qcow2)):
self._finalize_unshelve_qcow2_image(context, instance, backend)
# then represent back image_ref instead of snapshot image.
self._rebase_original_qcow2_image(context, instance, backend)
if need_inject:
self._inject_data(backend, instance, injection_info)
@ -4201,13 +4199,32 @@ class LibvirtDriver(driver.ComputeDriver):
return created_disks
def _finalize_unshelve_qcow2_image(self, context, instance, backend):
# NOTE(aarents): During qcow2 instance unshelve, backing file
# represents shelved image, not original instance.image_ref.
# We rebase here instance disk to original image.
def _needs_rebase_original_qcow2_image(self, instance, backend):
if not isinstance(backend, imagebackend.Qcow2):
return False
if instance.vm_state == vm_states.SHELVED_OFFLOADED:
return True
if instance.task_state == task_states.RESIZE_FINISH:
# We need to distinguish between local versus cross cell resize.
# Rebase is only needed in cross cell case because instance
# is spawn from a snapshot.
base_image_ref = instance.system_metadata.get(
'image_base_image_ref')
if base_image_ref != instance.image_ref:
return True
return False
def _rebase_original_qcow2_image(self, context, instance, backend):
# NOTE(aarents): During qcow2 instance unshelve/cross_cell_resize,
# backing file represents a snapshot image, not original
# instance.image_ref. We rebase here instance disk to original image.
# This second fetch call does nothing except downloading original
# backing file if missing, as image disk have already been
# created/resized by first fetch call.
if not self._needs_rebase_original_qcow2_image(instance, backend):
return
base_dir = self.image_cache_manager.cache_dir
base_image_ref = instance.system_metadata.get('image_base_image_ref')
root_fname = imagecache.get_cache_fname(base_image_ref)
@ -4219,9 +4236,9 @@ class LibvirtDriver(driver.ComputeDriver):
instance, None)
except exception.ImageNotFound:
# We must flatten here in order to remove dependency with an orphan
# backing file (as shelved image will be dropped once unshelve
# is successfull).
LOG.warning('Current disk image is created on top of shelved '
# backing file (as snapshot image will be dropped once
# unshelve/cross_cell_resize is successfull).
LOG.warning('Current disk image is created on top of a snapshot '
'image and cannot be rebased to original image '
'because it is no longer available in the image '
'service, disk will be consequently flattened.',