1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/tests/test_destroy.py
catborise 8d7f28482f fix(api): VM API answers as the VM pages do
- A VM delete reads delete_disk and delete_nvram as booleans from a JSON
  or form body. The view read them as HTML checkboxes from the form body
  only: JSON {"delete_disk": true} kept the disks while reporting success,
  and a form "delete_disk=false" deleted them. Refused options stop
  nothing; the API schema names both options.
- Power on and power cycle of a template answer 409 instead of reporting a
  start that did not happen.
- The VM details leave out the host's ISO images for non-superusers, as
  the VM page does; reading them also refreshed every pool of the host.
- Flavors take values of at least 1, as the web form does, and show their
  id for later changes; a migration needs neither live nor offline; a VM
  create on an unknown compute answers 404 instead of 500.
2026-10-26 17:47:18 +03:00

122 lines
6.2 KiB
Python

from contextlib import contextmanager
from unittest.mock import patch
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from libvirt import VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_NVRAM, libvirtError
from vrtManager.util import OperationError
class DestroyOrderTestCase(TestCase):
def setUp(self):
admin = get_user_model().objects.create_superuser("destroy_admin", "d@example.com", "pw")
self.client.force_login(admin)
self.client.raise_request_exception = False
compute = Compute.objects.create(name="destroy-compute", hostname="127.0.0.1", login="", password="", type=4)
self.instance = Instance.objects.create(
compute=compute, name="destroy-vm", uuid="11111111-2222-3333-4444-555555555555"
)
def destroy(self, undefine_error=None, data=None):
with patch("instances.models.wvmInstance") as wvm:
proxy = wvm.return_value
proxy.instance.isActive.return_value = False
proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], ["/pool/shared.qcow2"])
proxy.delete.side_effect = undefine_error
self.client.post(reverse("instances:destroy", args=[self.instance.id]), data or {"delete_disk": "1"})
return proxy
def test_the_nvram_goes_only_when_asked(self):
for data, wanted, unwanted in (
({"delete_nvram": "1"}, VIR_DOMAIN_UNDEFINE_NVRAM, VIR_DOMAIN_UNDEFINE_KEEP_NVRAM),
({}, VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_NVRAM),
):
with self.subTest(data=data):
Instance.objects.get_or_create(
compute=self.instance.compute, name="destroy-vm", uuid="11111111-2222-3333-4444-555555555555"
)
self.instance = Instance.objects.get(uuid="11111111-2222-3333-4444-555555555555")
flags = self.destroy(data=data).delete.call_args[0][0]
self.assertTrue(flags & wanted)
self.assertFalse(flags & unwanted)
def test_the_api_takes_its_options_as_booleans(self):
url = f"/api/v1/computes/{self.instance.compute_id}/instances/{self.instance.id}/"
for body, content_type, deleted in (
({"delete_disk": True}, "application/json", True),
("delete_disk=false", "application/x-www-form-urlencoded", False),
):
with self.subTest(body=body):
Instance.objects.get_or_create(
compute=self.instance.compute, name="destroy-vm", uuid="11111111-2222-3333-4444-555555555555"
)
self.instance = Instance.objects.get(uuid="11111111-2222-3333-4444-555555555555")
with patch("instances.models.wvmInstance") as wvm:
proxy = wvm.return_value
proxy.instance.isActive.return_value = False
proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], [])
url = f"/api/v1/computes/{self.instance.compute_id}/instances/{self.instance.id}/"
response = self.client.delete(url, body, content_type=content_type)
self.assertEqual(response.status_code, 200)
self.assertEqual(proxy.get_volume_by_path.called, deleted)
def test_disks_are_deleted_after_the_undefine(self):
proxy = self.destroy()
proxy.get_volume_by_path.assert_called_once_with("/pool/own.qcow2")
self.assertFalse(Instance.objects.filter(pk=self.instance.pk).exists())
def test_failed_undefine_deletes_no_disk(self):
proxy = self.destroy(undefine_error=libvirtError("refused"))
proxy.get_volume_by_path.assert_not_called()
self.assertTrue(Instance.objects.filter(pk=self.instance.pk).exists())
def test_destroy_and_delete_vol_hold_the_compute_exclusively(self):
calls = []
@contextmanager
def record(compute, *, shared=False):
calls.append(shared)
yield
with patch("instances.views.libvirt_compute_lock", record):
with patch("instances.models.wvmInstance"):
self.client.post(reverse("instances:delete_vol", args=[self.instance.id]), {"dev": "vda"})
self.destroy()
self.assertEqual(calls, [False, False])
def test_a_volume_the_vms_snapshots_need_is_kept_after_the_detach(self):
disk = {"dev": "vdb", "path": "/pool/data.qcow2", "storage": "default", "image": "data.qcow2"}
with patch("instances.models.wvmInstance") as wvm, patch("instances.views.wvmStorage") as storage:
proxy = wvm.return_value
proxy.get_disk_devices.return_value = [disk]
proxy.paths_used_by_other_domains.return_value = set()
proxy.wait_disk_detached.return_value = True
storage.return_value.del_volume.side_effect = OperationError("Volume data.qcow2 is in use")
response = self.client.post(reverse("instances:delete_vol", args=[self.instance.id]), {"dev": "vdb"})
self.assertEqual(response.status_code, 302)
proxy.detach_disk.assert_called_once_with("vdb")
messages = [str(m) for m in get_messages(response.wsgi_request)]
self.assertTrue(any("data.qcow2 is in use" in m and "detached" in m for m in messages), messages)
def test_confirmation_page_takes_no_lock(self):
calls = []
@contextmanager
def record(compute, *, shared=False):
calls.append(shared)
yield
with patch("instances.views.libvirt_compute_lock", record), patch("instances.models.wvmInstance"):
response = self.client.get(reverse("instances:destroy", args=[self.instance.id]))
self.assertEqual(response.status_code, 200)
self.assertEqual(calls, [])
def test_the_confirmation_page_of_a_paused_vm_offers_the_form(self):
with patch("instances.models.wvmInstance") as wvm:
wvm.return_value.get_status.return_value = 3 # paused
response = self.client.get(reverse("instances:destroy", args=[self.instance.id]))
self.assertIn('id="delete_form"', response.content.decode())