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

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.
This commit is contained in:
catborise 2026-10-15 10:11:34 +03:00
parent 8d2e88f4bb
commit 149c14f1bb
7 changed files with 44 additions and 22 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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"<source file='{later}'/><target dev='vdb' bus='virtio'/></disk>",
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):

View file

@ -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")

View file

@ -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())

View file

@ -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

View file

@ -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")