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

fix(accounts): an OTP mail links to the QR code instead of holding its secret

The Lost OTP? page, open without a login, and the admin's Email OTP QR
code mailed the QR code itself: the TOTP secret then stayed in the
mailbox and on every server the mail passed. The mail now carries a
signed link, good for an hour, to a page that shows the QR code (not
cached); the page shows nothing for an old or altered link or a user
without a device. tr and zh too.
This commit is contained in:
catborise 2026-11-05 17:28:13 +03:00
parent c62de11147
commit c42f74b4ee
12 changed files with 314 additions and 279 deletions

View file

@ -454,6 +454,8 @@ An LDAP user is authenticated by LDAP and authorized through the WebVirtCloud pe
Set `OTP_ENABLED = True` in `settings.py` to ask for a one-time code at login. It is per user: a user without a confirmed device signs in with the password alone.
The QR code is mailed as a link to a page that shows it, good for one hour: the mail does not hold the code's secret. Email OTP QR code on a user's page (Users) sends it; Lost OTP? on the login page sends it to an enrolled user who gives their address.
### Serial console
A VM without graphics (no `<graphics>` device, a `<console type='pty'>`) opens a serial console (xterm.js) instead of VNC. It is off by default. To turn it on, set `WEBVIRTCLOUD_SERIAL_CONSOLE=1` (Docker: in `docker-compose.override.yml`, then `docker compose up -d`) or `SERIAL_CONSOLE_ENABLED = True` in `settings.py` and `systemctl restart webvirt-cloud webvirt-socketiod`.

View file

@ -1,8 +0,0 @@
{% load i18n %}
{% blocktrans %}
Scan this QR code to get OTP for account '{{ user }}'
{% endblocktrans %}
<br>
{{ qr_svg }}
<p class="small">{% trans 'Some e-mail clients does not render SVG, also generating PNG.' %}</p>
<img src="{{ qr_png }}" alt="{{ totp_url }}">

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "OTP QR Code" %}{% endblock %}
{% block page_heading %}{% trans "OTP QR Code" %}{% endblock page_heading %}
{% block content %}
<div class="card">
<div class="card-body">
<p>{% blocktrans %}Scan this QR code to get OTP for account '{{ user }}'{% endblocktrans %}</p>
{{ qr_svg }}
</div>
</div>
{% endblock content %}

View file

@ -2,17 +2,19 @@ import base64
import hashlib
import re
import struct
import zlib
import time
from types import SimpleNamespace
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import TestCase
from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse
from django_otp.plugins.otp_totp.models import TOTPDevice
from lxml import html
from accounts.utils import (get_existing_totp_device, get_user_totp_device, send_email_with_otp, validate_ssh_key)
from accounts.utils import (OTP_LINK, OTP_LINK_MAX_AGE, get_existing_totp_device, get_user_totp_device, otp_qr_svg,
send_email_with_otp, validate_ssh_key)
class AccountsUtilsTestCase(TestCase):
@ -80,32 +82,30 @@ class AccountsUtilsTestCase(TestCase):
device = TOTPDevice.objects.create(user=self.user, confirmed=True)
self.assertEqual(get_existing_totp_device(self.user).id, device.id)
def test_send_email_with_otp(self):
@override_settings(OTP_ENABLED=True)
def test_the_mail_links_to_the_qr_code_and_holds_no_secret(self):
device = get_user_totp_device(self.user)
send_email_with_otp(self.user, device)
send_email_with_otp(RequestFactory().get("/"), self.user)
self.assertEqual(len(mail.outbox), 1)
self.assertIn("OTP QR Code", mail.outbox[0].subject)
self.assertEqual(mail.outbox[0].to, ["user@example.com"])
# the QR code as inline SVG and, for clients without SVG, as a PNG data URI
doc = html.fromstring(mail.outbox[0].alternatives[0][0])
self.assertEqual(len(doc.xpath("//svg")), 1)
(img,) = doc.xpath("//img")
self.assertEqual(img.get("alt"), device.config_url)
self.assertTrue(base64.b64decode(img.get("src").removeprefix("data:image/png;base64,")).startswith(b"\x89PNG"))
message = mail.outbox[0]
self.assertEqual((message.subject, message.to, message.alternatives), ("OTP QR Code", ["user@example.com"], []))
self.assertNotIn(device.bin_key.hex(), message.body)
self.assertNotIn("otpauth", message.body)
link = re.search(r"http://testserver(/\S+)", message.body).group(1)
page = self.client.get(link)
self.assertEqual(page["Cache-Control"], "no-store")
self.assertEqual(len(html.fromstring(page.content).xpath("//svg")), 1)
def test_otp_email_qr_codes_are_what_django_qr_code_drew(self):
# sha256 of the SVG and the PNG django-qr-code 4.2.0 made for this URL
@override_settings(OTP_ENABLED=True)
def test_a_link_that_is_old_or_not_signed_shows_nothing(self):
get_user_totp_device(self.user)
token = OTP_LINK.sign(str(self.user.pk))
with patch("django.core.signing.time.time", return_value=time.time() + OTP_LINK_MAX_AGE + 1):
self.assertEqual(self.client.get(reverse("accounts:otp_qr", args=[token])).status_code, 404)
self.assertEqual(self.client.get(reverse("accounts:otp_qr", args=[f"{self.user.pk}:x:y"])).status_code, 404)
def test_the_qr_code_is_what_django_qr_code_drew(self):
# sha256 of the SVG django-qr-code 4.2.0 made for this URL
url = "otpauth://totp/WebVirtCloud:alice?secret=JBSWY3DPEHPK3PXP&issuer=WebVirtCloud"
with patch("accounts.utils.send_mail") as send:
send_email_with_otp(self.user, SimpleNamespace(config_url=url))
page = send.call_args.kwargs["html_message"]
svg = re.search(r"<svg.*?</svg>", page, re.S).group(0)
png = base64.b64decode(re.search(r'<img src="data:image/png;base64,([^"]+)"', page).group(1))
svg = str(otp_qr_svg(SimpleNamespace(config_url=url)))
self.assertEqual(hashlib.sha256(svg.encode()).hexdigest(), "b57323be725891af13d862a3a3351cac3ac64a493793c0258cf3e4a5886467c2")
# the PNG's header and pixel rows: its compressed bytes depend on the zlib build
chunks, pos = {}, 8
while pos < len(png):
size, kind = struct.unpack(">I4s", png[pos:pos + 8])
chunks[kind] = chunks.get(kind, b"") + png[pos + 8:pos + 8 + size]
pos += 12 + size
self.assertEqual(hashlib.sha256(chunks[b"IHDR"] + zlib.decompress(chunks[b"IDAT"])).hexdigest(), "cca16de611c73468827f70001f69dc2e48633e6aae2b570a9391eb40a1f8ae88")

View file

