1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 19:56:48 +00:00
webvirtcloud/instances/test_quota.py
catborise 01a42079be fix(instances): the page of a VM on an unreachable host points to the list
The VM page reads the VM from libvirt throughout. When its host could
not be reached, the connection error replaced the page with the error
page and showed libvirt's message, which names the host and how it is
reached, to any user who may view the VM.

The page now checks Compute.status first. If the host is not connected,
it goes to the instance list with a message: administrators (superuser
or view_instances) get the host and the error, other users only that
the VM's host cannot be reached right now. The list shows the VM as
unknown.

This covers the page itself; actions on the VM still report libvirt's
error if the host drops. The detail page tests now mock a connected
host.

Checked against the test hosts: a real VM's page opens as before, a VM
on an unreachable compute leads to the list with the user's or the
administrator's message. The new tests fail on the previous code.
2026-10-05 12:32:42 +03:00

179 lines
8.5 KiB
Python

"""check_user_quota counts every VM of the user and refuses an increase it
cannot verify (fail closed); requests that add nothing are not checked."""
from types import SimpleNamespace
from unittest.mock import PropertyMock, patch
from accounts.models import UserAttributes, UserInstance
from appsettings.settings import get_settings
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 instances.utils import QUOTA_UNVERIFIED, check_user_quota
from libvirt import VIR_DOMAIN_RUNNING
from vrtManager.util import OperationError
def vm_reader(vcpu=2, memory=1024, disk_gib=10):
def make(*args, **kwargs):
return SimpleNamespace(
get_vcpu=lambda: vcpu,
get_memory=lambda: memory,
get_disk_devices=lambda: [{"size": disk_gib << 30}],
)
return make
class QuotaTests(TestCase):
def setUp(self):
get_settings() # app settings are normally loaded by middleware
self.user = get_user_model().objects.create_user("quota-user", password="pw")
UserAttributes.objects.create(user=self.user, max_instances=5, max_cpus=8, max_memory=8192, max_disk_size=100)
self.up = Compute.objects.create(name="up", hostname="wvc-alias", login="root", password="", type=2)
self.down = Compute.objects.create(name="down", hostname="192.0.2.1", login="u", password="p", type=1)
self.status_checks = []
def own(self, compute, n):
vm = Instance.objects.create(compute=compute, name=f"vm-{compute.name}-{n}", uuid=f"{n:08d}-0000-0000-0000-{compute.pk:012d}")
UserInstance.objects.create(user=self.user, instance=vm)
return vm
def check(self, *delta, reader=None):
test = self
def status(compute):
test.status_checks.append(id(compute))
return compute.pk != test.down.pk
reader = reader or vm_reader()
with patch.object(Compute, "status", property(status)), patch(
"instances.utils.wvmInstance", side_effect=reader
) as wvm:
result = check_user_quota(self.user, *delta)
return result.split(" ")[0], wvm.call_count # without the QUOTA_DEBUG detail
def test_vms_on_a_host_without_a_tcp_route_are_counted(self):
# an ssh_config alias: the old TCP probe failed and skipped these VMs
for n in range(4):
self.own(self.up, n)
self.assertEqual(self.check(0, 1, 0, 0), ("cpu", 4))
def test_a_host_that_is_down_refuses_an_increase(self):
self.own(self.up, 1)
self.own(self.down, 2)
self.assertEqual(self.check(0, 1, 0, 0)[0], QUOTA_UNVERIFIED)
def test_a_vm_that_cannot_be_read_refuses_an_increase(self):
self.own(self.up, 1)
def gone(*args, **kwargs):
raise OperationError("Domain not found")
self.assertEqual(self.check(0, 0, 512, 0, reader=gone)[0], QUOTA_UNVERIFIED)
def test_each_host_is_checked_once(self):
for n in range(3):
self.own(self.up, n)
self.check(0, 1, 0, 0)
# one Compute object asked (its status is cached), not one per VM
self.assertEqual(len(set(self.status_checks)), 1)
def test_a_user_over_quota_can_still_shrink(self):
for n in range(5):
self.own(self.up, n) # 10 vCPUs used of 8
# (the TCP probe of the previous code would skip these VMs and hide the case)
with patch("instances.utils.connection_manager.host_is_up", return_value=True):
self.assertEqual(self.check(0, -1, 0, 0), ("", 0))
self.assertEqual(self.check(0, 0, -512, 0), ("", 0))
def test_unlimited_users_are_not_checked(self):
UserAttributes.objects.filter(user=self.user).update(max_instances=-1, max_cpus=-1, max_memory=0, max_disk_size=-1)
self.own(self.down, 1)
self.assertEqual(self.check(1, 4, 4096, 50), ("", 0))
def test_only_an_instance_limit_needs_no_host(self):
UserAttributes.objects.filter(user=self.user).update(max_instances=2, max_cpus=-1, max_memory=-1, max_disk_size=-1)
self.own(self.down, 1)
self.assertEqual(self.check(1, 4, 4096, 50), ("", 0))
self.own(self.up, 2)
self.assertEqual(self.check(1, 4, 4096, 50), ("instance", 0))
def test_instance_limit_is_checked_before_any_host(self):
for n in range(5):
self.own(self.down, n)
self.assertEqual(self.check(1, 0, 0, 0), ("instance", 0))
def test_templates_are_not_counted(self):
for n in range(4):
vm = self.own(self.up, n)
Instance.objects.filter(pk=vm.pk).update(is_template=True)
self.assertEqual(self.check(0, 2, 0, 0), ("", 3))
class QuotaViewTests(TestCase):
"""The resize views turn the result into a message and charge only what changes."""
def setUp(self):
get_settings()
self.user = get_user_model().objects.create_user("quota-view-user", password="pw")
self.client.force_login(self.user)
compute = Compute.objects.create(name="qv", hostname="10.0.0.1", login="u", password="p", type=1)
self.vm = Instance.objects.create(compute=compute, name="qv-vm", uuid="12121212-0000-0000-0000-000000000000")
UserInstance.objects.create(user=self.user, instance=self.vm, is_change=True)
def post(self, view, data, quota="", status=VIR_DOMAIN_RUNNING):
with patch("instances.views.utils.check_user_quota", return_value=quota) as check, patch(
"instances.models.wvmInstance"
) as wvm:
wvm.return_value.get_vcpu.return_value = 1
wvm.return_value.get_memory.return_value = 2048
wvm.return_value.get_cur_memory.return_value = 2048
wvm.return_value.get_status.return_value = status
response = self.client.post(reverse(view, args=[self.vm.id]), data)
messages = [str(m) for m in get_messages(response.wsgi_request)]
return messages, check, wvm.return_value
def test_unverified_quota_has_its_own_message(self):
messages, _, proxy = self.post("instances:resizevm_cpu", {"vcpu": 2, "cur_vcpu": 2}, quota=QUOTA_UNVERIFIED)
self.assertEqual(len(messages), 1)
self.assertIn("a host of your virtual machines cannot be reached", messages[0])
self.assertNotIn("quota reached", messages[0])
proxy.resize_cpu.assert_not_called()
def test_running_vm_memory_charges_no_maximum_change(self):
# a running VM keeps its maximum: resize_mem changes the current memory only
_, check, proxy = self.post("instances:resize_memory", {"memory": 8192, "cur_memory": 1024})
self.assertEqual(check.call_args.args[1:], (0, 0, 0, 0))
# the maximum passed is the current one: if the VM shut down meanwhile,
# resize_mem would write it, and it must not be the unchecked 8192
proxy.resize_mem.assert_called_once_with("1024", 2048)
def test_shut_off_vm_memory_charges_the_maximum_change(self):
_, check, _ = self.post("instances:resize_memory", {"memory": 4096, "cur_memory": 1024}, status=5)
self.assertEqual(check.call_args.args[1:], (0, 0, 2048, 0))
class DetailPageQuotaTests(TestCase):
def test_the_detail_page_does_not_read_every_vm_for_the_quota(self):
get_settings()
user = get_user_model().objects.create_user("quota-detail-user", password="pw")
compute = Compute.objects.create(name="qd", hostname="10.0.0.1", login="u", password="p", type=1)
vm = Instance.objects.create(compute=compute, name="qd-vm", uuid="34343434-0000-0000-0000-000000000000")
UserInstance.objects.create(user=user, instance=vm)
self.client.force_login(user)
with patch("instances.models.wvmInstance") as wvm, patch(
"instances.views.utils.check_user_quota", side_effect=AssertionError("quota checked on GET")
), patch.object(Compute, "status", new_callable=PropertyMock, return_value=True):
proxy = wvm.return_value
proxy.get_memory.return_value = 1024
proxy.get_cur_memory.return_value = 1024
proxy.get_max_memory.return_value = 4096
for name in ("get_networks", "get_ifaces", "get_storages", "get_disk_devices", "get_media_devices", "get_net_devices"):
getattr(proxy, name).return_value = []
proxy.get_status.return_value = 5
response = self.client.get(reverse("instances:instance", args=[vm.id]))
self.assertEqual(response.status_code, 200)