mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
feat(locale): quota refusals and connection errors show in the reader's language
A quota refusal put the English name of the quota (cpu, memory, disk, instance) into a translated sentence, so a Turkish page read "memory kotası doldu". check_user_quota now returns which quota and its numbers (QuotaExceeded), each quota has a whole sentence, and the refusals put that sentence before what is refused; QUOTA_DEBUG adds the numbers as before. The clone job stores the refusal as a message, so the Jobs page shows it in its reader's language. The connection errors of vrtManager (did not answer within, still waited for, closed, failed with libvirt's text) were English f-strings; they are messages now, each close reason a whole one. A connection that died without a close callback raised an error reading "None"; it now names the host. Logs keep the English text.
This commit is contained in:
parent
50f861b2aa
commit
4da64c46cd
11 changed files with 698 additions and 567 deletions
|
|
@ -124,7 +124,7 @@ def clone_data(user, instance, fields):
|
|||
"""The clone's settings from the form fields and the source as it is now,
|
||||
and what it adds to the user's quota; OperationError with the reason when
|
||||
it cannot be made."""
|
||||
from instances.views import VOLUME_NAME_RE, quota_refused
|
||||
from instances.views import VOLUME_NAME_RE
|
||||
|
||||
data = dict()
|
||||
data["name"] = fields.get("name", "").strip()
|
||||
|
|
@ -187,10 +187,11 @@ def clone_data(user, instance, fields):
|
|||
if key.startswith("disk-") and value is not None and not VOLUME_NAME_RE.fullmatch(value)
|
||||
]
|
||||
|
||||
if not user.is_superuser and quota_msg == utils.QUOTA_UNVERIFIED:
|
||||
raise util.OperationError(utils.QUOTA_UNVERIFIED_TEXT)
|
||||
if not user.is_superuser and quota_msg:
|
||||
raise util.OperationError(quota_refused(quota_msg, _(
|
||||
"User '%(quota_msg)s' quota reached, cannot create '%(clone_name)s'!"
|
||||
)), quota_msg=quota_msg, clone_name=data["name"])
|
||||
raise util.OperationError(_("%(quota)s: cannot create '%(clone_name)s'."),
|
||||
quota=utils.quota_message(quota_msg), clone_name=data["name"])
|
||||
if check_instance:
|
||||
raise util.OperationError(_("Instance '%(clone_name)s' already exists!"), clone_name=data["name"])
|
||||
if not re.match(r"^[a-zA-Z0-9-]+$", data["name"]):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ 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 instances.utils import QUOTA_UNVERIFIED, QuotaExceeded, check_user_quota, quota_message
|
||||
from vrtManager.util import render
|
||||
from libvirt import VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING
|
||||
from operations.models import Operation
|
||||
from vrtManager.util import OperationError
|
||||
|
|
@ -71,7 +72,7 @@ class QuotaTests(TestCase):
|
|||
"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
|
||||
return getattr(result, "kind", result), wvm.call_count # a QuotaExceeded names the quota
|
||||
|
||||
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
|
||||
|
|
@ -191,12 +192,15 @@ class QuotaTests(TestCase):
|
|||
|
||||
for i in range(5):
|
||||
self.own(self.up, i)
|
||||
for value, expected in (("True", "instance (6 > 5)"), ("False", "instance")):
|
||||
with patch.object(Compute, "status", True), patch("instances.utils.wvmInstance", side_effect=vm_reader()):
|
||||
exceeded = check_user_quota(self.user, 1, 0, 0, 0)
|
||||
self.assertEqual(exceeded, QuotaExceeded("instance", 6, 5))
|
||||
for value, expected in (("True", "The quota of virtual machines is reached (6 > 5)"),
|
||||
("False", "The quota of virtual machines is reached")):
|
||||
with self.subTest(value=value):
|
||||
AppSettings.objects.filter(key="QUOTA_DEBUG").update(value=value)
|
||||
get_settings()
|
||||
with patch.object(Compute, "status", True), patch("instances.utils.wvmInstance", side_effect=vm_reader()):
|
||||
self.assertEqual(check_user_quota(self.user, 1, 0, 0, 0), expected)
|
||||
self.assertEqual(render("%(quota)s", {"quota": quota_message(exceeded)}, str), expected)
|
||||
|
||||
|
||||
class QuotaViewTests(TestCase):
|
||||
|
|
@ -228,7 +232,7 @@ class QuotaViewTests(TestCase):
|
|||
status=5)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertIn("a host of your virtual machines cannot be reached", messages[0])
|
||||
self.assertNotIn("quota reached", messages[0])
|
||||
self.assertNotIn("quota is reached", messages[0])
|
||||
proxy.resize_cpu.assert_not_called()
|
||||
|
||||
def test_running_vm_memory_charges_no_maximum_change(self):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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
|
||||
from instances.utils import QUOTA_UNVERIFIED, QuotaExceeded
|
||||
|
||||
|
||||
class TemplateFlagQuotaTests(TestCase):
|
||||
|
|
@ -49,7 +49,7 @@ class TemplateFlagQuotaTests(TestCase):
|
|||
|
||||
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")
|
||||
_, messages = self.post(self.staff, is_template=False, quota=QuotaExceeded("cpu", 9, 8))
|
||||
self.assertTrue(self.vm.is_template)
|
||||
self.assertTrue(any("quota" in m for m in messages), messages)
|
||||
|
||||
|
|
@ -70,6 +70,6 @@ class TemplateFlagQuotaTests(TestCase):
|
|||
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")
|
||||
check, _ = self.post(admin, is_template=False, quota=QuotaExceeded("cpu", 9, 8))
|
||||
self.assertFalse(self.vm.is_template)
|
||||
check.assert_not_called()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import random
|
||||
import re
|
||||
import string
|
||||
from collections import namedtuple
|
||||
|
||||
from accounts.models import UserInstance, UserAttributes
|
||||
from appsettings.settings import app_settings
|
||||
|
|
@ -9,12 +10,13 @@ from computes.models import Compute
|
|||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_noop
|
||||
from libvirt import libvirtError
|
||||
from logs.audit import reason, write
|
||||
from operations.models import Operation
|
||||
from vrtManager.connection import other_hosts, wvmConnect
|
||||
from vrtManager.instance import wvmInstance, wvmInstances
|
||||
from vrtManager.util import OperationError
|
||||
from vrtManager.util import OperationError, message
|
||||
|
||||
from .models import Instance
|
||||
|
||||
|
|
@ -73,13 +75,34 @@ def disk_gib(disks):
|
|||
|
||||
# check_user_quota could not read every VM of the user: an increase is refused
|
||||
QUOTA_UNVERIFIED = "unverified"
|
||||
QUOTA_UNVERIFIED_TEXT = gettext_noop(
|
||||
"The quota cannot be checked because a host of your virtual machines cannot be reached. Nothing was changed.")
|
||||
|
||||
# The quota a change would exceed: instance, cpu, memory or disk, the total it
|
||||
# would make and the limit
|
||||
QuotaExceeded = namedtuple("QuotaExceeded", "kind total limit")
|
||||
QUOTA_REACHED = {
|
||||
"instance": gettext_noop("The quota of virtual machines is reached"),
|
||||
"cpu": gettext_noop("The vCPU quota is reached"),
|
||||
"memory": gettext_noop("The memory quota is reached"),
|
||||
"disk": gettext_noop("The disk quota is reached"),
|
||||
}
|
||||
|
||||
|
||||
def quota_message(exceeded):
|
||||
"""The quota check's result as messages, with the numbers when QUOTA_DEBUG is on."""
|
||||
reached = message(QUOTA_REACHED[exceeded.kind])
|
||||
if app_settings.QUOTA_DEBUG != "True":
|
||||
return [reached]
|
||||
return [message(gettext_noop("%(quota)s (%(total)s > %(limit)s)"),
|
||||
quota=[reached], total=exceeded.total, limit=exceeded.limit)]
|
||||
|
||||
|
||||
@other_hosts()
|
||||
def check_user_quota(user, instance, cpu, memory, disk_size):
|
||||
"""
|
||||
Which quota the user would exceed by adding these amounts (instances,
|
||||
vCPUs, memory MiB, disk GiB) to their VMs, or "". A quota of -1 has no
|
||||
The quota the user would exceed by adding these amounts (instances,
|
||||
vCPUs, memory MiB, disk GiB) to their VMs, a QuotaExceeded, or "". A quota of -1 has no
|
||||
limit; 0 allows none. QUOTA_UNVERIFIED if a
|
||||
VM that counts cannot be read (its host is down, the VM is gone): the
|
||||
usage is unknown, so an increase is refused.
|
||||
|
|
@ -93,8 +116,6 @@ def check_user_quota(user, instance, cpu, memory, disk_size):
|
|||
if instance <= 0 and cpu <= 0 and memory <= 0 and disk_size <= 0:
|
||||
return msg
|
||||
|
||||
quota_debug = app_settings.QUOTA_DEBUG == "True"
|
||||
|
||||
user_instances = UserInstance.objects.filter(user=user, instance__is_template=False).select_related(
|
||||
"instance__compute"
|
||||
)
|
||||
|
|
@ -106,10 +127,7 @@ def check_user_quota(user, instance, cpu, memory, disk_size):
|
|||
]
|
||||
instance += user_instances.count() + len(clones)
|
||||
if ua.max_instances >= 0 and instance > ua.max_instances:
|
||||
msg = "instance"
|
||||
if quota_debug:
|
||||
msg += f" ({instance} > {ua.max_instances})"
|
||||
return msg
|
||||
return QuotaExceeded("instance", instance, ua.max_instances)
|
||||
if ua.max_cpus < 0 and ua.max_memory < 0 and ua.max_disk_size < 0:
|
||||
return msg
|
||||
|
||||
|
|
@ -141,17 +159,11 @@ def check_user_quota(user, instance, cpu, memory, disk_size):
|
|||
return QUOTA_UNVERIFIED
|
||||
|
||||
if ua.max_cpus >= 0 and cpu > ua.max_cpus:
|
||||
msg = "cpu"
|
||||
if quota_debug:
|
||||
msg += f" ({cpu} > {ua.max_cpus})"
|
||||
msg = QuotaExceeded("cpu", cpu, ua.max_cpus)
|
||||
if ua.max_memory >= 0 and memory > ua.max_memory:
|
||||
msg = "memory"
|
||||
if quota_debug:
|
||||
msg += f" ({memory} > {ua.max_memory})"
|
||||
msg = QuotaExceeded("memory", memory, ua.max_memory)
|
||||
if ua.max_disk_size >= 0 and disk_size > ua.max_disk_size:
|
||||
msg = "disk"
|
||||
if quota_debug:
|
||||
msg += f" ({disk_size} > {ua.max_disk_size})"
|
||||
msg = QuotaExceeded("disk", disk_size, ua.max_disk_size)
|
||||
return msg
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ from django.urls import reverse
|
|||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django.utils.translation import gettext_lazy, gettext_noop
|
||||
from django.views.decorators.http import require_POST
|
||||
from libvirt import (VIR_DOMAIN_PAUSED,
|
||||
VIR_DOMAIN_RUNNING,
|
||||
|
|
@ -657,12 +657,12 @@ def serialize_instance_mutation(func):
|
|||
return wrapper
|
||||
|
||||
|
||||
def quota_refused(quota_msg, refused):
|
||||
"""The message for a refused change: refused is the action's own
|
||||
'User %(quota_msg)s quota reached, ...' text, already formatted."""
|
||||
if quota_msg == utils.QUOTA_UNVERIFIED:
|
||||
return _("The quota cannot be checked because a host of your virtual machines cannot be reached. Nothing was changed.")
|
||||
return refused
|
||||
def quota_refused(quota, text, **values):
|
||||
"""The message for a change the quota refuses: text names the change
|
||||
after %(quota)s, the quota it would exceed."""
|
||||
if quota == utils.QUOTA_UNVERIFIED:
|
||||
return _(utils.QUOTA_UNVERIFIED_TEXT)
|
||||
return util.render(text, {"quota": utils.quota_message(quota), **values}, _)
|
||||
|
||||
|
||||
def serialize_user_quota(func):
|
||||
|
|
@ -1174,12 +1174,8 @@ def resizevm_cpu(request, pk):
|
|||
request.user, 0, int(new_vcpu) - vcpu, 0, 0
|
||||
)
|
||||
if not request.user.is_superuser and quota_msg:
|
||||
msg = quota_refused(quota_msg, _(
|
||||
"User %(quota_msg)s quota reached, cannot resize CPU of '%(instance_name)s'!"
|
||||
) % {
|
||||
"quota_msg": quota_msg,
|
||||
"instance_name": instance.name,
|
||||
})
|
||||
msg = quota_refused(quota_msg, gettext_noop("%(quota)s: cannot resize the CPU of '%(instance_name)s'."),
|
||||
instance_name=instance.name)
|
||||
messages.error(request, msg)
|
||||
else:
|
||||
cur_vcpu = new_cur_vcpu
|
||||
|
|
@ -1212,12 +1208,8 @@ def resize_memory(request, pk):
|
|||
return redirect(reverse("instances:instance", args=[instance.id]) + "#resize")
|
||||
quota_msg = utils.check_user_quota(request.user, 0, 0, int(new_memory) - memory, 0)
|
||||
if not request.user.is_superuser and quota_msg:
|
||||
msg = quota_refused(quota_msg, _(
|
||||
"User %(quota_msg)s quota reached, cannot resize memory of '%(instance_name)s'!"
|
||||
) % {
|
||||
"quota_msg": quota_msg,
|
||||
"instance_name": instance.name,
|
||||
})
|
||||
msg = quota_refused(quota_msg, gettext_noop("%(quota)s: cannot resize the memory of '%(instance_name)s'."),
|
||||
instance_name=instance.name)
|
||||
messages.error(request, msg)
|
||||
else:
|
||||
with audit_vm(request, instance, f"set memory to {new_cur_memory} MiB (maximum {new_memory} MiB)"):
|
||||
|
|
@ -1263,12 +1255,8 @@ def resize_disk(request, pk):
|
|||
request.user, 0, 0, 0, disk_new_sum - disk_sum
|
||||
)
|
||||
if not request.user.is_superuser and quota_msg:
|
||||
msg = quota_refused(quota_msg, _(
|
||||
"User %(quota_msg)s quota reached, cannot resize disks of '%(instance_name)s'!"
|
||||
) % {
|
||||
"quota_msg": quota_msg,
|
||||
"instance_name": instance.name,
|
||||
})
|
||||
msg = quota_refused(quota_msg, gettext_noop("%(quota)s: cannot resize the disks of '%(instance_name)s'."),
|
||||
instance_name=instance.name)
|
||||
messages.error(request, msg)
|
||||
else:
|
||||
for disk in disks_new:
|
||||
|
|
@ -2393,9 +2381,8 @@ def set_template(request, pk):
|
|||
quota_msg = utils.check_user_quota(request.user, 1, instance.vcpu, instance.memory,
|
||||
utils.disk_gib(instance.disks))
|
||||
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}))
|
||||
messages.error(request, quota_refused(quota_msg, gettext_noop("%(quota)s: '%(instance_name)s' stays a template."),
|
||||
instance_name=instance.name))
|
||||
return _back(request, pk, "options")
|
||||
with audit_vm(request, instance, "mark as template" if is_template else "unmark as template"):
|
||||
instance.is_template = is_template
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue