1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

fix(instances): snapshot tab reads less and fails less

The snapshot tab listed the VM's snapshots three times per page: once
per kind and once more for all of them. The list of all is now made
from the two kinds, so the combined flag is no longer used anywhere.

The revert confirm named only the disks the persistent definition
would lose. A disk hot-plugged into the running VM alone is detached
too, so the live definition is compared with what ran when the
snapshot was taken.

A storage pool refresh error after libvirt had already made, reverted
or deleted the snapshot was shown as if the operation had failed. It
is now logged, and the left-behind check is skipped then, since a stale
pool still lists the removed files. A volume removed while that check
reads it is skipped too.

An error reading the block jobs no longer breaks the instance page;
create, revert and delete still query the jobs again and refuse.
This commit is contained in:
catborise 2026-10-12 22:13:49 +03:00
parent 3fd422db7e
commit ff4ffab382
6 changed files with 136 additions and 21 deletions

View file

@ -263,7 +263,7 @@ class Instance(models.Model):
@cached_property
def snapshots(self) -> list[dict]:
return self._snapshots(VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL | VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL)
return sorted(self.internal_snapshots + self.external_snapshots, reverse=True, key=lambda k: k["date"])
@cached_property
def internal_snapshots(self) -> list[dict]:
@ -275,7 +275,11 @@ class Instance(models.Model):
@cached_property
def snapshot_jobs(self) -> list[dict]:
return self.proxy.get_snapshot_jobs()
# only for the page; create, revert and delete query the jobs again and refuse if that fails
try:
return self.proxy.get_snapshot_jobs()
except libvirtError:
return []
@cached_property
def inst_xml(self):

View file

@ -110,6 +110,23 @@ class LiveExternalSnapshotTestCase(SimpleTestCase):
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)

View file

@ -94,8 +94,11 @@ class SnapshotTabTestCase(TestCase):
proxy.get_snapshot.side_effect = lambda flag: {
libvirt.VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL: list(internal),
libvirt.VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL: list(external),
}.get(flag, [*internal, *external])
proxy.get_snapshot_jobs.return_value = list(jobs)
}[flag]
if isinstance(jobs, Exception):
proxy.get_snapshot_jobs.side_effect = jobs
else:
proxy.get_snapshot_jobs.return_value = list(jobs)
proxy.get_status.return_value = status
proxy.get_nvram.return_value = nvram
proxy.get_memory.return_value = proxy.get_cur_memory.return_value = 1024
@ -106,6 +109,7 @@ class SnapshotTabTestCase(TestCase):
proxy.get_console_type.return_value = "vnc"
response = self.client.get(reverse("instances:instance", args=[self.instance.id]))
self.assertEqual(response.status_code, 200)
self.proxy = proxy
return lxml_html.fromstring(response.content)
def snap(self, name, **kw):
@ -120,6 +124,19 @@ class SnapshotTabTestCase(TestCase):
self.assertIn("vdb (/pool/later.qcow2)", text)
self.assertIn("NVRAM", text)
def test_each_kind_is_listed_once_per_page(self):
doc = self.page(internal=[self.snap("i")], external=[self.snap("e")])
flags = sorted(c.args[0] for c in self.proxy.get_snapshot.call_args_list)
self.assertEqual(
flags, sorted([libvirt.VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL, libvirt.VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL])
)
self.assertEqual(doc.xpath("//i[contains(@title, 'snapshot(s)')]/@title"), ["There are 2 snapshot(s)"])
def test_a_failed_job_query_still_renders_the_tab(self):
# the actions themselves check for jobs again and refuse if that fails
doc = self.page(external=[self.snap("e")], jobs=libvirt.libvirtError("no job info"))
self.assertEqual(len(doc.xpath("//button[@name='revert_external_snapshot']")), 1)
def test_memory_checkbox_only_for_a_running_vm(self):
self.assertFalse(self.page(status=1).xpath("//input[@name='memory']")[0].get("disabled"))
self.assertIsNotNone(self.page(status=5).xpath("//input[@name='memory']")[0].get("disabled"))

View file

@ -520,7 +520,8 @@ class InstancesTestCase(TestCase):
)
self.assertEqual(response.status_code, 302)
del self.instance.snapshots
for cached in ("snapshots", "internal_snapshots", "external_snapshots"):
self.instance.__dict__.pop(cached, None)
self.assertEqual(len(self.instance.snapshots), 1)
response = self.client.post(
@ -541,7 +542,8 @@ class InstancesTestCase(TestCase):
)
self.assertEqual(response.status_code, 302)
del self.instance.snapshots
for cached in ("snapshots", "internal_snapshots", "external_snapshots"):
self.instance.__dict__.pop(cached, None)
self.assertEqual(len(self.instance.snapshots), 0)
def test_autostart(self):