1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 19:56:48 +00:00

fix(i18n): refusals and checks users read are translated

English-only texts users read, now in their language:
- the serial console's four refusals, which socketiod sends in the
  language LocaleMiddleware would choose (cookie, then browser) and logs
  in English; the texts live in console/refusals.py, as makemessages
  reads only .py files;
- a submission key reused for another request, a malformed MAC address,
  the moved-VM refusal of a script's request, the API's "Templates
  cannot be started" (now the page's sentence), "Pool is not active"
  and the ISO upload's checks;
- "Problem occurred with host" was a fragment with the host's name and
  error after it: one sentence with both.
This commit is contained in:
catborise 2026-11-06 23:16:41 +03:00
parent 6abea495f1
commit 6d22a18ba7
14 changed files with 187 additions and 41 deletions

View file

@ -27,7 +27,7 @@
<div class="col-lg-12">
{% if compute.status is not True %}
<div class="alert alert-danger shadow-sm">
{% trans 'Problem occurred with host' %} {{ compute.name }}: {{ compute.connection_error }}
{% blocktrans with name=compute.name error=compute.connection_error %}Problem occurred with host {{ name }}: {{ error }}{% endblocktrans %}
</div>
{% endif %}
{% if not instances %}

9
console/refusals.py Normal file
View file

@ -0,0 +1,9 @@
"""socketiod's refusals, which its page shows in the user's language. Here,
as makemessages reads only .py files and socketiod has no extension."""
from django.utils.translation import gettext_noop
TURNED_OFF = gettext_noop("The serial console is turned off on this server")
NOT_ALLOWED = gettext_noop("The console link has expired, or you may no longer open this console: reload the page")
OPEN_ELSEWHERE = gettext_noop("The console of this VM is open in another window")
TOO_MANY = gettext_noop("Too many consoles are open on this server: try again later")

View file

@ -30,7 +30,11 @@ import socketio # noqa: E402
from gunicorn.app.base import BaseApplication # noqa: E402
from django import db # noqa: E402
from django.conf import settings # noqa: E402
from django.http import HttpRequest # noqa: E402
from django.http.cookie import parse_cookie # noqa: E402
from django.utils import translation # noqa: E402
from console import refusals # noqa: E402
from console.tokens import authorize, session_user_id # noqa: E402
CONSOLECALLBACK = os.path.join(ROOT_PATH, "conf", "daemon", "consolecallback")
@ -49,30 +53,34 @@ sessions = {}
sessions_lock = threading.Lock()
def refused(reason):
"""The refusal the page shows; it is logged too."""
def refused(environ, reason):
"""The refusal the page shows, in its language (LocaleMiddleware's choice:
the language cookie, else the browser's); it is logged too, in English."""
logger.info("serial console refused: %s", reason)
return socketio.exceptions.ConnectionRefusedError(reason)
request = HttpRequest()
request.COOKIES, request.META = parse_cookie(environ.get("HTTP_COOKIE", "")), environ
with translation.override(translation.get_language_from_request(request)):
return socketio.exceptions.ConnectionRefusedError(translation.gettext(reason))
@sio.event
def connect(sid, environ, auth):
# the page shows the reason
if not settings.SERIAL_CONSOLE_ENABLED:
raise refused("The serial console is turned off on this server")
raise refused(environ, refusals.TURNED_OFF)
try:
instance, user = authorize((auth or {}).get("token"), "pty")
except PermissionError:
raise refused("The console link has expired, or you may no longer open this console: reload the page")
raise refused(environ, refusals.NOT_ALLOWED)
finally:
db.close_old_connections()
taken = None
with sessions_lock:
open_sid = next((s for s, session in sessions.items() if session["vm"] == instance.pk), None)
if open_sid is not None and not user.is_superuser:
raise refused("The console of this VM is open in another window")
raise refused(environ, refusals.OPEN_ELSEWHERE)
if open_sid is None and len(sessions) >= MAX_SESSIONS:
raise refused("Too many consoles are open on this server: try again later")
raise refused(environ, refusals.TOO_MANY)
if open_sid is not None: # a superuser takes over a console left open
taken = open_sid, sessions.pop(open_sid)
# the page tells its user who took the console; first, as its

