1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00
webvirtcloud/instances/tests/test_snapshot_jobs.py
catborise 1a8edafd14 fix(instances): a snapshot job checks its name again in the worker
The name was checked only when the job was queued; recovery counts any
snapshot of that name as the job's, so one taken outside the panel
meanwhile made a failed job succeed. precheck now refuses the name
before any effect.
2026-11-05 18:58:13 +03:00

123 lines
7 KiB
Python

"""Snapshot create and revert run as jobs: the page queues them, the worker calls libvirt."""
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, RevertSnapshot
from instances.models import Instance
from operations.jobs import Context, get_kind
from operations.models import Claim, Operation, acquire
class SnapshotJobsTestCase(TransactionTestCase):
def setUp(self):
get_settings()
compute = Compute.objects.create(name="snap-c", hostname="192.0.2.40", login="root", password="", type=1)
self.instance = Instance.objects.create(compute=compute, name="snap-vm",
uuid="21212121-3434-5656-7878-909090909090")
self.client.force_login(get_user_model().objects.create_superuser("snap_admin", "a@example.com", "pw"))
patcher = patch("instances.jobs.wvmInstance")
self.proxy = patcher.start().return_value
self.addCleanup(patcher.stop)
self.proxy.instance.snapshotListNames.return_value = []
self.proxy._snapshot.return_value.getXMLDesc.return_value = "<domainsnapshot><domain/></domainsnapshot>"
self.proxy.instance.isActive.return_value = False
self.proxy.instance.hasCurrentSnapshot.return_value = 0
def queue_and_run(self, view, data):
"""Post the form; nothing runs in the request; then run the job as the worker does."""
self.client.post(reverse(f"instances:{view}", args=[self.instance.pk]),
{**data, "submission_key": "key-snap0001"})
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
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()
ctx = Context(op, SimpleNamespace(identity="wvc-test:1:a"))
kind = get_kind(op.kind)
kind.precheck(op, ctx)
return op, kind.run(op, ctx)
def test_an_external_snapshot_is_taken_by_the_worker(self):
op, _ = self.queue_and_run("create_external_snapshot", {"name": "a", "description": "d", "memory": "on"})
self.assertEqual(op.kind, "snapshot.create")
self.assertEqual(self.proxy.create_external_snapshot.call_args.args, ("a", "d", True, False))
def test_an_internal_snapshot_is_taken_by_the_worker(self):
self.queue_and_run("snapshot", {"name": "b"})
self.proxy.create_snapshot.assert_called_once_with("b", "")
def test_a_taken_name_is_refused_on_the_page(self):
self.proxy.instance.snapshotListNames.return_value = ["a"]
self.client.post(reverse("instances:snapshot", args=[self.instance.pk]),
{"name": "a", "submission_key": "key-snap0001"})
self.assertFalse(Operation.objects.exists())
def test_a_name_that_is_empty_or_not_a_snapshot_name_is_refused_on_the_page(self):
for name in ("", ".hidden", "a b"):
response = self.client.post(reverse("instances:snapshot", args=[self.instance.pk]),
{"name": name, "submission_key": "key-snap0001"}, follow=True)
self.assertContains(response, "Snapshot names are 1-64 letters")
self.assertFalse(Operation.objects.exists())
def test_a_name_taken_outside_the_panel_meanwhile_is_refused_before_effects(self):
self.client.post(reverse("instances:snapshot", args=[self.instance.pk]),
{"name": "b", "submission_key": "key-snap0001"})
op = Operation.objects.get()
self.proxy.instance.snapshotListNames.return_value = ["b"] # virsh snapshot-create-as
with self.assertRaisesMessage(OperationError, "A snapshot named b already exists"):
CreateSnapshot().precheck(op, None)
self.proxy.create_snapshot.assert_not_called()
def test_create_recovery(self):
op = Operation(params={"instance": self.instance.pk, "name": "a", "external": True},
intent={"compute": self.instance.compute_id})
self.assertEqual(CreateSnapshot().reconcile(op).state, Operation.FAILED)
self.proxy.instance.snapshotListNames.return_value = ["a"]
self.assertEqual(CreateSnapshot().reconcile(op).state, Operation.SUCCEEDED)
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_of_a_running_vm_is_refused_before_effects(self):
# the page disables the button, but it may be older than the VM's start
self.proxy.check_internal_revert.side_effect = OperationError("Shut the VM down to revert to an internal snapshot")
with self.assertRaises(OperationError):
self.queue_and_run("revert_snapshot", {"name": "b"})
self.assertFalse(Operation.objects.get().effects_started)
self.proxy.snapshot_revert.assert_not_called()
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)