1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 20:26: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:
catborise 2026-10-13 11:47:40 +03:00
parent f766890825
commit 5f8f7a8643
8 changed files with 227 additions and 30 deletions

View file

@ -0,0 +1,35 @@
from django.core.management.color import no_style
from django.db import migrations
SETTINGS = [
("Snapshot Free Percent", "SNAPSHOT_FREE_SPACE_PERCENT", "10",
"Free space a storage pool keeps for a snapshot, in percent of the VM's disks in it (0: none)"),
("Snapshot Free Min GiB", "SNAPSHOT_FREE_SPACE_MIN_GIB", "1",
"Free space a storage pool keeps for a snapshot at least, in GiB (0: none)"),
]
def add_settings(apps, schema_editor):
setting = apps.get_model("appsettings", "AppSettings")
db_alias = schema_editor.connection.alias
# see 0014: move a PostgreSQL id sequence past the fixed ids first
with schema_editor.connection.cursor() as cursor:
for sql in schema_editor.connection.ops.sequence_reset_sql(no_style(), [setting]):
cursor.execute(sql)
for name, key, value, description in SETTINGS:
setting.objects.using(db_alias).get_or_create(
key=key, defaults={"name": name, "value": value, "choices": "", "description": description}
)
def del_settings(apps, schema_editor):
setting = apps.get_model("appsettings", "AppSettings")
db_alias = schema_editor.connection.alias
setting.objects.using(db_alias).filter(key__in=[key for _, key, _, _ in SETTINGS]).delete()
class Migration(migrations.Migration):
dependencies = [("appsettings", "0016_nic_type_rtl8139")]
operations = [migrations.RunPython(add_settings, del_settings)]

View file

@ -40,3 +40,14 @@ class NicTypeTestCase(TestCase):
nic_type.fix_rtl8139(apps, SimpleNamespace(connection=connection))
setting.refresh_from_db()
self.assertEqual((setting.value, setting.choices), ("rtl8139", "default,e1000,e1000e,rtl8139,virtio"))
class SnapshotFreeSpaceSettingsTestCase(TestCase):
def test_the_rows_fit_their_columns(self):
# PostgreSQL refuses a value longer than the column, sqlite does not
for key in ("SNAPSHOT_FREE_SPACE_PERCENT", "SNAPSHOT_FREE_SPACE_MIN_GIB"):
setting = AppSettings.objects.get(key=key)
for field in ("name", "key", "value", "description"):
limit = AppSettings._meta.get_field(field).max_length
if limit:
self.assertLessEqual(len(getattr(setting, field)), limit, f"{key} {field}")