View file

@ -116,6 +116,18 @@ class SocketiodTestCase(Base):
self.assertIn("Too many", self.refused("over", {"token": self.token(vm=other)}))
self.assertEqual(list(self.d.sessions), ["first"])
@override_settings(SERIAL_CONSOLE_ENABLED=False)
def test_a_refusal_is_in_the_page_s_language_and_logged_in_english(self):
# as LocaleMiddleware: the language cookie first, then the browser's
for environ, shown in (({"HTTP_COOKIE": "django_language=tr", "HTTP_ACCEPT_LANGUAGE": "zh-hans"}, "Seri konsol bu sunucuda kapalı"),
({"HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.9"}, "此服务器已关闭串行控制台"),
({}, "The serial console is turned off on this server")):
with self.subTest(shown), self.assertRaises(self.d.socketio.exceptions.ConnectionRefusedError) as refusal, \
self.assertLogs("socketiod", level="INFO") as log:
self.d.connect("sid", environ, {"token": self.token()})
self.assertEqual(refusal.exception.error_args["message"], shown)
self.assertIn("refused: The serial console is turned off on this server", log.output[0])
@override_settings(SERIAL_CONSOLE_ENABLED=True)
def test_input_and_resize_go_to_the_session_and_are_checked(self):
self.d.connect("sid-1", {}, {"token": self.token()})

View file

