1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

fix(instances): deleting a VM's data takes its snapshot files too

Deleting a VM with its data deleted only the images its disks pointed
at. With external snapshots that is the newest overlay: the image the
snapshots were taken on, the older overlays and the memory files stayed
on disk, with nothing referring to them once the VM and its snapshot
metadata were gone.

The files the VM's snapshots were taken on, down each disk's backing
chain, the overlays of snapshots kept off the chain after a revert, and
the snapshots' memory files are now the VM's own too. A template the
VM grew from ends the chain and stays, as does an overlay of a disk
detached since. The backing references of the VM's own volumes no
longer count as another domain's use, and a file another VM uses keeps
the files below it.

A backing file that cannot be read (other than one in no pool) stops the
split before anything is deleted, instead of ending the chain there.
This commit is contained in:
catborise 2026-10-12 22:56:20 +03:00
parent ff4ffab382
commit 08cad04468
4 changed files with 186 additions and 6 deletions

View file

@ -1479,8 +1479,9 @@ class wvmInstance(wvmConnect):
iso.extend(name for name in names if name.lower().endswith(".iso"))
return iso
def paths_used_by_other_domains(self):
"""Paths another domain uses as a disk or as a backing file of one."""
def paths_used_by_other_domains(self, own=()):
"""Paths another domain uses as a disk or as a backing file of one.
The volumes in own are this VM's: their backing files do not count."""
uuid = self.instance.UUIDString()
used = set()
for dom in self.wvm.listAllDomains():
@ -1499,19 +1500,73 @@ class wvmInstance(wvmConnect):
pool.refresh(0)
for vol in pool.listAllVolumes():
with contextlib.suppress(libvirtError):
used.update(etree.fromstring(vol.XMLDesc(0)).xpath("./backingStore/path/text()"))
if vol.path() not in own:
used.update(etree.fromstring(vol.XMLDesc(0)).xpath("./backingStore/path/text()"))
return used
def split_disk_paths_by_use(self):
"""This VM's disk paths, split into (only this VM, also used by another domain)."""
"""This VM's disk paths, split into (only this VM, also used by another
domain). With external snapshots, the files below a disk that the
snapshots were taken on, and their memory files, are the VM's too."""
self.refresh_instance_pools()
own = [disk["path"] for disk in self.get_disk_devices() if disk["path"]]
used_elsewhere = self.paths_used_by_other_domains()
own += self._snapshot_chain_files(own)
used_elsewhere = self.paths_used_by_other_domains(own)
# a kept file keeps the files below it
for path in [path for path in own if path in used_elsewhere]:
while (path := self._backing_path(path)) and path not in used_elsewhere:
used_elsewhere.add(path)
return (
[path for path in own if path not in used_elsewhere],
[path for path in own if path in used_elsewhere],
)
def _snapshot_chain_files(self, tops):
"""The files of this VM's external snapshots that these disk images
grow from or that grow from them: the backing files snapshots were
taken on, down the chain, the overlays of snapshots off the chain
(newer ones kept after a revert), and the snapshots' memory files. A
backing file the VM started from (a template) is no snapshot point
and ends the chain; an overlay that does not grow from the chain (a
disk detached since) stays."""
points, overlays, memory = set(), set(), []
for snap in self.instance.listAllSnapshots(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL):
tree = etree.fromstring(snap.getXMLDesc(0))
memory += tree.xpath("memory/@file")
overlays.update(tree.xpath("disks/disk/source/@file"))
saved = self._saved_definition(tree)
if saved is not None:
points.update(self._disk_sources(saved).values())
files = list(tops)
for path in tops:
while (path := self._backing_path(path)) in points and path not in files:
files.append(path)
grew = True
while grew:
grew = False
for top in overlays.difference(files):
if top in files: # reached by a branch in this pass
continue
branch, path = [top], top
while (path := self._backing_path(path)) in points | overlays and path not in files:
branch.append(path)
if path in files:
files += branch
grew = True
return files[len(tops):] + memory
def _backing_path(self, path):
"""The backing file of a volume; None if it has none or is in no pool.
Any other error is raised: a chain cut short would let a file a kept
one needs count as deletable."""
try:
vol = self.get_volume_by_path(path)
except libvirtError as err:
if err.get_error_code() != VIR_ERR_NO_STORAGE_VOL:
raise
return None
return util.get_xml_path(vol.XMLDesc(0), "/volume/backingStore/path")
def _snapshotCreateXML(self, xml, flag):
self.instance.snapshotCreateXML(xml, flag)