1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00

feat(instances): snapshot revert runs as a job

This commit is contained in:
catborise 2026-10-15 09:59:29 +03:00
parent 6f81043438
commit 8d2e88f4bb
7 changed files with 102 additions and 82 deletions

View file

@ -3,13 +3,14 @@
from types import SimpleNamespace
from unittest.mock import patch
from vrtManager.util import OperationError
from django.contrib.auth import get_user_model
from django.test import TransactionTestCase
from django.urls import reverse
from appsettings.settings import get_settings
from computes.models import Compute
from instances.jobs import CreateSnapshot
from instances.jobs import CreateSnapshot, RevertSnapshot
from instances.models import Instance
from operations.jobs import Context, get_kind
from operations.models import Claim, Operation, acquire
@ -36,7 +37,8 @@ class SnapshotJobsTestCase(TransactionTestCase):
op = Operation.objects.get()
self.assertIn((f"instance:{self.instance.uuid}", "exclusive"),
set(Claim.objects.values_list("resource", "mode")))
self.assertFalse(self.proxy.create_snapshot.called or self.proxy.create_external_snapshot.called)
self.assertFalse(self.proxy.create_snapshot.called or self.proxy.create_external_snapshot.called
or self.proxy.snapshot_revert.called or self.proxy.revert_external_snapshot.called)
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()
@ -69,3 +71,28 @@ class SnapshotJobsTestCase(TransactionTestCase):
self.proxy.instance.isActive.return_value = True
self.proxy.instance.jobInfo.return_value = [2] + [0] * 11 # VIR_DOMAIN_JOB_UNBOUNDED
self.assertEqual(CreateSnapshot().reconcile(op).state, Operation.RUNNING)
def test_an_external_revert_is_made_by_the_worker(self):
self.proxy.revert_external_snapshot.return_value = ["/pool/a.one"]
op, result = self.queue_and_run("revert_external_snapshot", {"name": "a"})
self.assertEqual((op.kind, result), ("snapshot.revert", {"left_behind": ["/pool/a.one"]}))
self.proxy.revert_external_snapshot.assert_called_once_with("a")
def test_a_revert_libvirt_cannot_do_is_refused_before_effects(self):
self.proxy.check_external_revert.side_effect = OperationError("taken by an older version")
with self.assertRaises(OperationError):
self.queue_and_run("revert_external_snapshot", {"name": "a"})
self.assertFalse(Operation.objects.get().effects_started)
def test_an_internal_revert_is_made_by_the_worker(self):
self.queue_and_run("revert_snapshot", {"name": "b"})
self.proxy.snapshot_revert.assert_called_once_with("b")
def test_revert_recovery(self):
op = Operation(params={"instance": self.instance.pk, "name": "a", "external": False},
intent={"compute": self.instance.compute_id, "was_current": "z"})
self.proxy.instance.hasCurrentSnapshot.return_value = 1
self.proxy.instance.snapshotCurrent.return_value.getName.return_value = "a"
self.assertEqual(RevertSnapshot().reconcile(op).state, Operation.SUCCEEDED)
op.intent["was_current"] = "a" # it was current before: nothing tells
self.assertEqual(RevertSnapshot().reconcile(op).state, Operation.UNCERTAIN)

View file

@ -30,15 +30,12 @@ class SnapshotViewsTestCase(TestCase):
admin = get_user_model().objects.create_superuser("snap_admin", "s@example.com", "pw")
self.client.force_login(admin)
def post(self, view, data, error=None, left=()):
def post(self, view, data, error=None):
with patch("instances.models.wvmInstance") as mock_wvm:
proxy = mock_wvm.return_value
method = {
"revert_external_snapshot": proxy.revert_external_snapshot,
"revert_snapshot": proxy.snapshot_revert,
"delete_snapshot": proxy.snapshot_delete,
}[view]
method.return_value = list(left) # files revert and delete left behind
if error:
method.side_effect = util.OperationError(error)
self.client.cookies.pop("messages", None) # only this request's messages
@ -60,20 +57,6 @@ class SnapshotViewsTestCase(TestCase):
get_settings()
self.assertEqual(_snapshot_free_space(), expected)
def test_revert_calls_the_backend_once_and_leaves_power_alone(self):
proxy, method, _ = self.post("revert_external_snapshot", {"name": "a"})
method.assert_called_once_with("a")
for power in ("start", "force_shutdown", "shutdown"):
getattr(proxy, power).assert_not_called()
def test_files_left_behind_are_named_in_a_warning(self):
_, _, messages = self.post("revert_external_snapshot", {"name": "a"}, left=["/pool/a.one", "/pool/vm.one.mem"])
warning = [m for m in messages if "/pool/a.one, /pool/vm.one.mem" in m]
self.assertEqual(len(warning), 1, messages)
self.assertIn("qemu user", warning[0])
_, _, messages = self.post("revert_external_snapshot", {"name": "a"})
self.assertFalse([m for m in messages if "not removed" in m], messages)
def delete(self, listed):
with patch("instances.jobs.wvmInstance") as mock_wvm:
proxy = mock_wvm.return_value
@ -101,7 +84,7 @@ class SnapshotViewsTestCase(TestCase):
self.assertFalse(Operation.objects.exists())
def test_refusals_become_messages(self):
for view in ("revert_external_snapshot", "revert_snapshot", "delete_snapshot"):
for view in ("delete_snapshot",):
with self.subTest(view):
_, _, messages = self.post(view, {"name": "a"}, error="refused for a reason")
self.assertTrue(any("refused for a reason" in m for m in messages), messages)

View file

@ -521,15 +521,6 @@ class InstancesTestCase(TestCase):
self.instance.__dict__.pop(cached, None)
self.assertEqual(len(self.instance.snapshots), 1)
response = self.client.post(
reverse("instances:revert_snapshot", args=[self.instance.id]),
{
"name": "test-snapshot",
},
HTTP_REFERER=reverse("index"),
)
self.assertEqual(response.status_code, 302)
response = self.client.post(
reverse("instances:delete_snapshot", args=[self.instance.id]),
{