1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00
webvirtcloud/storages/tests/test_volume_clone_job.py
catborise fa5a8c4d6e fix(storages): the pool page measures its pool fresh, shares one clone dialog and says what it cannot show
- 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.
2026-10-28 19:20:35 +03:00

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)