diff --git a/instances/test_template_flag.py b/instances/test_template_flag.py index 8d9b3e62..82bc2200 100644 --- a/instances/test_template_flag.py +++ b/instances/test_template_flag.py @@ -38,7 +38,11 @@ class TemplateFlagTestCase(TestCase): def _change_options(self, user, data): self.client.force_login(user) - with patch("instances.models.wvmInstance"): + with patch("instances.models.wvmInstance") as wvm: + # unmarking a template checks the owner's quota with the VM's size + wvm.return_value.get_vcpu.return_value = 1 + wvm.return_value.get_memory.return_value = 1024 + wvm.return_value.get_disk_devices.return_value = [] res = self.client.post( reverse("instances:change_options", args=[self.instance.id]), data ) diff --git a/instances/test_template_quota.py b/instances/test_template_quota.py new file mode 100644 index 00000000..0472e3be --- /dev/null +++ b/instances/test_template_quota.py @@ -0,0 +1,68 @@ +"""Templates do not count towards the quota and cannot be started. Turning a +template back into a VM adds it to its owner's usage, so for users with a +quota it is checked like any other increase.""" + +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.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 + + +class TemplateFlagQuotaTests(TestCase): + def setUp(self): + User = get_user_model() + self.staff = User.objects.create_user("tq-staff", password="pw", is_staff=True) + compute = Compute.objects.create(name="tq", hostname="10.0.0.5", login="u", password="p", type=1) + self.vm = Instance.objects.create(compute=compute, name="tq-vm", uuid="12345678-0000-0000-0000-00000000aaaa") + UserInstance.objects.create(user=self.staff, instance=self.vm, is_change=True) + + def post(self, user, is_template, quota=""): + self.client.force_login(user) + data = {"title": "", "description": ""} + if is_template: + data["is_template"] = "on" + with patch("instances.models.wvmInstance") as wvm, patch( + "instances.views.utils.check_user_quota", return_value=quota + ) as check: + wvm.return_value.get_vcpu.return_value = 2 + wvm.return_value.get_memory.return_value = 2048 + wvm.return_value.get_disk_devices.return_value = [{"size": 10 << 30}] + response = self.client.post(reverse("instances:change_options", args=[self.vm.id]), data) + self.vm.refresh_from_db() + return check, [str(m) for m in get_messages(response.wsgi_request)] + + def test_making_a_template_needs_no_quota(self): + check, _ = self.post(self.staff, is_template=True) + self.assertTrue(self.vm.is_template) + check.assert_not_called() + + def test_turning_a_template_into_a_vm_is_checked(self): + Instance.objects.filter(pk=self.vm.pk).update(is_template=True) + check, _ = self.post(self.staff, is_template=False) + self.assertFalse(self.vm.is_template) + check.assert_called_once_with(self.staff, 1, 2, 2048, 10) + + def test_over_quota_the_template_stays_a_template(self): + Instance.objects.filter(pk=self.vm.pk).update(is_template=True) + _, messages = self.post(self.staff, is_template=False, quota="cpu") + self.assertTrue(self.vm.is_template) + self.assertTrue(any("quota" in m for m in messages), messages) + + def test_an_unverifiable_quota_keeps_it_a_template(self): + Instance.objects.filter(pk=self.vm.pk).update(is_template=True) + _, messages = self.post(self.staff, is_template=False, quota=QUOTA_UNVERIFIED) + self.assertTrue(self.vm.is_template) + self.assertTrue(any("cannot be reached" in m for m in messages), messages) + + def test_superusers_have_no_quota(self): + Instance.objects.filter(pk=self.vm.pk).update(is_template=True) + admin = get_user_model().objects.create_superuser("tq-admin", "tq@example.com", "pw") + check, _ = self.post(admin, is_template=False, quota="cpu") + self.assertFalse(self.vm.is_template) + check.assert_not_called() diff --git a/instances/views.py b/instances/views.py index 118a94be..f441386a 100755 --- a/instances/views.py +++ b/instances/views.py @@ -1957,6 +1957,7 @@ def update_console(request, pk): @require_POST +@serialize_user_quota @serialize_instance_mutation def change_options(request, pk): instance = get_instance(request.user, pk, perm_type="change") @@ -1970,7 +1971,18 @@ def change_options(request, pk): # checkbox is disabled for everyone else, and a disabled checkbox is # not submitted, so for them the flag must be left alone. if request.user.is_superuser or request.user.is_staff: - instance.is_template = bool(request.POST.get("is_template", False)) + is_template = bool(request.POST.get("is_template", False)) + # A template is not counted and cannot be started: making it a + # VM again adds it to the owner's usage, so the quota applies. + if instance.is_template and not is_template and not request.user.is_superuser: + disk_sum = sum(int(disk.get("size") or 0) >> 30 for disk in instance.disks) + quota_msg = utils.check_user_quota(request.user, 1, instance.vcpu, instance.memory, disk_sum) + if quota_msg: + messages.error(request, quota_refused(quota_msg, _( + "User %(quota_msg)s quota reached, '%(instance_name)s' stays a template!" + ) % {"quota_msg": quota_msg, "instance_name": instance.name})) + is_template = True + instance.is_template = is_template instance.save(update_fields=["is_template"]) options = {}