mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 04:36:48 +00:00
A disk-only snapshot of a running VM is crash-consistent: data the guest has not written yet is missing from it. With a connected QEMU guest agent, the snapshot form now offers to freeze the guest's file systems while the disks are snapshotted (libvirt's QUIESCE flag); the box is checked by default and can be cleared. It is offered only for a running VM whose agent is connected, and not together with memory: libvirt accepts quiesce only for disk-only snapshots, and a memory snapshot keeps the whole guest state anyway. A request that does not fit is refused before libvirt is called.
178 lines
8.4 KiB
Python
178 lines
8.4 KiB
Python
"""Live checks for external snapshots on libvirt's own API. Runs only with
|
|
TEST_LIBVIRT_HOST set (see instances/tests/livetest.py)."""
|
|
|
|
import os
|
|
import unittest
|
|
|
|
import libvirt
|
|
from django.test import SimpleTestCase
|
|
from lxml import etree
|
|
from vrtManager import util
|
|
from vrtManager.connection import connection_manager
|
|
from vrtManager.instance import wvmInstance
|
|
|
|
from . import livetest
|
|
|
|
P = livetest.PREFIX
|
|
|
|
|
|
class LiveExternalSnapshotTestCase(SimpleTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
if not livetest.enabled():
|
|
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
|
|
cls.args = (
|
|
os.environ["TEST_LIBVIRT_HOST"],
|
|
os.environ.get("TEST_LIBVIRT_LOGIN", ""),
|
|
os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
|
|
int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
|
|
)
|
|
cls.conn = connection_manager.get_connection(*cls.args)
|
|
livetest.cleanup(cls.conn)
|
|
livetest.ensure_pool(cls.conn)
|
|
cls.inventory = livetest.inventory(cls.conn)
|
|
super().setUpClass()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
try:
|
|
livetest.remove_pool(cls.conn)
|
|
finally:
|
|
super().tearDownClass()
|
|
if livetest.inventory(cls.conn) != cls.inventory:
|
|
raise AssertionError("Live tests changed objects outside the test namespace")
|
|
|
|
def tearDown(self):
|
|
livetest.cleanup(self.conn)
|
|
|
|
def vm(self, name, state="stopped", uefi=False, extra_devices=""):
|
|
base = livetest.create_volume(self.conn, P + name)
|
|
dom = livetest.define_vm(self.conn, P + name, [base], uefi=uefi, extra_devices=extra_devices)
|
|
if state in ("running", "paused"):
|
|
dom.create()
|
|
if state == "paused":
|
|
dom.suspend()
|
|
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
|
|
|
|
def pool_files(self, name):
|
|
"""The test pool's files whose names start with the VM's name."""
|
|
pool = self.conn.storagePoolLookupByName(livetest.POOL)
|
|
pool.refresh(0)
|
|
return {vol for vol in pool.listVolumes() if vol.startswith(P + name + ".")}
|
|
|
|
def top(self, dom):
|
|
return etree.fromstring(dom.XMLDesc(0)).xpath("string(devices/disk[@device='disk']/source/@file)")
|
|
|
|
def test_create_revert_delete_keep_the_state(self):
|
|
for state, expected in (("stopped", libvirt.VIR_DOMAIN_SHUTOFF),
|
|
("running", libvirt.VIR_DOMAIN_RUNNING),
|
|
("paused", libvirt.VIR_DOMAIN_PAUSED)):
|
|
with self.subTest(state):
|
|
dom, vm = self.vm("snap-" + state, state)
|
|
base = self.top(dom)
|
|
vm.create_external_snapshot("one", "first")
|
|
vm.create_external_snapshot("two")
|
|
self.assertEqual(vm.revert_external_snapshot("one"), [], "files left behind")
|
|
self.assertEqual(dom.state()[0], expected)
|
|
self.assertEqual(vm.delete_external_snapshot("two"), [], "files left behind")
|
|
self.assertEqual(vm.delete_external_snapshot("one"), [], "files left behind")
|
|
self.assertEqual(self.pool_files("snap-" + state), {os.path.basename(base)}, "an overlay stayed on disk")
|
|
self.assertEqual(dom.snapshotNum(0), 0)
|
|
self.assertEqual(dom.state()[0], expected)
|
|
self.assertTrue(livetest.volume_exists(self.conn, base))
|
|
|
|
def test_memory_snapshot_resumes_and_its_file_goes_with_it(self):
|
|
dom, vm = self.vm("snap-mem", "running")
|
|
vm.create_external_snapshot("m", memory=True)
|
|
mem = os.path.join(livetest.POOL_PATH, f"{dom.UUIDString()}.m.mem")
|
|
self.assertTrue(livetest.volume_exists(self.conn, mem))
|
|
dom.suspend()
|
|
vm.revert_external_snapshot("m")
|
|
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
|
|
self.assertEqual(vm.delete_external_snapshot("m"), [])
|
|
self.assertFalse(livetest.volume_exists(self.conn, mem))
|
|
|
|
def test_uefi_vm_keeps_its_loader(self):
|
|
dom, vm = self.vm("snap-uefi", "running", uefi=True)
|
|
os_before = etree.tostring(etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).find("os"))
|
|
vm.create_external_snapshot("u")
|
|
vm.revert_external_snapshot("u")
|
|
vm.delete_external_snapshot("u")
|
|
self.assertEqual(etree.tostring(etree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)).find("os")), os_before)
|
|
self.assertEqual(dom.snapshotNum(0), 0)
|
|
|
|
def test_back_to_back_reverts_keep_a_running_vm_running(self):
|
|
# Without the retry, the second revert within one second stopped the VM.
|
|
dom, vm = self.vm("snap-collide", "running")
|
|
vm.create_external_snapshot("a")
|
|
for _ in range(3):
|
|
vm.revert_external_snapshot("a")
|
|
vm.revert_external_snapshot("a")
|
|
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
|
|
|
|
def test_a_disk_hot_plugged_live_only_is_named_and_detached_by_a_revert(self):
|
|
dom, vm = self.vm("snap-hotplug", "running")
|
|
vm.create_external_snapshot("a")
|
|
extra = livetest.create_volume(self.conn, P + "snap-hotplug-extra")
|
|
dom.attachDeviceFlags(
|
|
f"<disk type='file' device='disk'><driver name='qemu' type='qcow2'/><source file='{extra}'/>"
|
|
"<target dev='vdb' bus='virtio'/></disk>",
|
|
libvirt.VIR_DOMAIN_AFFECT_LIVE,
|
|
)
|
|
[snap] = vm.get_snapshot(libvirt.VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL)
|
|
self.assertEqual(snap["detaches"], [{"dev": "vdb", "path": extra}])
|
|
self.assertEqual(vm.revert_external_snapshot("a"), [], "the detached disk is not a leftover")
|
|
self.assertNotIn("vdb", etree.fromstring(dom.XMLDesc(0)).xpath("devices/disk/target/@dev"))
|
|
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
|
|
self.assertTrue(livetest.volume_exists(self.conn, extra))
|
|
vm.delete_external_snapshot("a")
|
|
|
|
def test_a_mixed_vm_can_be_cleaned_up(self):
|
|
dom, vm = self.vm("snap-mixed-clean")
|
|
dom.snapshotCreateXML("<domainsnapshot><name>i</name></domainsnapshot>", 0)
|
|
dom.snapshotCreateXML(
|
|
"<domainsnapshot><name>e</name></domainsnapshot>", libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
|
|
)
|
|
# libvirt refuses an external snapshot whose parent is internal, so the
|
|
# internal one goes first
|
|
vm.snapshot_delete("i")
|
|
vm.delete_external_snapshot("e")
|
|
self.assertEqual(dom.snapshotNum(0), 0)
|
|
|
|
def test_chain_stays_linear(self):
|
|
dom, vm = self.vm("snap-linear")
|
|
vm.create_external_snapshot("a")
|
|
vm.create_external_snapshot("b")
|
|
vm.revert_external_snapshot("a")
|
|
with self.assertRaisesRegex(util.OperationError, "Newer snapshots"):
|
|
vm.create_external_snapshot("c")
|
|
vm.delete_external_snapshot("b")
|
|
vm.create_external_snapshot("c")
|
|
|
|
def test_refusals(self):
|
|
iso = livetest.create_volume(self.conn, P + "snap-media")
|
|
cdrom = (f"<disk type='file' device='cdrom'><driver name='qemu' type='qcow2'/><source file='{iso}'/>"
|
|
"<target dev='sda' bus='sata'/><readonly/></disk>")
|
|
_, vm = self.vm("snap-cdrom", extra_devices=cdrom)
|
|
with self.assertRaisesRegex(util.OperationError, "Eject"):
|
|
vm.create_external_snapshot("x")
|
|
|
|
dom, vm = self.vm("snap-saved", "running")
|
|
dom.managedSave(0)
|
|
with self.assertRaisesRegex(util.OperationError, "saved state"):
|
|
vm.create_external_snapshot("x")
|
|
|
|
dom, vm = self.vm("snap-mixed")
|
|
dom.snapshotCreateXML("<domainsnapshot><name>i</name></domainsnapshot>", 0)
|
|
with self.assertRaisesRegex(util.OperationError, "internal snapshots"):
|
|
vm.create_external_snapshot("x")
|
|
|
|
def test_quiesce_without_a_connected_agent_is_refused(self):
|
|
# no guest OS runs an agent here: the channel exists but is not connected
|
|
channel = "<channel type='unix'><target type='virtio' name='org.qemu.guest_agent.0'/></channel>"
|
|
for name, extra in (("snap-noagent", ""), ("snap-agentchan", channel)):
|
|
with self.subTest(name):
|
|
dom, vm = self.vm(name, "running", extra_devices=extra)
|
|
with self.assertRaisesRegex(util.OperationError, "guest agent is not connected"):
|
|
vm.create_external_snapshot("q", quiesce=True)
|
|
self.assertEqual(dom.snapshotNum(0), 0)
|