@ -38,6 +38,7 @@ urlpatterns = [
path("api_token/created/", views.api_token_created, name="api_token_created"),
path("api_token/<str:token_key>/delete/", views.api_token_delete, name="api_token_delete"),
path("email_otp/", views.email_otp, name="email_otp"),
path("otp_qr/<str:token>/", views.otp_qr, name="otp_qr"),
path(
"admin_email_otp/<int:user_id>/",
views.admin_email_otp,

View file

@ -5,7 +5,8 @@ from decimal import Decimal
import segno
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.core.signing import TimestampSigner
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django_otp import devices_for_user
@ -56,24 +57,26 @@ def validate_ssh_key(key):
return True
def send_email_with_otp(user, device):
# An inline SVG (1.8 mm modules) and, for mail clients that show no SVG,
# a PNG (12 px modules); error correction M, 4-module border.
qr = segno.make(device.config_url, error="m", mode="byte", encoding="utf-8", micro=False)
style = {"border": 4, "dark": "black", "light": "white"}
# the link a mail carries to its user's OTP QR code: the mail holds no secret
OTP_LINK = TimestampSigner(salt="accounts.otp_qr")
OTP_LINK_MAX_AGE = 3600
def send_email_with_otp(request, user):
"""Mail the user a link to the page of their OTP QR code, good for
OTP_LINK_MAX_AGE: the QR code's secret is never in a mailbox."""
url = request.build_absolute_uri(reverse("accounts:otp_qr", args=[OTP_LINK.sign(str(user.pk))]))
send_mail(
_("OTP QR Code"),
_("Please view HTML version of this message."),
_("Open this link within an hour to see the OTP QR code of account '%(user)s':\n\n%(url)s")
% {"user": user, "url": url},
None,
[user.email],
html_message=render_to_string(
"accounts/email/otp.html",
{
"totp_url": device.config_url,
"user": user,
"qr_svg": mark_safe(qr.svg_inline(scale=Decimal("1.8"), unit="mm", **style)),
"qr_png": qr.png_data_uri(scale=12, **style),
},
),
fail_silently=False,
)
def otp_qr_svg(device):
"""The device's QR code as inline SVG: 1.8 mm modules, error correction M, a 4-module border."""
qr = segno.make(device.config_url, error="m", mode="byte", encoding="utf-8", micro=False)
return mark_safe(qr.svg_inline(scale=Decimal("1.8"), unit="mm", border=4, dark="black", light="white"))

View file

@ -9,6 +9,7 @@ from django.contrib.auth import get_user_model, update_session_auth_hash
from django.contrib.auth.decorators import login_not_required, login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.core.exceptions import PermissionDenied
from django.core.signing import BadSignature
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@ -28,7 +29,7 @@ from accounts.forms import ApiTokenForm, EmailOTPForm, ProfileForm, UserSSHKeyFo
from accounts.models import ApiToken, User, UserAttributes, UserInstance, UserSSHKey
from . import forms
from .utils import get_existing_totp_device, get_user_totp_device, send_email_with_otp
from .utils import OTP_LINK, OTP_LINK_MAX_AGE, get_existing_totp_device, get_user_totp_device, otp_qr_svg, send_email_with_otp
def profile(request):
@ -301,7 +302,7 @@ EMAIL_OTP_INTERVAL = 300
@login_not_required
def email_otp(request):
"""
"Lost OTP?": mail the OTP QR code to the user with this address. Open to
"Lost OTP?": mail a link to the OTP QR code to the user with this address. Open to
anyone, so it mails only one exact, active match, at most once per
EMAIL_OTP_INTERVAL, and answers the same in every case.
"""
@ -318,7 +319,7 @@ def email_otp(request):
device = get_existing_totp_device(user)
if device is not None:
try:
send_email_with_otp(user, device)
send_email_with_otp(request, user)
except Exception:
logging.getLogger(__name__).exception("Could not mail the OTP QR code to user %s", user.pk)
@ -337,6 +338,24 @@ def email_otp(request):
)
@login_not_required
def otp_qr(request, token):
"""The OTP QR code of the user a mailed link (send_email_with_otp) names."""
if not settings.OTP_ENABLED:
raise Http404
try:
user_id = OTP_LINK.unsign(token, max_age=OTP_LINK_MAX_AGE)
except BadSignature:
raise Http404
user = get_object_or_404(get_user_model(), pk=user_id, is_active=True)
device = get_existing_totp_device(user)
if device is None:
raise Http404
response = render(request, "accounts/otp_qr.html", {"user": user, "qr_svg": otp_qr_svg(device)})
response["Cache-Control"] = "no-store"
return response
@require_POST
@superuser_only
def admin_email_otp(request, user_id):
@ -345,7 +364,8 @@ def admin_email_otp(request, user_id):
if user.email != "":
try:
with audit(request, None, subject, "mail OTP QR code"):
send_email_with_otp(user, get_user_totp_device(user))
get_user_totp_device(user)
send_email_with_otp(request, user)
except OSError as err: # the SMTP server refused it, or did not answer within EMAIL_TIMEOUT
messages.error(request, _("The OTP QR code could not be mailed to %(user)s: %(error)s") % {
"user": user, "error": err})

Binary file not shown.

View file

@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-11-05 13:37+0000\n"
"POT-Creation-Date: 2026-11-05 14:27+0000\n"
"PO-Revision-Date: 2020-11-04 12:00+0000\n"
"Last-Translator: catborise <muhammetalisag@gmail.com>, 2020\n"
"Language-Team: Turkish (https://www.transifex.com/catborise/teams/110663/tr/)\n"
@ -314,19 +314,6 @@ msgstr "İptal"
msgid "Change password"
msgstr "Parolayı değiştir"
#: accounts/templates/accounts/email/otp.html:2
#, python-format
msgid ""
"\n"
"Scan this QR code to get OTP for account '%(user)s'\n"
msgstr ""
"\n"
"'%(user)s' hesabı için bu OTP QR kodunu tarayın\n"
#: accounts/templates/accounts/email/otp.html:7
msgid "Some e-mail clients does not render SVG, also generating PNG."
msgstr "Bazı e-posta istemcileri SVG göstermez, PNG de oluşturuluyor."
#: accounts/templates/accounts/email_otp_form.html:12
msgid ""
"\n"
@ -408,6 +395,16 @@ msgstr "Doğrula ve oturum aç"
msgid "2FA Protection Active"
msgstr "2FA Koruması Etkin"
#: accounts/templates/accounts/otp_qr.html:4
#: accounts/templates/accounts/otp_qr.html:6 accounts/utils.py:70
msgid "OTP QR Code"
msgstr "OTP QR Kodu"
#: accounts/templates/accounts/otp_qr.html:11
#, python-format
msgid "Scan this QR code to get OTP for account '%(user)s'"
msgstr "'%(user)s' hesabının OTP kodlarını almak için bu QR kodunu tarayın"
#: accounts/templates/api_token_created.html:4
#: accounts/templates/api_token_created.html:5
msgid "API token created"
@ -526,7 +523,7 @@ msgid "Add"
msgstr "Ekle"
#: accounts/templates/profile.html:64 accounts/templates/profile.html:70
#: accounts/views.py:69 accounts/views.py:73
#: accounts/views.py:70 accounts/views.py:74
msgid "Create an API token"
msgstr "API anahtarı oluştur"
@ -556,97 +553,100 @@ msgstr "%(name)s SSH anahtarı için işlemler"
msgid "No SSH keys."
msgstr "SSH anahtarı yok."
#: accounts/utils.py:65
msgid "OTP QR Code"
msgstr "OTP QR Kodu"
#: accounts/utils.py:71
#, python-format
msgid ""
"Open this link within an hour to see the OTP QR code of account '%(user)s':\n"
"\n"
"%(url)s"
msgstr ""
"'%(user)s' hesabının OTP QR kodunu görmek için bu bağlantıyı bir saat içinde açın:\n"
"\n"
"%(url)s"
#: accounts/utils.py:66
msgid "Please view HTML version of this message."
msgstr "Lütfen bu mesajın HTML sürümünü görüntüleyin."
#: accounts/views.py:46
#: accounts/views.py:47
msgid "Profile updated"
msgstr "Profil güncellendi"
#: accounts/views.py:72
#: accounts/views.py:73
#, python-format
msgid "A user has at most %(limit)s API tokens: delete one first."
msgstr ""
"Bir kullanıcının en fazla %(limit)s API anahtarı olabilir: önce birini silin."
#: accounts/views.py:107
#: accounts/views.py:108
msgid "API token deleted"
msgstr "API anahtarı silindi"
#: accounts/views.py:115
#: accounts/views.py:116
msgid "Delete API token"
msgstr "API anahtarını sil"
#: accounts/views.py:116
#: accounts/views.py:117
msgid "Scripts and tools that use it can no longer reach the API."
msgstr "Onu kullanan betikler ve araçlar artık API'ye erişemez."
#: accounts/views.py:126
#: accounts/views.py:127
msgid "SSH key added"
msgstr "SSH anahtarı eklendi"
#: accounts/views.py:134
#: accounts/views.py:135
msgid "Add SSH key"
msgstr "SSH Anahtarı Ekle"
#: accounts/views.py:155
#: accounts/views.py:156
msgid "SSH key deleted"
msgstr "SSH anahtarı silindi"
#: accounts/views.py:163
#: accounts/views.py:164
msgid "Delete SSH key"
msgstr "SSH anahtarı sil"
#: accounts/views.py:164
#: accounts/views.py:165
msgid ""
"VMs that already have it keep it; VMs created from now on do not get it."
msgstr ""
"Ona zaten sahip olan sanal makinelerde kalır; bundan sonra oluşturulan sanal "
"makinelere eklenmez."
#: accounts/views.py:212
#: accounts/views.py:213
msgid "Password Changed"
msgstr "Parola Değişti"
#: accounts/views.py:241
#: accounts/views.py:242
msgid "Create User Instance"
msgstr "Yeni Kullanıcı Sanal Makinesi Oluştur"
#: accounts/views.py:275
#: accounts/views.py:276
msgid "Update User Instance"
msgstr "Kullanıcı Sanal Makinesini Güncelle"
#: accounts/views.py:293
#: accounts/views.py:294
msgid "The user loses access to this VM; the VM itself is not changed."
msgstr ""
"Kullanıcı bu sanal makineye erişimini kaybeder; sanal makinenin kendisi "
"değişmez."
#: accounts/views.py:326
#: accounts/views.py:327
#, python-format
msgid "OTP Sent to %(email)s"
msgstr "OTP %(email)s ye gönderildi"
#: accounts/views.py:335
#: accounts/views.py:336
msgid "Email OTP"
msgstr "OTP E-Postası"
#: accounts/views.py:350
#: accounts/views.py:370
#, python-format
msgid "The OTP QR code could not be mailed to %(user)s: %(error)s"
msgstr "OTP QR kodu %(user)s kullanıcısına e-postayla gönderilemedi: %(error)s"
#: accounts/views.py:354
#: accounts/views.py:374
#, python-format
msgid "OTP QR code was emailed to user %(user)s"
msgstr "OTP QR kodu %(user)s kullanıcısına gönderildi"
#: accounts/views.py:358
#: accounts/views.py:378
msgid "User email not set, failed to send QR code"
msgstr "Kullanıcı e-postası girilmemiş, QR kod gönderimi başarısız"
@ -2699,57 +2699,57 @@ msgstr ""
msgid "The name of the virtual machine must not contain any special characters"
msgstr "Sanal makine adı özel karakterler içeremez"
#: instances/jobs.py:68
#: instances/jobs.py:69
msgid "The VM moved to another compute after the job was queued"
msgstr "İş kuyruğa alındıktan sonra sanal makine başka bir sunucuya taşındı"
#: instances/jobs.py:136
#: instances/jobs.py:137
msgid "Enter a whole number of vCPUs for the clone"
msgstr "Klon için vCPU sayısını tam sayı olarak girin"
#: instances/jobs.py:138
#: instances/jobs.py:139
msgid "Enter a whole number of memory (MiB) for the clone"
msgstr "Klon için belleği (MiB) tam sayı olarak girin"
#: instances/jobs.py:165
#: instances/jobs.py:166
msgid "Choose the clone's name from the list of free names"
msgstr "Klonun adını boş adlar listesinden seçin"
#: instances/jobs.py:193
#: instances/jobs.py:194
#, python-format
msgid "%(quota)s: cannot create '%(clone_name)s'."
msgstr "%(quota)s: '%(clone_name)s' oluşturulamıyor."
#: instances/jobs.py:196 instances/jobs.py:545
#: instances/jobs.py:197 instances/jobs.py:546
#, python-format
msgid "Instance '%(clone_name)s' already exists!"
msgstr "'%(clone_name)s' sanal makinesi zaten mevcut!"
#: instances/jobs.py:198
#: instances/jobs.py:199
#, python-format
msgid "Instance name '%(clone_name)s' contains invalid characters!"
msgstr "Sanal makine adı '%(clone_name)s' geçersiz karakterler içermektedir!"
#: instances/jobs.py:201
#: instances/jobs.py:202
#, python-format
msgid "Instance MAC '%(clone_mac)s' invalid format!"
msgstr "Sanal makine MAC adresi '%(clone_mac)s' geçersiz biçemde belirtilmiş!"
#: instances/jobs.py:203
#: instances/jobs.py:204
#, python-format
msgid "Disk name '%(disk_name)s' contains invalid characters!"
msgstr "Disk adı '%(disk_name)s' geçersiz karakterler içeriyor!"
#: instances/jobs.py:223
#: instances/jobs.py:224
#, python-format
msgid "The clone is made, but not as asked: %(error)s"
msgstr "Klon oluşturuldu, ama istendiği gibi değil: %(error)s"
#: instances/jobs.py:279
#: instances/jobs.py:280
msgid "The VM was not defined and no volume of it was left."
msgstr "Sanal makine tanımlanmadı ve ondan geriye birim kalmadı."
#: instances/jobs.py:281
#: instances/jobs.py:282
#, python-format
msgid ""
"The VM was not defined; check these volumes, then resolve this job: "
@ -2758,28 +2758,28 @@ msgstr ""
"Sanal makine tanımlanmadı; şu birimleri kontrol edin, sonra bu işi çözün: "
"%(volumes)s"
#: instances/jobs.py:302
#: instances/jobs.py:303
msgid "Delete snapshot"
msgstr "Anlık görüntüyü sil"
#: instances/jobs.py:312 vrtManager/instance.py:2286
#: instances/jobs.py:313 vrtManager/instance.py:2286
#, python-format
msgid "The VM has no external snapshot named %(name)s"
msgstr "Sanal makinede %(name)s adında harici anlık görüntü yok"
#: instances/jobs.py:325
#: instances/jobs.py:326
msgid "Merging the snapshot"
msgstr "Anlık görüntü birleştiriliyor"
#: instances/jobs.py:344
#: instances/jobs.py:345
msgid "The VM is no longer on its compute"
msgstr "Sanal makine artık kendi sunucusunda değil"
#: instances/jobs.py:347
#: instances/jobs.py:348
msgid "libvirt is still merging the snapshot"
msgstr "libvirt anlık görüntüyü hâlâ birleştiriyor"
#: instances/jobs.py:355
#: instances/jobs.py:356
msgid ""
"The snapshot is still listed and no merge runs. It cannot be told whether the"
" disks changed; check the VM before deleting the snapshot again or reverting "
@ -2789,44 +2789,44 @@ msgstr ""
"değişmediği bilinemiyor; anlık görüntüyü yeniden silmeden ya da ona dönmeden "
"önce sanal makineyi kontrol edin."
#: instances/jobs.py:362
#: instances/jobs.py:363
msgid "The VM is no longer on the compute the job ran on"
msgstr "Sanal makine artık işin çalıştığı sunucuda değil"
#: instances/jobs.py:368
#: instances/jobs.py:369
msgid "Take snapshot"
msgstr "Anlık görüntü al"
#: instances/jobs.py:380 vrtManager/instance.py:1882
#: instances/jobs.py:381 vrtManager/instance.py:1882
#, python-format
msgid "A snapshot named %(name)s already exists"
msgstr "%(name)s adında bir anlık görüntü zaten var"
#: instances/jobs.py:392
#: instances/jobs.py:393
msgid "Taking the snapshot"
msgstr "Anlık görüntü alınıyor"
#: instances/jobs.py:405
#: instances/jobs.py:406
msgid "libvirt is still taking the snapshot"
msgstr "libvirt anlık görüntüyü hâlâ alıyor"
#: instances/jobs.py:408
#: instances/jobs.py:409
msgid "The snapshot was not taken"
msgstr "Anlık görüntü alınmadı"
#: instances/jobs.py:431
#: instances/jobs.py:432
msgid "Revert snapshot"
msgstr "Snapshota dön"
#: instances/jobs.py:462
#: instances/jobs.py:463
msgid "Reverting the VM"
msgstr "Sanal makine geri döndürülüyor"
#: instances/jobs.py:473
#: instances/jobs.py:474
msgid "libvirt is still reverting the VM"
msgstr "libvirt sanal makineyi hâlâ geri döndürüyor"
#: instances/jobs.py:477
#: instances/jobs.py:478
msgid ""
"It cannot be told whether the VM was reverted; check it, then revert again or"
" resolve this job."
@ -2834,12 +2834,12 @@ msgstr ""
"Sanal makinenin geri döndürülüp döndürülmediği bilinemiyor; kontrol edin, "
"sonra yeniden geri döndürün ya da bu işi çözün."
#: instances/jobs.py:494
#: instances/jobs.py:495
msgid "The VM must be running, paused or shut off to be cloned"
msgstr ""
"Klonlanmak için sanal makine çalışıyor, duraklatılmış ya da kapalı olmalı"
#: instances/jobs.py:495
#: instances/jobs.py:496
#, python-format
msgid ""
"The guest's file systems could not be thawed (%(error)s); thaw them with: "
@ -2848,19 +2848,19 @@ msgstr ""
"Konuğun dosya sistemleri çözülemedi (%(error)s); şununla çözün: virsh "
"domfsthaw %(vm)s"
#: instances/jobs.py:514
#: instances/jobs.py:515
msgid "Clone VM"
msgstr "Sanal makineyi klonla"
#: instances/jobs.py:555 instances/jobs.py:562 instances/jobs.py:830
#: instances/jobs.py:556 instances/jobs.py:563 instances/jobs.py:831
msgid "Copying disks"
msgstr "Diskler kopyalanıyor"
#: instances/jobs.py:614
#: instances/jobs.py:615
msgid "libvirt has no result of the copy"
msgstr "libvirt'te kopyanın sonucu yok"
#: instances/jobs.py:625
#: instances/jobs.py:626
#, python-format
msgid ""
"The copy failed: %(error)s. The guest's file systems could not be thawed "
@ -2869,11 +2869,11 @@ msgstr ""
"Kopyalama başarısız oldu: %(error)s. Konuğun dosya sistemleri çözülemedi "
"(%(thaw)s); şununla çözün: virsh domfsthaw %(vm)s"
#: instances/jobs.py:628 instances/jobs.py:712
#: instances/jobs.py:629 instances/jobs.py:713
msgid "The copy failed"
msgstr "Kopyalama başarısız oldu"
#: instances/jobs.py:653
#: instances/jobs.py:654
#, python-format
msgid ""
"The VM is no longer on the compute the job ran on; check these volumes, then "
@ -2882,7 +2882,7 @@ msgstr ""
"Sanal makine artık işin çalıştığı sunucuda değil; şu birimleri kontrol edin, "
"sonra bu işi çözün: %(volumes)s"
#: instances/jobs.py:688
#: instances/jobs.py:689
#, python-format
msgid ""
"A volume of the clone was being created; check these volumes, then resolve "
@ -2891,15 +2891,15 @@ msgstr ""
"Klonun bir birimi oluşturuluyordu; şu birimleri kontrol edin, sonra bu işi "
"çözün: %(volumes)s"
#: instances/jobs.py:692
#: instances/jobs.py:693
msgid "The job stopped before the copy started"
msgstr "İş, kopyalama başlamadan durdu"
#: instances/jobs.py:699
#: instances/jobs.py:700
msgid "libvirt is still copying the disks"
msgstr "libvirt diskleri hâlâ kopyalıyor"
#: instances/jobs.py:702
#: instances/jobs.py:703
#, python-format
msgid ""
"Another backup runs on the VM; check these volumes, then resolve this job: "
@ -2908,7 +2908,7 @@ msgstr ""
"Sanal makinede başka bir yedekleme çalışıyor; şu birimleri kontrol edin, "
"sonra bu işi çözün: %(volumes)s"
#: instances/jobs.py:707
#: instances/jobs.py:708
#, python-format
msgid ""
"libvirt has no result of the copy; check these volumes, then resolve this "
@ -2917,140 +2917,140 @@ msgstr ""
"libvirt'te kopyanın sonucu yok; şu birimleri kontrol edin, sonra bu işi "
"çözün: %(volumes)s"
#: instances/jobs.py:735 instances/templates/create_instance_w1.html:60
#: instances/jobs.py:736 instances/templates/create_instance_w1.html:60
#: instances/templates/create_instance_w2.html:356
msgid "Create VM"
msgstr "Sanal makine oluştur"
#: instances/jobs.py:759 instances/jobs.py:804 instances/views.py:2491
#: instances/jobs.py:760 instances/jobs.py:805 instances/views.py:2491
msgid "A virtual machine with this name already exists"
msgstr "Bu isimde bir sanal makine mevcut"
#: instances/jobs.py:761 instances/jobs.py:801
#: instances/jobs.py:762 instances/jobs.py:802
msgid "There is an instance with same name. Remove it and try again!"
msgstr "Aynı isimde başka bir sanal makine var. Onu silip yeniden deneyin!"
#: instances/jobs.py:764
#: instances/jobs.py:765
msgid "First you need to create or select an image"
msgstr "Öncelikle bir imaj oluşturun ya da mevcut bir imajı seçin"
#: instances/jobs.py:766
#: instances/jobs.py:767
msgid "Choose one disk source: a new disk, existing volumes or a template"
msgstr ""
"Tek bir disk kaynağı seçin: yeni bir disk, mevcut birimler ya da bir şablon"
#: instances/jobs.py:768
#: instances/jobs.py:769
msgid "Image has already exist. Please check volumes or change instance name"
msgstr ""
"İmaj zaten mevcut. Lütfen ya makine adını değişin ya da disk alanlarını "
"kontrol edin"
#: instances/jobs.py:770
#: instances/jobs.py:771
msgid "Invalid cache mode"
msgstr "Geçersiz önbellek modu"
#: instances/jobs.py:772
#: instances/jobs.py:773
#, python-format
msgid "This host offers no CPU mode or model %(mode)s"
msgstr "Bu sunucu %(mode)s CPU modunu veya modelini sunmuyor"
#: instances/jobs.py:774
#: instances/jobs.py:775
#, python-format
msgid "A CD-ROM cannot use the %(bus)s bus"
msgstr "CD-ROM %(bus)s veri yolunu kullanamaz"
#: instances/jobs.py:777
#: instances/jobs.py:778
msgid "The cloud-init seed is read from a CD-ROM: choose its bus"
msgstr "cloud-init tohumu bir CD-ROM'dan okunur: veri yolunu seçin"
#: instances/jobs.py:780
#: instances/jobs.py:781
msgid "The cloud-init seed needs a directory storage pool for the disk"
msgstr "cloud-init tohumu, disk için bir dizin depolama havuzu gerektirir"
#: instances/jobs.py:786
#: instances/jobs.py:787
#, python-format
msgid "This host offers no firmware %(firmware)s"
msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor"
#: instances/jobs.py:788 vrtManager/instance.py:3010
#: instances/jobs.py:789 vrtManager/instance.py:3010
msgid "This host offers no TPM 2.0: install swtpm on it"
msgstr "Bu sunucu TPM 2.0 sunmuyor: üzerine swtpm kurun"
#: instances/jobs.py:849
#: instances/jobs.py:850
msgid "Writing the cloud-init seed"
msgstr "cloud-init tohumu yazılıyor"
#: instances/jobs.py:856
#: instances/jobs.py:857
msgid "Defining the VM"
msgstr "Sanal makine tanımlanıyor"
#: instances/jobs.py:889
#: instances/jobs.py:890
#, python-format
msgid "%(error)s. Remove these volumes by hand: %(volumes)s"
msgstr "%(error)s. Şu birimleri elle silin: %(volumes)s"
#: instances/jobs.py:914
#: instances/jobs.py:915
#, python-format
msgid "The instance is already on %(compute)s"
msgstr "Sanal makine zaten %(compute)s üzerinde"
#: instances/jobs.py:916
#: instances/jobs.py:917
#, python-format
msgid "%(compute)s is not reachable"
msgstr "%(compute)s sunucusuna ulaşılamıyor"
#: instances/jobs.py:918
#: instances/jobs.py:919
#, python-format
msgid "%(compute)s is the VM's own host under another name"
msgstr "%(compute)s, sanal makinenin kendi sunucusunun başka bir adı"
#: instances/jobs.py:936
#: instances/jobs.py:937
msgid "Migrate VM"
msgstr "Sanal makineyi taşı"
#: instances/jobs.py:980
#: instances/jobs.py:981
msgid "Migrating the VM"
msgstr "Sanal makine taşınıyor"
#: instances/jobs.py:1027
#: instances/jobs.py:1028
msgid "libvirt is still migrating the VM"
msgstr "libvirt sanal makineyi hâlâ taşıyor"
#: instances/jobs.py:1034
#: instances/jobs.py:1035
msgid "The VM stayed on the source host"
msgstr "Sanal makine kaynak sunucuda kaldı"
#: instances/jobs.py:1036
#: instances/jobs.py:1037
msgid ""
"The VM is on both computes or on neither; check them, then resolve this job."
msgstr ""
"Sanal makine ya iki sunucuda da var ya da hiçbirinde yok; kontrol edin, sonra"
" bu işi çözün."
#: instances/jobs.py:1068
#: instances/jobs.py:1085
msgid "Move disk"
msgstr "Diski taşı"
#: instances/jobs.py:1075
#: instances/jobs.py:1092
msgid "Choose a disk of the VM"
msgstr "Sanal makinenin bir diskini seçin"
#: instances/jobs.py:1099
#: instances/jobs.py:1116
msgid "Copying the disk"
msgstr "Disk kopyalanıyor"
#: instances/jobs.py:1142
#: instances/jobs.py:1159
#, python-format
msgid "Another block job runs on %(dev)s; check the VM"
msgstr ""
"%(dev)s üzerinde başka bir blok işi çalışıyor; sanal makineyi kontrol edin"
#: instances/jobs.py:1148
#: instances/jobs.py:1165
#, python-format
msgid "The disk stayed in %(path)s; remove %(dest)s if it is there"
msgstr "Disk %(path)s içinde kaldı; varsa %(dest)s dosyasını silin"
#: instances/jobs.py:1150
#: instances/jobs.py:1167
#, python-format
msgid "The definitions of %(dev)s name %(paths)s; check the VM"
msgstr "%(dev)s tanımları %(paths)s gösteriyor; sanal makineyi kontrol edin"

Binary file not shown.

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-11-05 13:37+0000\n"
"POT-Creation-Date: 2026-11-05 14:27+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@ -311,19 +311,6 @@ msgstr "取消"
msgid "Change password"
msgstr "修改密码"
#: accounts/templates/accounts/email/otp.html:2
#, python-format
msgid ""
"\n"
"Scan this QR code to get OTP for account '%(user)s'\n"
msgstr ""
"\n"
"扫描此二维码获取账户“%(user)s”的 OTP\n"
#: accounts/templates/accounts/email/otp.html:7
msgid "Some e-mail clients does not render SVG, also generating PNG."
msgstr "部分邮件客户端无法显示 SVG,因此同时生成了 PNG。"
#: accounts/templates/accounts/email_otp_form.html:12
msgid ""
"\n"
@ -405,6 +392,16 @@ msgstr "验证并登录"
msgid "2FA Protection Active"
msgstr "双因素保护已启用"
#: accounts/templates/accounts/otp_qr.html:4
#: accounts/templates/accounts/otp_qr.html:6 accounts/utils.py:70
msgid "OTP QR Code"
msgstr "OTP 二维码"
#: accounts/templates/accounts/otp_qr.html:11
#, python-format
msgid "Scan this QR code to get OTP for account '%(user)s'"
msgstr "扫描此二维码以获取账户“%(user)s”的 OTP"
#: accounts/templates/api_token_created.html:4
#: accounts/templates/api_token_created.html:5
msgid "API token created"
@ -520,7 +517,7 @@ msgid "Add"
msgstr "添加"
#: accounts/templates/profile.html:64 accounts/templates/profile.html:70
#: accounts/views.py:69 accounts/views.py:73
#: accounts/views.py:70 accounts/views.py:74
msgid "Create an API token"
msgstr "创建 API 令牌"
@ -548,92 +545,95 @@ msgstr "SSH 密钥 %(name)s 的操作"
msgid "No SSH keys."
msgstr "没有 SSH 密钥。"
#: accounts/utils.py:65
msgid "OTP QR Code"
msgstr "OTP 二维码"
#: accounts/utils.py:71
#, python-format
msgid ""
"Open this link within an hour to see the OTP QR code of account '%(user)s':\n"
"\n"
"%(url)s"
msgstr ""
"请在一小时内打开此链接,查看账户“%(user)s”的 OTP 二维码:\n"
"\n"
"%(url)s"
#: accounts/utils.py:66
msgid "Please view HTML version of this message."
msgstr "请查看此邮件的 HTML 版本。"
#: accounts/views.py:46
#: accounts/views.py:47
msgid "Profile updated"
msgstr "资料已更新"
#: accounts/views.py:72
#: accounts/views.py:73
#, python-format
msgid "A user has at most %(limit)s API tokens: delete one first."
msgstr "每个用户最多有 %(limit)s 个 API 令牌:请先删除一个。"
#: accounts/views.py:107
#: accounts/views.py:108
msgid "API token deleted"
msgstr "API 令牌已删除"
#: accounts/views.py:115
#: accounts/views.py:116
msgid "Delete API token"
msgstr "删除 API 令牌"
#: accounts/views.py:116
#: accounts/views.py:117
msgid "Scripts and tools that use it can no longer reach the API."
msgstr "使用它的脚本和工具将无法再访问 API。"
#: accounts/views.py:126
#: accounts/views.py:127
msgid "SSH key added"
msgstr "SSH 密钥已添加"
#: accounts/views.py:134
#: accounts/views.py:135
msgid "Add SSH key"
msgstr "添加 SSH 密钥"
#: accounts/views.py:155
#: accounts/views.py:156
msgid "SSH key deleted"
msgstr "SSH 密钥已删除"
#: accounts/views.py:163
#: accounts/views.py:164
msgid "Delete SSH key"
msgstr "删除 SSH 密钥"
#: accounts/views.py:164
#: accounts/views.py:165
msgid ""
"VMs that already have it keep it; VMs created from now on do not get it."
msgstr "已有此密钥的虚拟机会保留它;此后创建的虚拟机不会再获得它。"
#: accounts/views.py:212
#: accounts/views.py:213
msgid "Password Changed"
msgstr "密码已修改"
#: accounts/views.py:241
#: accounts/views.py:242
msgid "Create User Instance"
msgstr "创建用户虚拟机"
#: accounts/views.py:275
#: accounts/views.py:276
msgid "Update User Instance"
msgstr "更新用户虚拟机"
#: accounts/views.py:293
#: accounts/views.py:294
msgid "The user loses access to this VM; the VM itself is not changed."
msgstr "该用户将失去对此虚拟机的访问权限;虚拟机本身不受影响。"
#: accounts/views.py:326
#: accounts/views.py:327
#, python-format
msgid "OTP Sent to %(email)s"
msgstr "OTP 已发送至 %(email)s"
#: accounts/views.py:335
#: accounts/views.py:336
msgid "Email OTP"
msgstr "邮件发送 OTP"
#: accounts/views.py:350
#: accounts/views.py:370
#, python-format
msgid "The OTP QR code could not be mailed to %(user)s: %(error)s"
msgstr "无法将 OTP 二维码通过邮件发送给 %(user)s:%(error)s"
#: accounts/views.py:354
#: accounts/views.py:374
#, python-format
msgid "OTP QR code was emailed to user %(user)s"
msgstr "OTP 二维码已通过邮件发送给用户 %(user)s"
#: accounts/views.py:358
#: accounts/views.py:378
msgid "User email not set, failed to send QR code"
msgstr "未设置用户邮箱,无法发送二维码"
@ -2604,332 +2604,332 @@ msgstr "控制台监听的不只是 localhost;只有管理员可以移除其
msgid "The name of the virtual machine must not contain any special characters"
msgstr "虚拟机名称不能包含任何特殊字符"
#: instances/jobs.py:68
#: instances/jobs.py:69
msgid "The VM moved to another compute after the job was queued"
msgstr "任务排队后,虚拟机已移到另一个计算节点"
#: instances/jobs.py:136
#: instances/jobs.py:137
msgid "Enter a whole number of vCPUs for the clone"
msgstr "请为克隆输入整数个 vCPU"
#: instances/jobs.py:138
#: instances/jobs.py:139
msgid "Enter a whole number of memory (MiB) for the clone"
msgstr "请为克隆输入整数的内存(MiB)"
#: instances/jobs.py:165
#: instances/jobs.py:166
msgid "Choose the clone's name from the list of free names"
msgstr "请从可用名称列表中选择克隆的名称"
#: instances/jobs.py:193
#: instances/jobs.py:194
#, python-format
msgid "%(quota)s: cannot create '%(clone_name)s'."
msgstr "%(quota)s:无法创建“%(clone_name)s”。"
#: instances/jobs.py:196 instances/jobs.py:545
#: instances/jobs.py:197 instances/jobs.py:546
#, python-format
msgid "Instance '%(clone_name)s' already exists!"
msgstr "虚拟机 '%(clone_name)s' 已存在!"
#: instances/jobs.py:198
#: instances/jobs.py:199
#, python-format
msgid "Instance name '%(clone_name)s' contains invalid characters!"
msgstr "虚拟机名称 '%(clone_name)s' 包含无效字符!"
#: instances/jobs.py:201
#: instances/jobs.py:202
#, python-format
msgid "Instance MAC '%(clone_mac)s' invalid format!"
msgstr "虚拟机 MAC '%(clone_mac)s' 格式无效!"
#: instances/jobs.py:203
#: instances/jobs.py:204
#, python-format
msgid "Disk name '%(disk_name)s' contains invalid characters!"
msgstr "磁盘名称 '%(disk_name)s' 包含无效字符!"
#: instances/jobs.py:223
#: instances/jobs.py:224
#, python-format
msgid "The clone is made, but not as asked: %(error)s"
msgstr "克隆已创建,但与要求不符:%(error)s"
#: instances/jobs.py:279
#: instances/jobs.py:280
msgid "The VM was not defined and no volume of it was left."
msgstr "虚拟机未定义,也没有留下它的卷。"
#: instances/jobs.py:281
#: instances/jobs.py:282
#, python-format
msgid ""
"The VM was not defined; check these volumes, then resolve this job: "
"%(volumes)s"
msgstr "虚拟机未定义;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:302
#: instances/jobs.py:303
msgid "Delete snapshot"
msgstr "删除快照"
#: instances/jobs.py:312 vrtManager/instance.py:2286
#: instances/jobs.py:313 vrtManager/instance.py:2286
#, python-format
msgid "The VM has no external snapshot named %(name)s"
msgstr "虚拟机没有名为 %(name)s 的外部快照"
#: instances/jobs.py:325
#: instances/jobs.py:326
msgid "Merging the snapshot"
msgstr "正在合并快照"
#: instances/jobs.py:344
#: instances/jobs.py:345
msgid "The VM is no longer on its compute"
msgstr "虚拟机已不在其计算节点上"
#: instances/jobs.py:347
#: instances/jobs.py:348
msgid "libvirt is still merging the snapshot"
msgstr "libvirt 仍在合并快照"
#: instances/jobs.py:355
#: instances/jobs.py:356
msgid ""
"The snapshot is still listed and no merge runs. It cannot be told whether the"
" disks changed; check the VM before deleting the snapshot again or reverting "
"to it."
msgstr "快照仍在列表中,且没有正在运行的合并。无法判断磁盘是否已更改;再次删除该快照或恢复到它之前,请检查虚拟机。"
#: instances/jobs.py:362
#: instances/jobs.py:363
msgid "The VM is no longer on the compute the job ran on"
msgstr "虚拟机已不在运行该任务的计算节点上"
#: instances/jobs.py:368
#: instances/jobs.py:369
msgid "Take snapshot"
msgstr "创建快照"
#: instances/jobs.py:380 vrtManager/instance.py:1882
#: instances/jobs.py:381 vrtManager/instance.py:1882
#, python-format
msgid "A snapshot named %(name)s already exists"
msgstr "名为 %(name)s 的快照已存在"
#: instances/jobs.py:392
#: instances/jobs.py:393
msgid "Taking the snapshot"
msgstr "正在创建快照"
#: instances/jobs.py:405
#: instances/jobs.py:406
msgid "libvirt is still taking the snapshot"
msgstr "libvirt 仍在创建快照"
#: instances/jobs.py:408
#: instances/jobs.py:409
msgid "The snapshot was not taken"
msgstr "未创建快照"
#: instances/jobs.py:431
#: instances/jobs.py:432
msgid "Revert snapshot"
msgstr "恢复快照"
#: instances/jobs.py:462
#: instances/jobs.py:463
msgid "Reverting the VM"
msgstr "正在恢复虚拟机"
#: instances/jobs.py:473
#: instances/jobs.py:474
msgid "libvirt is still reverting the VM"
msgstr "libvirt 仍在恢复虚拟机"
#: instances/jobs.py:477
#: instances/jobs.py:478
msgid ""
"It cannot be told whether the VM was reverted; check it, then revert again or"
" resolve this job."
msgstr "无法判断虚拟机是否已恢复;请检查后再次恢复,或处理此任务。"
#: instances/jobs.py:494
#: instances/jobs.py:495
msgid "The VM must be running, paused or shut off to be cloned"
msgstr "虚拟机必须处于运行、暂停或关机状态才能克隆"
#: instances/jobs.py:495
#: instances/jobs.py:496
#, python-format
msgid ""
"The guest's file systems could not be thawed (%(error)s); thaw them with: "
"virsh domfsthaw %(vm)s"
msgstr "无法解冻客户机的文件系统(%(error)s);请用以下命令解冻:virsh domfsthaw %(vm)s"
#: instances/jobs.py:514
#: instances/jobs.py:515
msgid "Clone VM"
msgstr "克隆虚拟机"
#: instances/jobs.py:555 instances/jobs.py:562 instances/jobs.py:830
#: instances/jobs.py:556 instances/jobs.py:563 instances/jobs.py:831
msgid "Copying disks"
msgstr "正在复制磁盘"
#: instances/jobs.py:614
#: instances/jobs.py:615
msgid "libvirt has no result of the copy"
msgstr "libvirt 没有复制的结果"
#: instances/jobs.py:625
#: instances/jobs.py:626
#, python-format
msgid ""
"The copy failed: %(error)s. The guest's file systems could not be thawed "
"(%(thaw)s); thaw them with: virsh domfsthaw %(vm)s"
msgstr "复制失败:%(error)s。无法解冻客户机的文件系统(%(thaw)s);请用以下命令解冻:virsh domfsthaw %(vm)s"
#: instances/jobs.py:628 instances/jobs.py:712
#: instances/jobs.py:629 instances/jobs.py:713
msgid "The copy failed"
msgstr "复制失败"
#: instances/jobs.py:653
#: instances/jobs.py:654
#, python-format
msgid ""
"The VM is no longer on the compute the job ran on; check these volumes, then "
"resolve this job: %(volumes)s"
msgstr "虚拟机已不在运行该任务的计算节点上;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:688
#: instances/jobs.py:689
#, python-format
msgid ""
"A volume of the clone was being created; check these volumes, then resolve "
"this job: %(volumes)s"
msgstr "克隆的一个卷正在创建中;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:692
#: instances/jobs.py:693
msgid "The job stopped before the copy started"
msgstr "任务在复制开始前停止"
#: instances/jobs.py:699
#: instances/jobs.py:700
msgid "libvirt is still copying the disks"
msgstr "libvirt 仍在复制磁盘"
#: instances/jobs.py:702
#: instances/jobs.py:703
#, python-format
msgid ""
"Another backup runs on the VM; check these volumes, then resolve this job: "
"%(volumes)s"
msgstr "虚拟机上正在运行另一个备份;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:707
#: instances/jobs.py:708
#, python-format
msgid ""
"libvirt has no result of the copy; check these volumes, then resolve this "
"job: %(volumes)s"
msgstr "libvirt 没有复制的结果;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:735 instances/templates/create_instance_w1.html:60
#: instances/jobs.py:736 instances/templates/create_instance_w1.html:60
#: instances/templates/create_instance_w2.html:356
msgid "Create VM"
msgstr "创建虚拟机"
#: instances/jobs.py:759 instances/jobs.py:804 instances/views.py:2491
#: instances/jobs.py:760 instances/jobs.py:805 instances/views.py:2491
msgid "A virtual machine with this name already exists"
msgstr "同名虚拟机已存在"
#: instances/jobs.py:761 instances/jobs.py:801
#: instances/jobs.py:762 instances/jobs.py:802
msgid "There is an instance with same name. Remove it and try again!"
msgstr "已存在同名虚拟机。请删除后重试!"
#: instances/jobs.py:764
#: instances/jobs.py:765
msgid "First you need to create or select an image"
msgstr "请先创建或选择一个镜像"
#: instances/jobs.py:766
#: instances/jobs.py:767
msgid "Choose one disk source: a new disk, existing volumes or a template"
msgstr "请选择一种磁盘来源:新磁盘、现有卷或模板"
#: instances/jobs.py:768
#: instances/jobs.py:769
msgid "Image has already exist. Please check volumes or change instance name"
msgstr "镜像已存在。请检查卷或更改虚拟机名称"
#: instances/jobs.py:770
#: instances/jobs.py:771
msgid "Invalid cache mode"
msgstr "无效的缓存模式"
#: instances/jobs.py:772
#: instances/jobs.py:773
#, python-format
msgid "This host offers no CPU mode or model %(mode)s"
msgstr "此主机不提供 CPU 模式或型号 %(mode)s"
#: instances/jobs.py:774
#: instances/jobs.py:775
#, python-format
msgid "A CD-ROM cannot use the %(bus)s bus"
msgstr "CD-ROM 不能使用 %(bus)s 总线"
#: instances/jobs.py:777
#: instances/jobs.py:778
msgid "The cloud-init seed is read from a CD-ROM: choose its bus"
msgstr "cloud-init 种子从 CD-ROM 读取:请选择其总线"
#: instances/jobs.py:780
#: instances/jobs.py:781
msgid "The cloud-init seed needs a directory storage pool for the disk"
msgstr "cloud-init 种子需要为磁盘使用目录类型的存储池"
#: instances/jobs.py:786
#: instances/jobs.py:787
#, python-format
msgid "This host offers no firmware %(firmware)s"
msgstr "此主机不提供固件 %(firmware)s"
#: instances/jobs.py:788 vrtManager/instance.py:3010
#: instances/jobs.py:789 vrtManager/instance.py:3010
msgid "This host offers no TPM 2.0: install swtpm on it"
msgstr "此主机不提供 TPM 2.0:请在主机上安装 swtpm"
#: instances/jobs.py:849
#: instances/jobs.py:850
msgid "Writing the cloud-init seed"
msgstr "正在写入 cloud-init 种子"
#: instances/jobs.py:856
#: instances/jobs.py:857
msgid "Defining the VM"
msgstr "正在定义虚拟机"
#: instances/jobs.py:889
#: instances/jobs.py:890
#, python-format
msgid "%(error)s. Remove these volumes by hand: %(volumes)s"
msgstr "%(error)s。请手动删除这些卷:%(volumes)s"
#: instances/jobs.py:914
#: instances/jobs.py:915
#, python-format
msgid "The instance is already on %(compute)s"
msgstr "虚拟机已在 %(compute)s 上"
#: instances/jobs.py:916
#: instances/jobs.py:917
#, python-format
msgid "%(compute)s is not reachable"
msgstr "无法连接 %(compute)s"
#: instances/jobs.py:918
#: instances/jobs.py:919
#, python-format
msgid "%(compute)s is the VM's own host under another name"
msgstr "%(compute)s 是该虚拟机所在主机的另一个名称"
#: instances/jobs.py:936
#: instances/jobs.py:937
msgid "Migrate VM"
msgstr "迁移虚拟机"
#: instances/jobs.py:980
#: instances/jobs.py:981
msgid "Migrating the VM"
msgstr "正在迁移虚拟机"
#: instances/jobs.py:1027
#: instances/jobs.py:1028
msgid "libvirt is still migrating the VM"
msgstr "libvirt 仍在迁移虚拟机"
#: instances/jobs.py:1034
#: instances/jobs.py:1035
msgid "The VM stayed on the source host"
msgstr "虚拟机留在了源主机上"
#: instances/jobs.py:1036
#: instances/jobs.py:1037
msgid ""
"The VM is on both computes or on neither; check them, then resolve this job."
msgstr "虚拟机同时位于两个计算节点上,或都不在;请检查后处理此任务。"
#: instances/jobs.py:1068
#: instances/jobs.py:1085
msgid "Move disk"
msgstr "移动磁盘"
#: instances/jobs.py:1075
#: instances/jobs.py:1092
msgid "Choose a disk of the VM"
msgstr "请选择虚拟机的一个磁盘"
#: instances/jobs.py:1099
#: instances/jobs.py:1116
msgid "Copying the disk"
msgstr "正在复制磁盘"
#: instances/jobs.py:1142
#: instances/jobs.py:1159
#, python-format
msgid "Another block job runs on %(dev)s; check the VM"
msgstr "%(dev)s 上正在运行另一个块任务;请检查虚拟机"
#: instances/jobs.py:1148
#: instances/jobs.py:1165
#, python-format
msgid "The disk stayed in %(path)s; remove %(dest)s if it is there"
msgstr "磁盘仍在 %(path)s;如果 %(dest)s 存在,请将其删除"
#: instances/jobs.py:1150
#: instances/jobs.py:1167
#, python-format
msgid "The definitions of %(dev)s name %(paths)s; check the VM"
msgstr "%(dev)s 的定义指向 %(paths)s;请检查虚拟机"

View file

@ -35,6 +35,7 @@ ALLOWED_FOR_ANY_USER = {
"instances:states": "the VM list's poll: only the user's own instances",
"accounts:login": "login page",
"accounts:email_otp": "part of the login flow (its own issues are tracked separately)",
"accounts:otp_qr": "the OTP QR code a mailed link opens: only with that signed, hour-old-at-most link",
"accounts:logout": "logout",
"accounts:profile": "the user's own profile",
"accounts:change_password": "the user's own password; new users get the permission by default",
@ -64,6 +65,7 @@ ALLOWED_FOR_ANY_USER = {
ALLOWED_FOR_ANONYMOUS = {
"accounts:login": "login page",
"accounts:email_otp": "part of the login flow (its own issues are tracked separately)",
"accounts:otp_qr": "the OTP QR code a mailed link opens: only with that signed, hour-old-at-most link",
"ds_openstack_index": "cloud-init datasource, fetched by VMs that do not log in",
"ds_openstack_metadata": "cloud-init datasource, fetched by VMs that do not log in",
"ds_openstack_userdata": "cloud-init datasource, fetched by VMs that do not log in",