mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +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
|
|
@ -176,3 +176,15 @@ class LiveExternalSnapshotTestCase(SimpleTestCase):
|
|||
with self.assertRaisesRegex(util.OperationError, "guest agent is not connected"):
|
||||
vm.create_external_snapshot("q", quiesce=True)
|
||||
self.assertEqual(dom.snapshotNum(0), 0)
|
||||
|
||||
def test_a_pool_without_room_for_the_overlays_is_refused(self):
|
||||
# a sparse disk ten times the pool: a tenth of it is more than the pool has free
|
||||
pool = self.conn.storagePoolLookupByName(livetest.POOL)
|
||||
big = livetest.create_volume(self.conn, P + "snap-big", size_mib=pool.info()[1] * 10 // 1024**2)
|
||||
dom = livetest.define_vm(self.conn, P + "snap-big", [big])
|
||||
vm = wvmInstance(*self.args, None, uuid=dom.UUIDString())
|
||||
with self.assertRaisesRegex(util.OperationError, f"Storage pool {livetest.POOL} has .* GiB free; the snapshot needs"):
|
||||
vm.create_external_snapshot("x")
|
||||
self.assertEqual(dom.snapshotNum(0), 0)
|
||||
vm.create_external_snapshot("x", free_percent=0, free_min_gib=0)
|
||||
vm.delete_external_snapshot("x")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from django.urls import reverse
|
|||
from lxml import html as lxml_html
|
||||
from vrtManager import util
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
from computes.models import Compute
|
||||
from instances.models import Instance
|
||||
|
||||
|
|
@ -48,11 +49,22 @@ class SnapshotViewsTestCase(TestCase):
|
|||
self.assertEqual(response.status_code, 302)
|
||||
return proxy, method, [str(m) for m in get_messages(response.wsgi_request)]
|
||||
|
||||
def test_create_passes_name_description_memory_and_quiesce(self):
|
||||
def test_create_passes_name_description_memory_quiesce_and_free_space(self):
|
||||
free = {"free_percent": 10.0, "free_min_gib": 1.0} # the Settings page defaults
|
||||
_, method, _ = self.post("create_external_snapshot", {"name": "a", "description": "d", "memory": "on"})
|
||||
method.assert_called_once_with("a", "d", True, False)
|
||||
method.assert_called_once_with("a", "d", True, False, **free)
|
||||
_, method, _ = self.post("create_external_snapshot", {"name": "a", "description": "", "quiesce": "on"})
|
||||
method.assert_called_once_with("a", "", False, True)
|
||||
method.assert_called_once_with("a", "", False, True, **free)
|
||||
|
||||
def test_the_free_space_comes_from_the_settings_page(self):
|
||||
for percent, minimum, expected in (("20", "0.5", {"free_percent": 20.0, "free_min_gib": 0.5}),
|
||||
("0", "0", {"free_percent": 0.0, "free_min_gib": 0.0}),
|
||||
("-1", "x", {}), ("nan", "inf", {})): # invalid: the backend default
|
||||
with self.subTest(percent=percent, minimum=minimum):
|
||||
AppSettings.objects.filter(key="SNAPSHOT_FREE_SPACE_PERCENT").update(value=percent)
|
||||
AppSettings.objects.filter(key="SNAPSHOT_FREE_SPACE_MIN_GIB").update(value=minimum)
|
||||
_, method, _ = self.post("create_external_snapshot", {"name": "a"})
|
||||
method.assert_called_once_with("a", "", False, False, **expected)
|
||||
|
||||
def test_revert_and_delete_call_the_backend_once_and_leave_power_alone(self):
|
||||
for view in ("revert_external_snapshot", "delete_external_snapshot"):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ except ImportError:
|
|||
import crypt
|
||||
import functools
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
|
|
@ -1391,6 +1392,20 @@ def _warn_left_behind(request, files):
|
|||
)
|
||||
|
||||
|
||||
def _snapshot_free_space():
|
||||
"""The free space each storage pool keeps for a snapshot, from the
|
||||
Settings page; an invalid value leaves the backend's default."""
|
||||
values = {}
|
||||
for key, argument in (("SNAPSHOT_FREE_SPACE_PERCENT", "free_percent"), ("SNAPSHOT_FREE_SPACE_MIN_GIB", "free_min_gib")):
|
||||
try:
|
||||
value = float(getattr(app_settings, key))
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
continue
|
||||
if math.isfinite(value) and value >= 0:
|
||||
values[argument] = value
|
||||
return values
|
||||
|
||||
|
||||
@require_POST
|
||||
@serialize_instance_mutation
|
||||
def create_external_snapshot(request, pk):
|
||||
|
|
@ -1402,7 +1417,7 @@ def create_external_snapshot(request, pk):
|
|||
memory = request.POST.get("memory") == "on"
|
||||
quiesce = request.POST.get("quiesce") == "on"
|
||||
try:
|
||||
instance.proxy.create_external_snapshot(name, desc, memory, quiesce)
|
||||
instance.proxy.create_external_snapshot(name, desc, memory, quiesce, **_snapshot_free_space())
|
||||
except libvirtError as err:
|
||||
messages.error(request, _("Snapshot was not created: %(err)s") % {"err": error_text(request, err)})
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue