libvirt:on snapshot delete, use qemu-img to blockRebase if VM is stopped

Libvirt won't do a blockRebase on a domain that is not running. So,
in that case, use "qemu-img rebase" instead.

Note: For now, trying to rebase a network disk using qemu-img raises
a NovaException error because I can't test that it successfully works
for every protocol (gluster, sheepdog, etc) that executes this code
path. I successfully tested this with file-based disk.

Change-Id: I0e6819a6c8dc70b9bd7d1a9c18dc185b4537a3e4
Closes-Bug: #1444806
Closes-Bug: #1465416
(cherry picked from commit 1cf793df25)
This commit is contained in:
Jordan Pittier 2015-07-15 14:07:29 +02:00 committed by Deepak C Shetty
parent 0871729c21
commit aea8790218
2 changed files with 156 additions and 8 deletions

View File

@ -292,6 +292,9 @@ class FakeVirtDomain(object):
def fsThaw(self, disks=None, flags=0):
pass
def isActive(self):
return True
class CacheConcurrencyTestCase(test.NoDBTestCase):
def setUp(self):
@ -14594,6 +14597,97 @@ class LibvirtVolumeSnapshotTestCase(test.NoDBTestCase):
self.mox.VerifyAll()
def _setup_block_rebase_domain_and_guest_mocks(self, dom_xml):
mock_domain = mock.Mock(spec=fakelibvirt.virDomain)
mock_domain.XMLDesc.return_value = dom_xml
guest = libvirt_guest.Guest(mock_domain)
exc = fakelibvirt.make_libvirtError(
fakelibvirt.libvirtError, 'virDomainBlockRebase() failed',
error_code=fakelibvirt.VIR_ERR_OPERATION_INVALID)
mock_domain.blockRebase.side_effect = exc
return mock_domain, guest
@mock.patch.object(host.Host, "has_min_version",
mock.Mock(return_value=True))
@mock.patch("nova.virt.libvirt.guest.Guest.is_active",
mock.Mock(return_value=False))
@mock.patch('nova.virt.libvirt.utils.get_disk_type',
return_value="fake_fmt")
@mock.patch('nova.utils.execute')
def test_volume_snapshot_delete_when_dom_not_running(self, mock_execute,
mock_get_disk_type):
"""Deleting newest snapshot of a file-based image when the domain is
not running should trigger a blockRebase using qemu-img not libvirt.
In this test, we rebase the image with another image as backing file.
"""
mock_domain, guest = self._setup_block_rebase_domain_and_guest_mocks(
self.dom_xml)
instance = objects.Instance(**self.inst)
snapshot_id = 'snapshot-1234'
with mock.patch.object(self.drvr._host, 'get_guest',
return_value=guest):
self.drvr._volume_snapshot_delete(self.c, instance,
self.volume_uuid, snapshot_id,
self.delete_info_1)
mock_get_disk_type.assert_called_once_with("snap.img")
mock_execute.assert_called_once_with('qemu-img', 'rebase',
'-b', 'snap.img', '-F',
'fake_fmt', 'disk1_file')
@mock.patch.object(host.Host, "has_min_version",
mock.Mock(return_value=True))
@mock.patch("nova.virt.libvirt.guest.Guest.is_active",
mock.Mock(return_value=False))
@mock.patch('nova.virt.libvirt.utils.get_disk_type',
return_value="fake_fmt")
@mock.patch('nova.utils.execute')
def test_volume_snapshot_delete_when_dom_not_running_and_no_rebase_base(
self, mock_execute, mock_get_disk_type):
"""Deleting newest snapshot of a file-based image when the domain is
not running should trigger a blockRebase using qemu-img not libvirt.
In this test, the image is rebased onto no backing file (i.e.
it will exist independently of any backing file)
"""
mock_domain, mock_guest = (
self._setup_block_rebase_domain_and_guest_mocks(self.dom_xml))
instance = objects.Instance(**self.inst)
snapshot_id = 'snapshot-1234'
with mock.patch.object(self.drvr._host, 'get_guest',
return_value=mock_guest):
self.drvr._volume_snapshot_delete(self.c, instance,
self.volume_uuid, snapshot_id,
self.delete_info_3)
self.assertEqual(0, mock_get_disk_type.call_count)
mock_execute.assert_called_once_with('qemu-img', 'rebase',
'-b', '', 'disk1_file')
@mock.patch.object(host.Host, "has_min_version",
mock.Mock(return_value=True))
@mock.patch("nova.virt.libvirt.guest.Guest.is_active",
mock.Mock(return_value=False))
def test_volume_snapshot_delete_when_dom_with_nw_disk_not_running(self):
"""Deleting newest snapshot of a network disk when the domain is not
running should raise a NovaException.
"""
mock_domain, mock_guest = (
self._setup_block_rebase_domain_and_guest_mocks(
self.dom_netdisk_xml))
instance = objects.Instance(**self.inst)
snapshot_id = 'snapshot-1234'
with mock.patch.object(self.drvr._host, 'get_guest',
return_value=mock_guest):
ex = self.assertRaises(exception.NovaException,
self.drvr._volume_snapshot_delete,
self.c, instance, self.volume_uuid,
snapshot_id, self.delete_info_1)
self.assertIn('has not been fully tested', six.text_type(ex))
def test_volume_snapshot_delete_2(self):
"""Deleting older snapshot -- blockCommit."""

