1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/computes/test_operation_locks.py
catborise 51dedb7b8f fix(instances): disk edits keep their XML; changes lock per VM
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.
2026-10-04 01:08:11 +03:00

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