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

fix(instances): name snapshot files libvirt could not remove

When libvirt cannot remove a snapshot file it merged or discarded, it
only logs a warning and the delete or revert still succeeds. On NFS it
removes them as the qemu user, so an image directory the qemu user may
not write to left every merged overlay on disk without a word.

Revert and delete now compare the files in use before and after and
show a warning naming the ones still on disk. Only a snapshot's own
files (overlays, memory) and overlays on a snapshot point count, and a
disk the revert detached is never named, so a kept disk is not offered
for removal.
This commit is contained in:
catborise 2026-10-12 16:32:51 +03:00
parent ff1508b3c2
commit 3fd422db7e
6 changed files with 194 additions and 11 deletions

View file

@ -54,6 +54,12 @@ class LiveExternalSnapshotTestCase(SimpleTestCase):
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)")
@ -66,10 +72,11 @@ class LiveExternalSnapshotTestCase(SimpleTestCase):
base = self.top(dom)
vm.create_external_snapshot("one", "first")
vm.create_external_snapshot("two")
vm.revert_external_snapshot("one")
self.assertEqual(vm.revert_external_snapshot("one"), [], "files left behind")
self.assertEqual(dom.state()[0], expected)
vm.delete_external_snapshot("two")
vm.delete_external_snapshot("one")
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))
@ -82,7 +89,7 @@ class LiveExternalSnapshotTestCase(SimpleTestCase):
dom.suspend()
vm.revert_external_snapshot("m")
self.assertEqual(dom.state()[0], libvirt.VIR_DOMAIN_RUNNING)
vm.delete_external_snapshot("m")
self.assertEqual(vm.delete_external_snapshot("m"), [])
self.assertFalse(livetest.volume_exists(self.conn, mem))
def test_uefi_vm_keeps_its_loader(self):

View file

@ -25,7 +25,7 @@ class SnapshotViewsTestCase(TestCase):
admin = get_user_model().objects.create_superuser("snap_admin", "s@example.com", "pw")
self.client.force_login(admin)
def post(self, view, data, error=None):
def post(self, view, data, error=None, left=()):
with patch("instances.models.wvmInstance") as mock_wvm:
proxy = mock_wvm.return_value
method = {
@ -36,8 +36,10 @@ class SnapshotViewsTestCase(TestCase):
"delete_snapshot": proxy.snapshot_delete,
"snapshot": proxy.create_snapshot,
}[view]
method.return_value = list(left) # files revert and delete left behind
if error:
method.side_effect = util.OperationError(error)
self.client.cookies.pop("messages", None) # only this request's messages
response = self.client.post(
reverse(f"instances:{view}", args=[self.instance.id]),
data,
@ -60,6 +62,16 @@ class SnapshotViewsTestCase(TestCase):
for power in ("start", "force_shutdown", "shutdown"):
getattr(proxy, power).assert_not_called()
def test_files_left_behind_are_named_in_a_warning(self):
for view in ("revert_external_snapshot", "delete_external_snapshot"):
with self.subTest(view):
_, _, messages = self.post(view, {"name": "a"}, left=["/pool/a.one", "/pool/vm.one.mem"])
warning = [m for m in messages if "/pool/a.one, /pool/vm.one.mem" in m]
self.assertEqual(len(warning), 1, messages)
self.assertIn("qemu user", warning[0])
_, _, messages = self.post(view, {"name": "a"})
self.assertFalse([m for m in messages if "not removed" in m], messages)
def test_refusals_become_messages(self):
for view in ("create_external_snapshot", "revert_external_snapshot", "delete_external_snapshot",
"revert_snapshot", "delete_snapshot", "snapshot"):

View file

@ -1371,6 +1371,20 @@ def revert_snapshot(request, pk):
return _back(request, pk, "managesnapshot")
def _warn_left_behind(request, files):
"""libvirt only logs it when it cannot remove a snapshot file."""
if files:
messages.warning(
request,
_(
"These snapshot files were not removed, although nothing uses them any more: %(files)s. "
"On NFS libvirt removes them as the qemu user, who needs write access to their directory. "
"Remove them by hand."
)
% {"files": ", ".join(files)},
)
@require_POST
@serialize_instance_mutation
def create_external_snapshot(request, pk):
@ -1398,11 +1412,12 @@ def revert_external_snapshot(request, pk):
raise PermissionDenied
name = request.POST.get("name", "")
try:
instance.proxy.revert_external_snapshot(name)
left = instance.proxy.revert_external_snapshot(name)
except libvirtError as err:
messages.error(request, _("Snapshot was not reverted: %(err)s") % {"err": error_text(request, err)})
else:
messages.success(request, _("Successful revert snapshot: ") + name)
_warn_left_behind(request, left)
msg = _("Revert external snapshot: %(snap)s") % {"snap": name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return _back(request, pk, "managesnapshot")
@ -1416,10 +1431,11 @@ def delete_external_snapshot(request, pk):
raise PermissionDenied
name = request.POST.get("name", "")
try:
instance.proxy.delete_external_snapshot(name)
left = instance.proxy.delete_external_snapshot(name)
except libvirtError as err:
messages.error(request, _("Snapshot was not deleted: %(err)s") % {"err": error_text(request, err)})
else:
_warn_left_behind(request, left)
msg = _("Delete external snapshot: %(snap)s") % {"snap": name}
addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
return _back(request, pk, "managesnapshot")