mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
- The page lists the volumes first: that refreshes the pool, and libvirt measures a pool's space only when it is refreshed, so the size and usage shown were as of the previous refresh. - A space that cannot be read says so instead of 0 bytes, and the pool's status, read from the same pool info, no longer fails the page after it; a stopped pool says its space and volumes are unavailable until it is started, instead of 0 bytes and nothing. - The API's pool is measured after the same refresh, and a stopped pool lists no volumes there instead of failing. - A clone the job refuses (a name in use, a volume gone) is a message on the page, not a server error. - An ISO image is recognised by its name (storages.jobs.is_iso): libvirt calls its format raw, so the template's "iso" check never disabled its Clone button. The clone job refuses one too; the upload uses the same rule. - One clone dialog serves every volume, filled and reset by the button that opens it, instead of one dialog per volume in the page; its fields have labels and its title names the dialog. - Volume rows are searched by name only (data-search), with a message when none matches, in a live region for screen readers; names show in full. The search script loads only with the volume table: on a stopped pool's page it failed for want of a search box. An empty pool says "This pool has no volumes". - The unused wvmStorage.get_volumes_details goes: update_volumes is the same.
74 lines
4 KiB
Python
74 lines
4 KiB
Python
"""Volume clone runs as a job: the page queues it, the worker copies."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TransactionTestCase
|
|
from django.urls import reverse
|
|
|
|
from computes.models import Compute
|
|
from operations.jobs import Context
|
|
from operations.models import Claim, Operation, acquire
|
|
from storages.jobs import CloneVolume
|
|
|
|
|
|
class VolumeCloneJobTestCase(TransactionTestCase):
|
|
def setUp(self):
|
|
self.compute = Compute.objects.create(name="vol-c", hostname="192.0.2.20", login="root", password="", type=1)
|
|
admin = get_user_model().objects.create_superuser("vol_admin", "a@example.com", "pw")
|
|
self.client.force_login(admin)
|
|
patcher = patch("storages.jobs.wvmStorage")
|
|
self.stg = patcher.start().return_value
|
|
self.addCleanup(patcher.stop)
|
|
self.stg.get_volumes.return_value = ["base.qcow2"]
|
|
self.stg.get_volume_format_type.return_value = "qcow2"
|
|
self.stg.clone_name.return_value = "copy.qcow2"
|
|
self.stg.clone_volume.return_value = "copy.qcow2"
|
|
|
|
@patch("storages.views.wvmStorage")
|
|
def test_the_page_queues_the_copy_and_the_worker_makes_it(self, page):
|
|
page.return_value.is_active.return_value = False
|
|
page.return_value.get_size.return_value = (1, 1)
|
|
url = reverse("storage", args=[self.compute.pk, "pool-a"])
|
|
response = self.client.post(url, {"cln_volume": "1", "image": "base.qcow2", "name": "copy",
|
|
"submission_key": "key-vol00001"})
|
|
self.assertRedirects(response, url, fetch_redirect_response=False)
|
|
op = Operation.objects.get()
|
|
self.assertEqual((op.kind, op.intent["target"]), ("volume.clone", "copy.qcow2"))
|
|
self.assertIn((f"volume:{self.compute.pk}:pool-a/copy.qcow2", "exclusive"),
|
|
set(Claim.objects.values_list("resource", "mode")))
|
|
self.stg.clone_volume.assert_not_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()
|
|
CloneVolume().run(op, Context(op, SimpleNamespace(identity="wvc-test:1:a")))
|
|
op.refresh_from_db()
|
|
self.stg.clone_volume.assert_called_once_with("base.qcow2", "copy", None, False)
|
|
self.assertEqual(op.intent["created"], "copy.qcow2")
|
|
self.assertIn(f"key=pool%3A{self.compute.pk}%3Apool-a", self.client.get(url).content.decode())
|
|
|
|
@patch("storages.views.wvmStorage")
|
|
def test_a_refused_copy_is_a_message(self, page):
|
|
# a name in use, an ISO image: the job's plan refuses, the page says why
|
|
page.return_value.is_active.return_value = False
|
|
page.return_value.get_size.return_value = (1, 1)
|
|
url = reverse("storage", args=[self.compute.pk, "pool-a"])
|
|
self.stg.get_volumes.return_value = ["base.qcow2", "copy.qcow2", "install.ISO"]
|
|
for key, image, why in (("key-vol00002", "base.qcow2", "Name of volume already in use"),
|
|
("key-vol00003", "install.ISO", "An ISO image is not cloned")):
|
|
with self.subTest(image=image):
|
|
response = self.client.post(url, {"cln_volume": "1", "image": image, "name": "copy",
|
|
"submission_key": key}, follow=True)
|
|
self.assertContains(response, why)
|
|
self.assertFalse(Operation.objects.exists())
|
|
|
|
def test_recovery(self):
|
|
self.stg.get_volumes.return_value = ["base.qcow2", "copy.qcow2"]
|
|
op = Operation(params={"compute": self.compute.pk, "pool": "pool-a"}, intent={"target": "copy.qcow2"})
|
|
self.assertEqual(CloneVolume().reconcile(op).state, Operation.UNCERTAIN) # there, not proven complete
|
|
self.stg.get_volumes.return_value = []
|
|
self.assertEqual(CloneVolume().reconcile(op).state, Operation.FAILED)
|
|
op.intent["created"] = "copy.qcow2"
|
|
self.assertEqual(CloneVolume().reconcile(op).state, Operation.SUCCEEDED)
|