mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
Disk edits: - edit_disk built a new <disk> from the form and replaced the old one, losing what the form does not show (e.g. encryption and virtio driver options); a bus change detached and re-attached the disk. The disk of the persistent definition is now edited in place and the domain defined once; a bus change picks a free device name and drops the old address and virtio-only options. Pending persistent changes of a running VM are kept. - Clone omits empty disk devices, refuses a disk without a supported source and destination, and checks the name before allocating storage. Locks: - Every VM change held its compute's exclusive lock, so one slow operation blocked all VMs on that compute. A VM change now holds its own VM lock plus a shared compute lock; reconciliation, migration and cloning hold the compute exclusively (two clones of different VMs must not pass the name check together). - Quota-checked changes of one user (CPU, memory and disk resize, clone) run one at a time under a per-user lock, also across computes, so concurrent requests cannot all pass the check against the old allocation. It is the outermost lock; superusers, who have no quota, skip it. - A busy lock answers 409 to the API and a message in the web UI. API power and destroy actions return the view's error instead of success.
60 lines
2.2 KiB
Python
60 lines
2.2 KiB
Python
import threading
|
|
from types import SimpleNamespace
|
|
|
|
from django.test import SimpleTestCase
|
|
|
|
from computes.utils import libvirt_compute_lock, libvirt_instance_lock
|
|
|
|
|
|
class OperationLocksTestCase(SimpleTestCase):
|
|
def setUp(self):
|
|
self.compute = SimpleNamespace(pk=987654321)
|
|
self.first = SimpleNamespace(pk=987654321, compute=self.compute)
|
|
self.second = SimpleNamespace(pk=987654322, compute=self.compute)
|
|
|
|
def test_sibling_vm_can_run_while_same_vm_and_reconcile_are_excluded(self):
|
|
acquired = threading.Event()
|
|
release = threading.Event()
|
|
errors = []
|
|
|
|
def hold_first_vm():
|
|
try:
|
|
with libvirt_instance_lock(self.first):
|
|
acquired.set()
|
|
if not release.wait(5):
|
|
raise TimeoutError("Test did not release the VM lock")
|
|
except Exception as err:
|
|
errors.append(err)
|
|
|
|
worker = threading.Thread(target=hold_first_vm)
|
|
worker.start()
|
|
try:
|
|
self.assertTrue(acquired.wait(2))
|
|
with libvirt_instance_lock(self.second, timeout=0.1):
|
|
pass
|
|
with self.assertRaises(TimeoutError):
|
|
with libvirt_instance_lock(self.first, timeout=0.05):
|
|
pass
|
|
with self.assertRaises(TimeoutError):
|
|
with libvirt_compute_lock(self.compute, timeout=0.05):
|
|
pass
|
|
finally:
|
|
release.set()
|
|
worker.join(5)
|
|
self.assertFalse(worker.is_alive())
|
|
self.assertEqual(errors, [])
|
|
with libvirt_compute_lock(self.compute, timeout=0.1):
|
|
pass
|
|
|
|
def test_instance_lock_is_reentrant_without_releasing_the_outer_lock(self):
|
|
with libvirt_instance_lock(self.first):
|
|
with libvirt_instance_lock(self.first):
|
|
pass
|
|
with self.assertRaisesRegex(RuntimeError, "Cannot upgrade"):
|
|
with libvirt_compute_lock(self.compute, timeout=0.05):
|
|
pass
|
|
|
|
def test_compute_exclusive_owner_can_take_an_instance_lock(self):
|
|
with libvirt_compute_lock(self.compute):
|
|
with libvirt_instance_lock(self.first):
|
|
pass
|