View File

@ -1798,6 +1798,51 @@ class LibvirtDriver(driver.ComputeDriver):
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_snapshot)
timer.start(interval=0.5).wait()
@staticmethod
def _rebase_with_qemu_img(guest, device, active_disk_object,
rebase_base):
"""Rebase a device tied to a guest using qemu-img.
:param guest:the Guest which owns the device being rebased
:type guest: nova.virt.libvirt.guest.Guest
:param device: the guest block device to rebase
:type device: nova.virt.libvirt.guest.BlockDevice
:param active_disk_object: the guest block device to rebase
:type active_disk_object: nova.virt.libvirt.config.\
LibvirtConfigGuestDisk
:param rebase_base: the new parent in the backing chain
:type rebase_base: None or string
"""
# It's unsure how well qemu-img handles network disks for
# every protocol. So let's be safe.
active_protocol = active_disk_object.source_protocol
if active_protocol is not None:
msg = _("Something went wrong when deleting a volume snapshot: "
"rebasing a %(protocol)s network disk using qemu-img "
"has not been fully tested") % {'protocol':
active_protocol}
LOG.error(msg)
raise exception.NovaException(msg)
if rebase_base is None:
# If backing_file is specified as "" (the empty string), then
# the image is rebased onto no backing file (i.e. it will exist
# independently of any backing file).
backing_file = ""
qemu_img_extra_arg = []
else:
# If the rebased image is going to have a backing file then
# explicitly set the backing file format to avoid any security
# concerns related to file format auto detection.
backing_file = rebase_base
b_file_fmt = libvirt_utils.get_disk_type(backing_file)
qemu_img_extra_arg = ['-F', b_file_fmt]
qemu_img_extra_arg.append(active_disk_object.source_path)
utils.execute("qemu-img", "rebase", "-b", backing_file,
*qemu_img_extra_arg)
def _volume_snapshot_delete(self, context, instance, volume_id,
snapshot_id, delete_info=None):
"""Note:
@ -1951,15 +1996,24 @@ class LibvirtDriver(driver.ComputeDriver):
'relative': str(relative)}, instance=instance)
dev = guest.get_block_device(rebase_disk)
result = dev.rebase(rebase_base, relative=relative)
if result == 0:
LOG.debug('blockRebase started successfully',
instance=instance)
if guest.is_active():
result = dev.rebase(rebase_base, relative=relative)
if result == 0:
LOG.debug('blockRebase started successfully',
instance=instance)
while dev.wait_for_job(abort_on_error=True):
LOG.debug('waiting for blockRebase job completion',
instance=instance)
time.sleep(0.5)
while dev.wait_for_job(abort_on_error=True):
LOG.debug('waiting for blockRebase job completion',
instance=instance)
time.sleep(0.5)
# If the guest is not running libvirt won't do a blockRebase.
# In that case, let's ask qemu-img to rebase the disk.
else:
LOG.debug('Guest is not running so doing a block rebase '
'using "qemu-img rebase"', instance=instance)
self._rebase_with_qemu_img(guest, dev, active_disk_object,
rebase_base)
else:
# commit with blockCommit()