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