@ -5,6 +5,7 @@ from django.contrib.messages import get_messages
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext as _
from instances.jobs import MIGRATE_OPTIONS
from instances.models import Flavor, Instance
from instances.views import (
@ -144,7 +145,7 @@ class InstanceViewSet(viewsets.ViewSet):
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
if queryset.is_template:
return Response({"status": "Templates cannot be started"}, status=status.HTTP_409_CONFLICT)
return Response({"status": _("Templates cannot be started.")}, status=status.HTTP_409_CONFLICT)
return instance_action_response(poweron, request, pk, "poweron command send")
@extend_schema(request=None, responses=ActionStatusSerializer)
@ -162,7 +163,7 @@ class InstanceViewSet(viewsets.ViewSet):
if compute_pk is not None and str(queryset.compute_id) != str(compute_pk):
raise Http404()
if queryset.is_template:
return Response({"status": "Templates cannot be started"}, status=status.HTTP_409_CONFLICT)
return Response({"status": _("Templates cannot be started.")}, status=status.HTTP_409_CONFLICT)
return instance_action_response(powercycle, request, pk, "powercycle command send")
@extend_schema(request=None, responses=ActionStatusSerializer)

View file

@ -640,7 +640,7 @@ def _busy(request, pk, busy):
def _moved(request, pk):
if hasattr(request, "accepted_renderer") or from_script(request):
return JsonResponse({"detail": "The VM was moved meanwhile; try again."}, status=409)
return JsonResponse({"detail": _("The VM was moved meanwhile; try again.")}, status=409)
messages.error(request, _("The VM was moved meanwhile; try again."))
return get_safe_redirect(request, default=reverse("instances:instance", args=[pk]))

Binary file not shown.

View file

@ -2119,8 +2119,10 @@ msgid "New Instance"
msgstr "Yeni sanal makine"
#: computes/templates/computes/instances.html:30
msgid "Problem occurred with host"
msgstr "Sunucu ile ilgili bir problem meydana geldi"
#, python-format
#| msgid "Problem occurred with host"
msgid "Problem occurred with host %(name)s: %(error)s"
msgstr "%(name)s sunucusuyla ilgili bir sorun oluştu: %(error)s"
#: computes/templates/computes/instances.html:36
#: computes/templates/computes/list.html:16
@ -2379,7 +2381,7 @@ msgstr ""
"Sunucudaki /etc/default/webvirt-gstfsd içindeki GSTFSD_KEY değerinin 64 "
"onaltılık karakterini girin"
#: computes/views.py:202
#: computes/views.py:205
#, python-format
msgid ""
"Its VMs keep running on the host and leave the panel; their owners are kept "
@ -2390,6 +2392,28 @@ msgstr ""
"Kaldırılan Sanal Makineler sayfasında %(days)s gün tutulur ve sunucu yeniden "
"eklenirse geri gelir."
#: console/refusals.py:6
#| msgid "The device is not on this host"
msgid "The serial console is turned off on this server"
msgstr "Seri konsol bu sunucuda kapalı"
#: console/refusals.py:7
msgid ""
"The console link has expired, or you may no longer open this console: reload "
"the page"
msgstr ""
"Konsol bağlantısının süresi doldu ya da bu konsolu artık açamazsınız: sayfayı"
" yeniden yükleyin"
#: console/refusals.py:8
#| msgid "The console starts scaled to its window"
msgid "The console of this VM is open in another window"
msgstr "Bu sanal makinenin konsolu başka bir pencerede açık"
#: console/refusals.py:9
msgid "Too many consoles are open on this server: try again later"
msgstr "Bu sunucuda çok fazla konsol açık: daha sonra yeniden deneyin"
#: console/templates/console-menu.html:6
msgid "Send key(s)"
msgstr "Anahtar(ları) gönder"
@ -2634,6 +2658,11 @@ msgstr ""
"'%(type)s' konsol tipi desteklenmiyor. Lütfen sanal makinenin grafik tipini "
"VNC olarak değiştirin."
#: instances/api/viewsets.py:148 instances/api/viewsets.py:166
#: instances/views.py:792 instances/views.py:807
msgid "Templates cannot be started."
msgstr "Şablon makineler başlatılamaz."
#: instances/forms.py:13 instances/templates/create_instance_w2.html:69
#: instances/templates/instances/settings_tab.html:857
msgid "Memory (MiB)"
@ -2999,7 +3028,6 @@ msgstr "Sanal makine tanımlanıyor"
#: instances/jobs.py:906
#, python-format
#| msgid "The panel failed unexpectedly; its log has the details."
msgid ""
"The panel failed unexpectedly; its log has the details. Remove these volumes "
"by hand: %(volumes)s"
@ -5924,7 +5952,7 @@ msgstr ""
"Sunucu aygıtları olan bir sanal makine taşınamaz: hedefte bu aygıtlar "
"olmayabilir. Önce onları kaldırın."
#: instances/views.py:644
#: instances/views.py:643 instances/views.py:644
msgid "The VM was moved meanwhile; try again."
msgstr "Sanal makine bu arada taşındı; yeniden deneyin."
@ -5958,10 +5986,6 @@ msgstr ""
msgid "Invalid disk options: %(options)s"
msgstr "Geçersiz disk seçenekleri: %(options)s"
#: instances/views.py:792 instances/views.py:807
msgid "Templates cannot be started."
msgstr "Şablon makineler başlatılamaz."
#: instances/views.py:999
#, python-format
msgid "Set the gstfsd key of %(compute)s first (Computes, edit the host)"
@ -7628,6 +7652,12 @@ msgstr ""
msgid "%(phase)s (over its time limit)"
msgstr "%(phase)s (süre sınırını aştı)"
#: operations/jobs.py:151
#| msgid "The form has no valid submission key; reload the page"
msgid "This form was already submitted for another request; reload the page"
msgstr ""
"Bu form başka bir istek için zaten gönderildi; sayfayı yeniden yükleyin"
#: operations/jobs.py:157
msgid "The form has no valid submission key; reload the page"
msgstr "Formun geçerli bir gönderim anahtarı yok; sayfayı yeniden yükleyin"
@ -7907,10 +7937,14 @@ msgstr ""
"Kurtarma bir saat boyunca işin durumunu okuyamadı; sanal makineyi kontrol "
"edin, sonra bu işi çözün"
#: operations/worker.py:474
#: operations/worker.py:479
msgid "The worker stopped before the job changed anything"
msgstr "Worker, iş herhangi bir şeyi değiştirmeden önce durdu"
#: storages/api/viewsets.py:203 storages/api/viewsets.py:224
msgid "Pool is not active"
msgstr "Havuz etkin değil"
#: storages/forms.py:30 storages/templates/storage.html:102
#: storages/templates/storages.html:43
msgid "Path"
@ -8009,6 +8043,26 @@ msgstr "Kopya oluşturulmadı"
msgid "Upload ISO image"
msgstr "ISO imajı yükle"
#: storages/jobs.py:118
#| msgid "Invalid file size."
msgid "Invalid file name"
msgstr "Geçersiz dosya adı"
#: storages/jobs.py:120
#| msgid "Only .iso files can be uploaded."
msgid "Only .iso files can be uploaded"
msgstr "Yalnızca .iso dosyaları yüklenebilir"
#: storages/jobs.py:122
#| msgid "Invalid file size."
msgid "Invalid file size"
msgstr "Geçersiz dosya boyutu"
#: storages/jobs.py:124
#| msgid "Invalid chunk index or total chunks."
msgid "Invalid chunk index or total chunks"
msgstr "Geçersiz parça dizini veya toplam parça sayısı"
#: storages/templates/create_stg_block.html:7
msgid "New pool"
msgstr "Yeni havuz"
@ -9834,6 +9888,11 @@ msgstr "XML geçerli değil: %(err)s"
msgid "The XML may not declare a DTD"
msgstr "XML bir DTD bildiremez"
#: vrtManager/util.py:143
#, python-format
msgid "MAC address must be of the format AA:BB:CC:DD:EE:FF, was %(mac)s"
msgstr "MAC adresi AA:BB:CC:DD:EE:FF biçiminde olmalı; girilen: %(mac)s"
#: vrtManager/virtsecrets.py:26
msgid "A Ceph key is base64 text, as `ceph auth get-key` prints it"
msgstr ""

Binary file not shown.

View file

@ -2050,8 +2050,10 @@ msgid "New Instance"
msgstr "新建虚拟机"
#: computes/templates/computes/instances.html:30
msgid "Problem occurred with host"
msgstr "主机出现问题"
#, python-format
#| msgid "Problem occurred with host"
msgid "Problem occurred with host %(name)s: %(error)s"
msgstr "主机 %(name)s 出现问题:%(error)s"
#: computes/templates/computes/instances.html:36
#: computes/templates/computes/list.html:16
@ -2302,7 +2304,7 @@ msgid ""
"/etc/default/webvirt-gstfsd"
msgstr "请输入主机 /etc/default/webvirt-gstfsd 中 GSTFSD_KEY 的 64 个十六进制字符"
#: computes/views.py:202
#: computes/views.py:205
#, python-format
msgid ""
"Its VMs keep running on the host and leave the panel; their owners are kept "
@ -2310,6 +2312,26 @@ msgid ""
"again."
msgstr "其虚拟机继续在主机上运行并离开面板;其所有者在“已移除的虚拟机”页面保留 %(days)s 天,如果再次添加该主机则会恢复。"
#: console/refusals.py:6
#| msgid "The device is not on this host"
msgid "The serial console is turned off on this server"
msgstr "此服务器已关闭串行控制台"
#: console/refusals.py:7
msgid ""
"The console link has expired, or you may no longer open this console: reload "
"the page"
msgstr "控制台链接已过期,或您已无权打开此控制台:请重新加载页面"
#: console/refusals.py:8
#| msgid "The console starts scaled to its window"
msgid "The console of this VM is open in another window"
msgstr "此虚拟机的控制台已在另一个窗口中打开"
#: console/refusals.py:9
msgid "Too many consoles are open on this server: try again later"
msgstr "此服务器上打开的控制台过多:请稍后再试"
#: console/templates/console-menu.html:6
msgid "Send key(s)"
msgstr "发送按键"
@ -2541,6 +2563,11 @@ msgid ""
"VNC."
msgstr "不支持控制台类型 '%(type)s'。请将虚拟机图形切换为 VNC。"
#: instances/api/viewsets.py:148 instances/api/viewsets.py:166
#: instances/views.py:792 instances/views.py:807
msgid "Templates cannot be started."
msgstr "模板无法启动。"
#: instances/forms.py:13 instances/templates/create_instance_w2.html:69
#: instances/templates/instances/settings_tab.html:857
msgid "Memory (MiB)"
@ -2879,7 +2906,6 @@ msgstr "正在定义虚拟机"
#: instances/jobs.py:906
#, python-format
#| msgid "The panel failed unexpectedly; its log has the details."
msgid ""
"The panel failed unexpectedly; its log has the details. Remove these volumes "
"by hand: %(volumes)s"
@ -5623,7 +5649,7 @@ msgid ""
" Remove them first."
msgstr "有主机设备的虚拟机无法迁移:目标主机可能没有这些设备。请先移除它们。"
#: instances/views.py:644
#: instances/views.py:643 instances/views.py:644
msgid "The VM was moved meanwhile; try again."
msgstr "该虚拟机在此期间已被移动;请重试。"
@ -5652,10 +5678,6 @@ msgstr ""
msgid "Invalid disk options: %(options)s"
msgstr "无效的磁盘选项:%(options)s"
#: instances/views.py:792 instances/views.py:807
msgid "Templates cannot be started."
msgstr "模板无法启动。"
#: instances/views.py:999
#, python-format
msgid "Set the gstfsd key of %(compute)s first (Computes, edit the host)"
@ -7226,6 +7248,11 @@ msgstr "字母、数字、点、短横线和下划线。副本包含该过滤器
msgid "%(phase)s (over its time limit)"
msgstr "%(phase)s(已超出时间限制)"
#: operations/jobs.py:151
#| msgid "The form has no valid submission key; reload the page"
msgid "This form was already submitted for another request; reload the page"
msgstr "此表单已为另一个请求提交过;请重新加载页面"
#: operations/jobs.py:157
msgid "The form has no valid submission key; reload the page"
msgstr "表单没有有效的提交密钥;请重新加载页面"
@ -7490,10 +7517,14 @@ msgid ""
"resolve this job"
msgstr "恢复在一小时内无法读取任务状态;请检查虚拟机,然后处理此任务"
#: operations/worker.py:474
#: operations/worker.py:479
msgid "The worker stopped before the job changed anything"
msgstr "工作进程在任务做出任何更改之前停止"
#: storages/api/viewsets.py:203 storages/api/viewsets.py:224
msgid "Pool is not active"
msgstr "存储池未激活"
#: storages/forms.py:30 storages/templates/storage.html:102
#: storages/templates/storages.html:43
msgid "Path"
@ -7591,6 +7622,26 @@ msgstr "未创建副本"
msgid "Upload ISO image"
msgstr "上传 ISO 镜像"
#: storages/jobs.py:118
#| msgid "Invalid file size."
msgid "Invalid file name"
msgstr "文件名无效"
#: storages/jobs.py:120
#| msgid "Only .iso files can be uploaded."
msgid "Only .iso files can be uploaded"
msgstr "只能上传 .iso 文件"
#: storages/jobs.py:122
#| msgid "Invalid file size."
msgid "Invalid file size"
msgstr "文件大小无效"
#: storages/jobs.py:124
#| msgid "Invalid chunk index or total chunks."
msgid "Invalid chunk index or total chunks"
msgstr "分块索引或分块总数无效"
#: storages/templates/create_stg_block.html:7
msgid "New pool"
msgstr "新建存储池"
@ -9274,6 +9325,11 @@ msgstr "XML 无效:%(err)s"
msgid "The XML may not declare a DTD"
msgstr "XML 不能声明 DTD"
#: vrtManager/util.py:143
#, python-format
msgid "MAC address must be of the format AA:BB:CC:DD:EE:FF, was %(mac)s"
msgstr "MAC 地址的格式必须为 AA:BB:CC:DD:EE:FF,实际为 %(mac)s"
#: vrtManager/virtsecrets.py:26
msgid "A Ceph key is base64 text, as `ceph auth get-key` prints it"
msgstr "Ceph 密钥是 base64 文本,即 `ceph auth get-key` 输出的内容"

View file

@ -13,7 +13,7 @@ from django.db import transaction
from django.db.models import F, Value
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import gettext_noop
from django.utils.translation import gettext, gettext_noop
from logs.audit import write
from vrtManager.util import OperationError
@ -148,7 +148,7 @@ def _existing(user, key, kind_name, params):
Operation.objects.filter(pk=op.pk).update(submission_key=f"expired:{op.pk.hex}")
return None
if op.kind != kind_name or op.params != params:
raise SubmissionConflict("This form was already submitted for another request; reload the page")
raise SubmissionConflict(gettext("This form was already submitted for another request; reload the page"))
return op

View file

@ -5,6 +5,7 @@ from appsettings.settings import app_settings
from computes.models import Compute
from computes.utils import libvirt_compute_lock
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema, inline_serializer
from libvirt import libvirtError
from logs.audit import audit, failed
@ -199,7 +200,7 @@ class VolumeViewSet(viewsets.ViewSet):
return Response({"status": "Volume: " + ret + " is created"})
else:
failed(request, compute, subject, action, "pool is not active")
return Response({"status": "Pool is not active"}, status=status.HTTP_409_CONFLICT)
return Response({"status": _("Pool is not active")}, status=status.HTTP_409_CONFLICT)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -220,4 +221,4 @@ class VolumeViewSet(viewsets.ViewSet):
return Response({"status": "Volume: " + pk + " is deleted"})
else:
failed(request, compute, subject, action, "pool is not active")
return Response({"status": "Pool is not active"}, status=status.HTTP_409_CONFLICT)
return Response({"status": _("Pool is not active")}, status=status.HTTP_409_CONFLICT)

View file

@ -5,7 +5,7 @@ import logging
import os
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_noop
from django.utils.translation import gettext, gettext_noop
from libvirt import libvirtError
from vrtManager import util
from vrtManager.storage import wvmStorage
@ -115,13 +115,13 @@ class UploadVolume(Kind):
raise PermissionDenied
name = params["name"]
if not name or name in (".", "..") or "\x00" in name:
raise ValueError("Invalid file name")
raise ValueError(gettext("Invalid file name"))
if len(name) <= 4 or not is_iso(name):
raise ValueError("Only .iso files can be uploaded")
raise ValueError(gettext("Only .iso files can be uploaded"))
if params["size"] <= 0:
raise ValueError("Invalid file size")
raise ValueError(gettext("Invalid file size"))
if params["chunks"] < 1:
raise ValueError("Invalid chunk index or total chunks")
raise ValueError(gettext("Invalid chunk index or total chunks"))
def plan(self, user, params):
compute, stg = _storage(params)

View file

@ -140,7 +140,7 @@ def validate_macaddr(val):
form = re.match("^([0-9a-fA-F]{1,2}:){5}[0-9a-fA-F]{1,2}$", val)
if form is None:
raise ValueError("MAC address must be of the format AA:BB:CC:DD:EE:FF, was {}".format(val))
raise ValueError(gettext("MAC address must be of the format AA:BB:CC:DD:EE:FF, was %(mac)s") % {"mac": val})
def vol_dev_type(vol_bus):