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.
62 lines
2.5 KiB
Python
62 lines
2.5 KiB
Python
import threading
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
from accounts.models import UserInstance
|
|
from computes.models import Compute
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import connection
|
|
from django.test import Client, TransactionTestCase, override_settings
|
|
from django.urls import reverse
|
|
from instances.models import Instance
|
|
|
|
QUOTA_CPUS = 4
|
|
|
|
|
|
# Sessions and messages in cookies: concurrent writes to the in-memory test
|
|
# database would fail for reasons unrelated to the quota race.
|
|
@override_settings(
|
|
SESSION_ENGINE="django.contrib.sessions.backends.signed_cookies",
|
|
MESSAGE_STORAGE="django.contrib.messages.storage.cookie.CookieStorage",
|
|
)
|
|
class QuotaRaceTestCase(TransactionTestCase):
|
|
"""Two concurrent resizes of one user's VMs must not both pass the quota."""
|
|
|
|
def setUp(self):
|
|
self.user = get_user_model().objects.create_user("quota_user", password="pw")
|
|
compute = Compute.objects.create(name="quota-compute", hostname="127.0.0.1", login="", password="", type=4)
|
|
self.vms = [
|
|
Instance.objects.create(compute=compute, name=f"quota-vm-{i}", uuid=f"1111111{i}-2222-3333-4444-555555555555")
|
|
for i in range(2)
|
|
]
|
|
for vm in self.vms:
|
|
UserInstance.objects.create(user=self.user, instance=vm, is_change=True)
|
|
self.allocated = 2 # two VMs with one vCPU each
|
|
|
|
def fake_quota(self, user, instance, cpu, memory, disk):
|
|
seen = self.allocated
|
|
time.sleep(0.3) # widen the window between the check and the change
|
|
return "cpu" if seen + cpu > QUOTA_CPUS else ""
|
|
|
|
def fake_resize(self, cur_vcpu, vcpu):
|
|
self.allocated += int(vcpu) - 1
|
|
|
|
def resize(self, vm):
|
|
client = Client()
|
|
client.force_login(self.user)
|
|
client.post(reverse("instances:resizevm_cpu", args=[vm.id]), {"vcpu": 3, "cur_vcpu": 3})
|
|
connection.close()
|
|
|
|
def test_second_concurrent_resize_is_refused(self):
|
|
with patch("instances.views.utils.check_user_quota", self.fake_quota), patch(
|
|
"instances.models.wvmInstance"
|
|
) as wvm:
|
|
wvm.return_value.get_vcpu.return_value = 1
|
|
wvm.return_value.resize_cpu.side_effect = self.fake_resize
|
|
threads = [threading.Thread(target=self.resize, args=(vm,)) for vm in self.vms]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
self.assertEqual(wvm.return_value.resize_cpu.call_count, 1)
|
|
self.assertLessEqual(self.allocated, QUOTA_CPUS)
|