mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
fix(instances): a snapshot needs free space in the VM's storage pools
A disk-only snapshot was taken without any space check, though its overlays grow as the guest writes and a later delete merges them down; a full pool pauses the guest and can leave the snapshot undeletable. Only a memory snapshot was checked, for the memory alone. Before a snapshot, each storage pool with a disk of the VM must have free a share of those disks' virtual size (10%, at least 1 GiB), and the pool of the memory file the VM's memory on top. Both values are on the Settings page (Snapshot Free Percent, Snapshot Free Min GiB; 0 turns a rule off, an invalid value keeps the default). The pools are refreshed first, and a failed refresh or an unreadable volume refuses the snapshot instead of skipping the check; a disk in no pool is not checked. Nothing is reserved: later writes can still fill a pool.
This commit is contained in:
parent
f766890825
commit
5f8f7a8643
8 changed files with 227 additions and 30 deletions
|
|
@ -1576,12 +1576,14 @@ class wvmInstance(wvmConnect):
|
|||
</domainsnapshot>"""
|
||||
self._snapshotCreateXML(xml, 0)
|
||||
|
||||
def create_external_snapshot(self, name, desc="", memory=False, quiesce=False):
|
||||
def create_external_snapshot(self, name, desc="", memory=False, quiesce=False, free_percent=10, free_min_gib=1):
|
||||
"""A disk-only snapshot, or with memory too (running VMs only). Every
|
||||
disk gets a libvirt-named overlay next to it; the memory file goes
|
||||
next to the first disk. quiesce: the guest agent freezes the guest's
|
||||
file systems for a disk-only snapshot of a running VM (libvirt
|
||||
accepts it only without memory, which keeps the whole state anyway)."""
|
||||
accepts it only without memory, which keeps the whole state anyway).
|
||||
free_percent, free_min_gib: the free space each pool keeps (see
|
||||
_check_snapshot_space)."""
|
||||
if not SNAPSHOT_NAME.fullmatch(name):
|
||||
raise util.OperationError(
|
||||
"Snapshot names are 1-64 letters, digits, '.', '_' or '-' and do not start with '.'"
|
||||
|
|
@ -1612,7 +1614,17 @@ class wvmInstance(wvmConnect):
|
|||
raise util.OperationError(f"Eject the media from {dev} first")
|
||||
elif disk.get("type") != "file":
|
||||
raise util.OperationError(f"Disk {dev} is not a file; external snapshots need file disks")
|
||||
memory_xml = self._snapshot_memory_xml(name, tree) if memory else ""
|
||||
disks = list(self._disk_sources(tree).values())
|
||||
# free space, and the top overlays, are known to the pools only after
|
||||
# a refresh; nothing has changed yet when it fails
|
||||
try:
|
||||
self.refresh_instance_pools()
|
||||
except libvirtError as err:
|
||||
raise util.OperationError(
|
||||
f"The storage pools could not be refreshed, so their free space is unknown: {err}"
|
||||
) from None
|
||||
memory_xml, memory_pool = self._snapshot_memory(name, disks) if memory else ("", None)
|
||||
self._check_snapshot_space(disks, memory_pool, free_percent, free_min_gib)
|
||||
xml = (
|
||||
f"<domainsnapshot><name>{name}</name>"
|
||||
f"<description>{util.xml_escape(desc)}</description>{memory_xml}</domainsnapshot>"
|
||||
|
|
@ -1625,24 +1637,50 @@ class wvmInstance(wvmConnect):
|
|||
self._snapshotCreateXML(xml, flags)
|
||||
self._refresh_pools_after_snapshot()
|
||||
|
||||
def _snapshot_memory_xml(self, name, tree):
|
||||
"""The <memory> element of a snapshot that saves memory: a file in the
|
||||
storage pool of the first disk, which must have room for it."""
|
||||
def _snapshot_memory(self, name, disks):
|
||||
"""The <memory> element of a snapshot that saves memory, and the
|
||||
storage pool its file goes into: the pool of the first disk."""
|
||||
if self.get_status() != 1:
|
||||
raise util.OperationError("Memory is saved only from a running VM")
|
||||
disks = list(self._disk_sources(tree).values())
|
||||
if not disks:
|
||||
raise util.OperationError("The VM has no disk to keep its memory next to")
|
||||
# the top overlay is known to its pool only after a refresh
|
||||
self.refresh_instance_pools()
|
||||
try:
|
||||
pool = self.get_volume_by_path(disks[0]).storagePoolLookupByVolume()
|
||||
except libvirtError:
|
||||
raise util.OperationError(f"{disks[0]} is not in a storage pool; memory is saved only into one") from None
|
||||
if pool.info()[3] < self.get_cur_memory() * 1024 * 1024:
|
||||
raise util.OperationError(f"Storage pool {pool.name()} has less free space than the VM's memory")
|
||||
path = os.path.join(os.path.dirname(disks[0]), f"{self.get_uuid()}.{name}.mem")
|
||||
return f"<memory snapshot='external' file='{util.xml_escape(path)}'/>"
|
||||
return f"<memory snapshot='external' file='{util.xml_escape(path)}'/>", pool.name()
|
||||
|
||||
def _check_snapshot_space(self, disks, memory_pool, free_percent, free_min_gib):
|
||||
"""Each storage pool with a disk of the VM keeps free_percent of the
|
||||
virtual size of those disks free, at least free_min_gib GiB, and the
|
||||
pool of the memory file the VM's memory on top. The
|
||||
overlays grow as the guest writes, and a delete merges them down.
|
||||
This is checked before the snapshot only; nothing is reserved. A
|
||||
disk in no pool is not checked."""
|
||||
needed, pools = {}, {}
|
||||
for path in disks:
|
||||
try:
|
||||
vol = self.get_volume_by_path(path)
|
||||
except libvirtError as err:
|
||||
if err.get_error_code() != VIR_ERR_NO_STORAGE_VOL:
|
||||
raise
|
||||
continue
|
||||
pool = vol.storagePoolLookupByVolume()
|
||||
pools[pool.name()] = pool
|
||||
needed[pool.name()] = needed.get(pool.name(), 0) + vol.info()[1] * free_percent / 100
|
||||
for name in needed:
|
||||
needed[name] = max(needed[name], free_min_gib * 1024**3)
|
||||
if memory_pool:
|
||||
needed[memory_pool] += self.get_cur_memory() * 1024 * 1024
|
||||
for name, size in needed.items():
|
||||
free = pools[name].info()[3]
|
||||
if free < size:
|
||||
raise util.OperationError(
|
||||
f"Storage pool {name} has {free / 1024**3:.1f} GiB free; the snapshot needs {size / 1024**3:.1f} GiB "
|
||||
f"({free_percent:g}% of the VM's disks in it, at least {free_min_gib:g} GiB, "
|
||||
"and the memory if it is saved)"
|
||||
)
|
||||
|
||||
def revert_external_snapshot(self, name):
|
||||
"""libvirt returns the disks and the definition to the snapshot's. A
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue