mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 04:06:47 +00:00
- A Host devices tab on the VM page lists the VM's USB host devices, each once: how it is chosen (its address, or any device of its model), the device the running VM holds and whether its next start has it; its other host devices (PCI, SCSI, mediated) are listed for the XML editor. - Superusers add a USB device of the host and take it back, on a running VM too. The add form is loaded when the tab opens, as its choices read every VM of the host; a hub, a device the host has its own disks or network interfaces on, or one another VM names is not offered, with the reason, and refused on the server under the compute's claim. A device is chosen by model when the host has one of it, else by its address. - A VM without a USB controller is refused; a device is taken from each definition as that definition describes it. - A clone gets none of the VM's host devices (the clone form says so), and a VM with host devices is not migrated. A snapshot revert is refused when it would give the VM a device another VM names or the host uses. - A NIC that is a host device is no longer edited or removed from the Network tab: the XML editor changes it.
106 lines
6 KiB
Python
106 lines
6 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_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_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)
|