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:
parent
3fd422db7e
commit
ff4ffab382
6 changed files with 136 additions and 21 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue