1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 04:06:47 +00:00
webvirtcloud/instances/tests/test_live_snapshots.py
catborise ff1508b3c2 feat(instances): external snapshots use libvirt's snapshot API
Create, revert and delete are one libvirt call each. The pflash-to-rom
loader rewrite, the hand-written block commit and the s1./s2. names are
gone, so UEFI VMs can take external snapshots, a revert keeps disks added
later, and deleting an older snapshot keeps the newer one.

Create refuses what libvirt could not revert or delete later: internal
snapshots on the VM, a saved state, a running snapshot job, disks that
are not files, CD-ROM media, a branch after a revert, and memory without
a running VM or room in the first disk's pool. A disk-only revert keeps
the VM running, paused or shut off; one with memory resumes it as saved.
A revert in the same second as the previous one, which libvirt fails
after stopping the VM, is retried once.
A snapshot the old code took of a UEFI VM saved the loader as rom; its
revert is refused (deleting it still works).

The snapshots tab lists internal and external snapshots separately, can
save memory too, shows running snapshot jobs, and the revert confirmation
names the disks it detaches and that NVRAM is not restored.
2026-10-12 15:25:44 +03:00

144 lines
6.2 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 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")
vm.revert_external_snapshot("one")
self.assertEqual(dom.state()[0], expected)
vm.delete_external_snapshot("two")
vm.delete_external_snapshot("one")
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)
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_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")