Rebase qcow2 images when unshelving an instance

During unshelve, instance is spawn with image created by shelve
and is deleted just after, instance.image_ref still point
to the original instance build image.

In qcow2 environment, this is an issue because instance backing file
don't match anymore instance.image_ref and during live-migration/resize,
target host will fetch image corresponding to instance.image_ref
involving instance corruption.

This change fetches original image and rebase instance disk on it.
This avoid image_ref mismatch and bring back storage benefit to keep common
image in cache.

If original image is no more available in glance, backing file is merged into
disk(flatten), ensuring instance integrity during next live-migration/resize
operation.

Change-Id: I1a33fadf0b7439cf06c06cba2bc06df6cef0945b
Closes-Bug: #1732428
This commit is contained in:
Alexandre Arents 2019-11-26 10:26:32 +00:00
parent 2ad25db5f3
commit 8953a68946
2 changed files with 106 additions and 0 deletions

View File

@ -22622,6 +22622,74 @@ class LibvirtDriverTestCase(test.NoDBTestCase, TraitsComparisonMixin):
self.assertEqual('migrating instance across cells',
mock_debug.call_args[0][2])
@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):
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)
# Instance state during unshelve spawn().
inst_params = {
'image_ref': shelved_image_ref,
'vm_state': vm_states.SHELVED_OFFLOADED,
'system_metadata': {'image_base_image_ref': base_image_ref}
}
instance = self._create_instance(params=inst_params)
disk_images = {'image_id': instance.image_ref}
instance_dir = libvirt_utils.get_instance_path(instance)
disk_path = os.path.join(instance_dir, 'disk')
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
if original_image_in_glance:
# We expect final backing file is original image, not shelved one.
expected_backing_file = os.path.join(
imagecache.ImageCacheManager().cache_dir,
base_root_fname)
else:
# None means rebase will merge backing file into disk(flatten).
expected_backing_file = None
mock_fetch.side_effect = [
None,
exception.ImageNotFound(image_id=base_image_ref)
]
drvr._create_and_inject_local_root(
self.context, instance, False, '', disk_images, None, None)
mock_fetch.assert_has_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),
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)
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()
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)
@mock.patch('nova.virt.libvirt.driver.imagebackend')
@mock.patch('nova.virt.libvirt.driver.LibvirtDriver._inject_data')
@mock.patch('nova.virt.libvirt.driver.imagecache')

View File

@ -4131,6 +4131,14 @@ 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
# 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)
if need_inject:
self._inject_data(backend, instance, injection_info)
@ -4140,6 +4148,36 @@ 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.
# 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.
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)
base_backing_fname = os.path.join(base_dir, root_fname)
try:
self._try_fetch_image_cache(backend, libvirt_utils.fetch_image,
context, root_fname, base_image_ref,
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 '
'image and cannot be rebased to original image '
'because it is no longer available in the image '
'service, disk will be consequently flattened.',
instance=instance)
base_backing_fname = None
LOG.info('Rebasing disk image.', instance=instance)
self._rebase_with_qemu_img(backend.path, base_backing_fname)
def _create_configdrive(self, context, instance, injection_info,
rescue=False):
# As this method being called right after the definition of a