From 149c14f1bba6376dbc8979f18e29b7b6883b255f Mon Sep 17 00:00:00 2001 From: catborise Date: Thu, 15 Oct 2026 10:11:34 +0300 Subject: [PATCH] docs: snapshot create and revert run as jobs Live tests that expected the snapshot or clone result at once now call the backend or run the queued job. --- README.md | 1 + instances/tests/test_create_instance.py | 8 ++++---- instances/tests/test_live_dataloss.py | 23 +++++++++++------------ instances/tests/test_live_guest.py | 11 ++++++++--- instances/tests/test_live_jobs.py | 15 +++++++++++++++ instances/tests/test_snapshot_views.py | 6 +++--- instances/tests/test_views.py | 2 ++ 7 files changed, 44 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 05d0cb00..90ede644 100644 --- a/README.md +++ b/README.md @@ -602,6 +602,7 @@ When the restored version predates the worker, `supervisorctl` reports that `web - Docker no longer publishes port 6080; the console goes through nginx on the panel's port (`WS_PUBLIC_PORT`, default 80). - Long operations run as jobs in a worker process (`manage.py run_worker`); follow them on the VM page and on the Jobs page. - Cloning a VM or a volume and creating a VM run as jobs: the page returns at once and the job panel follows them. The API's VM create answers 202 with the job and needs a new `submission_key` per request. The create form no longer takes a console password; set it on the VM page. +- Taking and reverting a snapshot run as jobs, like deleting an external snapshot: the page returns at once and the job panel follows them. Deleting an internal snapshot stays immediate. - Manual installs that keep their own `settings.py` copy the SQLite `OPTIONS` and `TEST` entries from `settings.py.template` and add `"operations"` to `INSTALLED_APPS`. - The SQLite database must be on a local file system (WAL). - Outside compose, stop the Docker container with `docker stop -t 95` so the worker can finish running jobs. diff --git a/instances/tests/test_create_instance.py b/instances/tests/test_create_instance.py index 07c525f8..35534404 100644 --- a/instances/tests/test_create_instance.py +++ b/instances/tests/test_create_instance.py @@ -8,10 +8,9 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from instances.api.serializers import CreateInstanceSerializer -from instances.jobs import CreateInstance from instances.models import Instance from instances.utils import nic_macs -from operations.jobs import Context +from operations.jobs import Context, Outcome, finish, get_kind from operations.models import Operation, acquire @@ -19,11 +18,12 @@ wvm_create = MagicMock() # the page and the job reach the same host def run(op): - """The queued job, run as the worker runs it.""" + """The queued job, run and finished as the worker does.""" acquire("wvc-test:1:a", "wvc-test") Operation.objects.filter(pk=op.pk).update(state=Operation.RUNNING, worker="wvc-test:1:a") op.refresh_from_db() - CreateInstance().run(op, Context(op, SimpleNamespace(identity="wvc-test:1:a"))) + result = get_kind(op.kind).run(op, Context(op, SimpleNamespace(identity="wvc-test:1:a"))) + finish(op, Outcome(Operation.SUCCEEDED, result=result)) @patch("instances.jobs.wvmCreate", new=wvm_create) diff --git a/instances/tests/test_live_dataloss.py b/instances/tests/test_live_dataloss.py index 33963b40..0079f278 100644 --- a/instances/tests/test_live_dataloss.py +++ b/instances/tests/test_live_dataloss.py @@ -95,7 +95,7 @@ class LiveDataLossTestCase(TestCase): def test_revert_external_snapshot_keeps_disk_added_later(self): base = livetest.create_volume(self.conn, P + "r03-a") dom, inst = self.vm("r03-a", [base]) - self.post("create_external_snapshot", inst, {"name": "snap"}) + inst.proxy.create_external_snapshot("snap") later = livetest.create_volume(self.conn, P + "r03-later") dom.attachDeviceFlags( @@ -103,7 +103,7 @@ class LiveDataLossTestCase(TestCase): f"", libvirt.VIR_DOMAIN_AFFECT_CONFIG, ) - self.post("revert_external_snapshot", inst, {"name": "snap"}) + inst.proxy.revert_external_snapshot("snap") self.assertTrue(livetest.volume_exists(self.conn, later), "revert deleted a disk the snapshot never had") self.assertEqual(set(self.disk_sources(dom, libvirt.VIR_DOMAIN_XML_INACTIVE)), {"vda"}) @@ -111,11 +111,11 @@ class LiveDataLossTestCase(TestCase): def test_delete_older_external_snapshot_keeps_newer_snapshot_files(self): base = livetest.create_volume(self.conn, P + "r03-b") dom, inst = self.vm("r03-b", [base]) - self.post("create_external_snapshot", inst, {"name": "older"}) - self.post("create_external_snapshot", inst, {"name": "newer"}) + inst.proxy.create_external_snapshot("older") + inst.proxy.create_external_snapshot("newer") sources = set(self.disk_sources(dom).values()) - self.post("delete_external_snapshot", inst, {"name": "older"}) + inst.proxy.delete_external_snapshot("older") for path in sources: self.assertTrue(livetest.volume_exists(self.conn, path), f"{path} was deleted") @@ -130,13 +130,12 @@ class LiveDataLossTestCase(TestCase): dom.create() os_before = self.os_xml(dom) - response = self.post("snapshot", inst, {"name": "snap"}) - - # Taken where libvirt supports it, otherwise refused with a message; - # never a server error and never a rewritten loader or NVRAM. - self.assertEqual(response.status_code, 302) - if dom.snapshotNum(0) == 0: - self.assertTrue(list(get_messages(response.wsgi_request))) + # Taken where libvirt supports it, otherwise refused; never a + # rewritten loader or NVRAM. + try: + inst.proxy.create_snapshot("snap") + except libvirt.libvirtError: + self.assertEqual(dom.snapshotNum(0), 0) self.assertEqual(self.os_xml(dom), os_before) def os_xml(self, dom): diff --git a/instances/tests/test_live_guest.py b/instances/tests/test_live_guest.py index 3eb8d038..4a3fdf7c 100644 --- a/instances/tests/test_live_guest.py +++ b/instances/tests/test_live_guest.py @@ -23,7 +23,10 @@ from django.urls import reverse from lxml import etree from vrtManager.connection import connection_manager +from operations.models import Operation + from . import livetest +from .test_create_instance import run from instances.models import Instance from instances.utils import refr @@ -339,7 +342,9 @@ class LiveGuestTestCase(TestCase): source, inst = self.define("gsrc") mac = "52:54:00:aa:06:01" - self.post("clone", inst, {"name": P + "gclone", "disk-vda": P + "gclone.qcow2", "clone-net-mac-0": mac}) + self.post("clone", inst, {"name": P + "gclone", "disk-vda": P + "gclone.qcow2", "clone-net-mac-0": mac, + "submission_key": "key-gclone"}) + run(Operation.objects.get(kind="instance.clone")) clone = self.conn.lookupByName(P + "gclone") self.start(clone) @@ -357,12 +362,12 @@ class LiveGuestTestCase(TestCase): dom, inst = self.boot("gsnap") mac = "52:54:00:aa:07:01" - self.post("snapshot", inst, {"name": "before"}) + inst.proxy.create_snapshot("before") self.assertEqual(dom.snapshotNum(0), 1, "snapshot was not taken") self.post("add_network", inst, {"add-net-mac": mac, "add-net-network": "net:default", "add-net-model": "virtio"}) self.assertTrue(wait_for(lambda: mac in self.guest_view(dom)["macs"], 60), "guest does not see the new NIC") - self.post("revert_snapshot", inst, {"name": "before"}) + inst.proxy.snapshot_revert("before") self.wait_agent(dom) self.assertTrue(dom.isActive(), "reverting a running snapshot left the VM stopped") diff --git a/instances/tests/test_live_jobs.py b/instances/tests/test_live_jobs.py index ac86ae0b..0e133455 100644 --- a/instances/tests/test_live_jobs.py +++ b/instances/tests/test_live_jobs.py @@ -104,6 +104,21 @@ class LiveJobsTestCase(TransactionTestCase): self.assertEqual(op.state, Operation.SUCCEEDED, op.error) return op + def test_snapshot_create_and_revert_run_as_jobs(self): + running, _vm = self.vm("smoke-ext", "running") + stopped, _vm = self.vm("smoke-int") + steps = ((running, "snapshot.create", {"name": "m", "external": True, "memory": True}), + (running, "snapshot.create", {"name": "d", "external": True}), + (running, "snapshot.revert", {"name": "m", "external": True}), + (stopped, "snapshot.create", {"name": "i"}), + (stopped, "snapshot.revert", {"name": "i"})) + for n, (dom, kind, params) in enumerate(steps): + instance = Instance.objects.get_or_create(compute=self.compute, name=dom.name(), uuid=dom.UUIDString())[0] + self.run_job(jobs.submit(kind, self.admin, {"instance": instance.pk, **params}, f"key-snap{n:04d}")) + self.assertEqual(running.snapshotCurrent(0).getName(), "m") + self.assertTrue(running.isActive()) # a memory snapshot resumes the VM + self.assertEqual(stopped.snapshotCurrent(0).getName(), "i") + def test_clone_create_and_volume_clone_run_as_jobs(self): dom, _vm = self.vm("smoke-src") source = Instance.objects.create(compute=self.compute, name=dom.name(), uuid=dom.UUIDString()) diff --git a/instances/tests/test_snapshot_views.py b/instances/tests/test_snapshot_views.py index bdb33fc4..e899b08e 100644 --- a/instances/tests/test_snapshot_views.py +++ b/instances/tests/test_snapshot_views.py @@ -1,6 +1,6 @@ -"""The snapshot views call the backend once (an external snapshot delete -queues a job instead) and show its refusals as messages; they no longer -start, pause or stop the VM themselves.""" +"""Deleting an internal snapshot calls the backend once and shows its +refusals as messages; it no longer starts, pauses or stops the VM itself. The +other snapshot actions queue jobs (test_snapshot_jobs).""" from datetime import datetime from unittest.mock import PropertyMock, patch diff --git a/instances/tests/test_views.py b/instances/tests/test_views.py index c5f49931..f2ba1efd 100644 --- a/instances/tests/test_views.py +++ b/instances/tests/test_views.py @@ -662,10 +662,12 @@ class InstancesTestCase(TestCase): "clone-title": "", "clone-description": "", "clone": "", + "submission_key": "key-clone", }, HTTP_REFERER=reverse("index"), ) self.assertEqual(response.status_code, 302) + run(Operation.objects.get(kind="instance.clone")) self.assertEqual(Instance.objects.count(), instance_count + 1) clone_qs = Instance.objects.filter(name="wvc-test-vm-clone")