1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(quota): a snapshot revert must fit the quota

A revert gives the VM the vCPUs and memory it had when the snapshot was
taken. An owner could take a snapshot, shrink the VM, give the freed share
to another VM and revert, ending over the quota. The revert now checks
what it adds to the VM (against the larger of its running and next-start
sizes) as a resize does: on the page when it is queued, and again in the
worker before it runs, as a resize may have used the quota meanwhile.
This commit is contained in:
catborise 2026-11-07 15:58:44 +03:00
parent aab8dbf1df
commit d8a184eff7
7 changed files with 231 additions and 123 deletions

View file

@ -434,6 +434,28 @@ def _snapshot_definitions(proxy, params):
return any(definition.xpath(HOST_DEVICES) for definition in definitions), definitions
def _revert_quota(user, instance, proxy, params):
"""A revert gives the VM the vCPUs and memory it had then: what that adds
to what it has now (running or at its next start) must fit the user's
quota, as a resize must."""
if user.is_superuser or instance.is_template:
return
sizes = [(d.findtext("vcpu"), d.findtext("memory")) for d in _snapshot_definitions(proxy, params)[1]]
if not sizes or any(None in size for size in sizes): # an old snapshot may record only the VM's UUID
raise util.OperationError(_("Snapshot %(name)s does not record the VM's vCPUs and memory: "
"the quota cannot be checked."), name=params["name"])
with proxy.cached_reads():
cpus = max(int(vcpu) for vcpu, _memory in sizes) - max(proxy.get_vcpu(), proxy.get_vcpu(config=True))
memory = (max(int(memory) // 1024 for _vcpu, memory in sizes)
- max(proxy.get_memory(), proxy.get_memory(config=True)))
quota = utils.check_user_quota(user, 0, cpus, memory, 0)
if quota == utils.QUOTA_UNVERIFIED:
raise util.OperationError(utils.QUOTA_UNVERIFIED_TEXT)
if quota:
raise util.OperationError(_("%(quota)s: cannot revert to snapshot %(name)s."),
quota=utils.quota_message(quota), name=params["name"])
@register
class RevertSnapshot(Kind):
name = "snapshot.revert"
@ -445,13 +467,21 @@ class RevertSnapshot(Kind):
"external": bool(raw.get("external"))}
def validate(self, user, params):
_proxy(_authorized(user, params["instance"]))._snapshot(params["name"], params["external"])
instance = _authorized(user, params["instance"])
proxy = _proxy(instance)
proxy._snapshot(params["name"], params["external"])
_revert_quota(user, instance, proxy, params)
def plan(self, user, params):
plan = _vm_plan(params)
instance = _instance(params["instance"])
# a revert that gives the VM host devices holds the whole compute: no other VM takes them meanwhile
if _snapshot_definitions(_proxy(_instance(params["instance"])), params)[0]:
if _snapshot_definitions(_proxy(instance), params)[0]:
plan.claims[claims.compute_key(plan.intent["compute"])] = Claim.EXCLUSIVE
# the user's quota until it ends: the worker's check and the revert are one step to other
# increases (a resize, another revert), as a resize's check and change are
if not user.is_superuser and not instance.is_template:
plan.claims[claims.quota_key(user.pk)] = Claim.EXCLUSIVE
return plan
def precheck(self, op, ctx):
@ -460,6 +490,8 @@ class RevertSnapshot(Kind):
proxy.check_external_revert(op.params["name"])
else:
proxy.check_internal_revert()
# again: a resize while it was queued may have used the quota
_revert_quota(op.user, _instance(op.params["instance"]), proxy, op.params)
# a revert gives the VM the host devices it had then: not one given since to another VM
has_hostdevs, definitions = _snapshot_definitions(proxy, op.params)
if has_hostdevs:

View file

@ -167,8 +167,8 @@ class RevertTests(TestCase):
def test_the_compute_is_held_only_for_a_revert_with_host_devices(self):
compute = f"compute:{self.instance.compute_id}"
self.snapshot(by_model())
self.assertEqual(self.kind.plan(None, self.params).claims[compute], "exclusive")
self.assertEqual(self.kind.plan(MagicMock(is_superuser=True), self.params).claims[compute], "exclusive")
self.snapshot("")
self.assertEqual(self.kind.plan(None, self.params).claims[compute], "shared")
self.assertEqual(self.kind.plan(MagicMock(is_superuser=True), self.params).claims[compute], "shared")
self.precheck() # no host device: the host's devices are not read
self.proxy.wvm.listAllDevices.assert_not_called()

View file

@ -1,14 +1,17 @@
"""Snapshot create and revert run as jobs: the page queues them, the worker calls libvirt."""
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import PropertyMock, patch
from vrtManager.util import OperationError
from django.contrib.auth import get_user_model
from django.test import TransactionTestCase
from django.urls import reverse
from accounts.models import UserAttributes, UserInstance
from appsettings.settings import get_settings
from django.contrib.auth.models import Permission
from django.contrib.messages import get_messages
from computes.models import Compute
from instances.jobs import CreateSnapshot, RevertSnapshot
from instances.models import Instance
@ -30,6 +33,10 @@ class SnapshotJobsTestCase(TransactionTestCase):
self.proxy._snapshot.return_value.getXMLDesc.return_value = "<domainsnapshot><domain/></domainsnapshot>"
self.proxy.instance.isActive.return_value = False
self.proxy.instance.hasCurrentSnapshot.return_value = 0
# the quota's reads of the user's VMs
reads = patch("instances.utils.wvmInstance")
self.vm_reads = reads.start().return_value
self.addCleanup(reads.stop)
def queue_and_run(self, view, data):
"""Post the form; nothing runs in the request; then run the job as the worker does."""
@ -95,6 +102,49 @@ class SnapshotJobsTestCase(TransactionTestCase):
self.assertEqual((op.kind, result), ("snapshot.revert", {"left_behind": ["/pool/a.one"]}))
self.proxy.revert_external_snapshot.assert_called_once_with("a")
def test_a_revert_that_would_exceed_the_owner_s_quota_is_refused(self):
# snapshot at 4 vCPUs, shrunk to 2, the 2 given to another VM: reverting gives 6 of 4
owner = get_user_model().objects.create_user("snap_owner", password="pw")
owner.user_permissions.add(Permission.objects.get(codename="snapshot_instances"))
UserAttributes.objects.update_or_create(user=owner, defaults={"max_cpus": 4, "max_memory": 2048,
"max_instances": -1, "max_disk_size": -1})
UserInstance.objects.create(user=owner, instance=self.instance, is_change=True)
for proxy in (self.proxy, self.vm_reads): # the VM now: 2 vCPUs, 1 GiB, running and at its next start
proxy.get_vcpu.return_value, proxy.get_memory.return_value = 2, 1024
proxy.get_disk_devices.return_value = []
other = Instance.objects.create(compute=self.instance.compute, name="snap-other", uuid="21212121-3434-5656-7878-909090909091")
UserInstance.objects.create(user=owner, instance=other, is_change=True)
patch.object(Compute, "status", new_callable=PropertyMock, return_value=True).start()
self.addCleanup(patch.stopall)
for vcpu, memory, admitted in ((4, 1024, False), (2, 2048, False), (2, 1024, True)):
with self.subTest(vcpu=vcpu, memory=memory):
self.client.force_login(owner) # a new session: no messages of the last one
self.proxy._snapshot.return_value.getXMLDesc.return_value = (
f"<domainsnapshot><domain><vcpu>{vcpu}</vcpu><memory>{memory * 1024}</memory></domain></domainsnapshot>")
response = self.client.post(reverse("instances:revert_snapshot", args=[self.instance.pk]),
{"name": "a", "submission_key": f"key-snap{vcpu}{memory}"})
self.assertEqual(Operation.objects.exists(), admitted)
shown = " ".join(str(m) for m in get_messages(response.wsgi_request))
self.assertEqual("cannot revert to snapshot a" in shown, not admitted, shown)
self.client.logout()
# the admitted revert holds the owner's quota until it ends: no resize or other revert
# passes the check meanwhile (its own check in the worker and its revert are one step)
op = Operation.objects.get()
self.assertIn((f"quota:user:{owner.pk}", "exclusive"), set(Claim.objects.filter(operation=op).values_list("resource", "mode")))
# the worker checks again: the VM may have been shrunk while it was queued (2 -> 1 vCPU, used elsewhere)
self.proxy.get_vcpu.return_value = 1
with self.assertRaisesMessage(OperationError, "cannot revert to snapshot a"):
RevertSnapshot().precheck(op, None)
# an old snapshot may record only the VM's UUID: refused, not a server error
Operation.objects.all().delete()
Claim.objects.all().delete()
self.proxy._snapshot.return_value.getXMLDesc.return_value = "<domainsnapshot><domain><uuid>x</uuid></domain></domainsnapshot>"
self.client.force_login(owner)
response = self.client.post(reverse("instances:revert_snapshot", args=[self.instance.pk]),
{"name": "a", "submission_key": "key-snapold1"})
self.assertFalse(Operation.objects.exists())
self.assertIn("does not record the VM's vCPUs and memory", " ".join(str(m) for m in get_messages(response.wsgi_request)))
def test_a_revert_libvirt_cannot_do_is_refused_before_effects(self):
self.proxy.check_external_revert.side_effect = OperationError("taken by an older version")
with self.assertRaises(OperationError):

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-07 12:30+0000\n"
"POT-Creation-Date: 2026-11-07 13:19+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"
@ -2750,7 +2750,7 @@ msgstr "Klonun adını boş adlar listesinden seçin"
msgid "%(quota)s: cannot create '%(clone_name)s'."
msgstr "%(quota)s: '%(clone_name)s' oluşturulamıyor."
#: instances/jobs.py:200 instances/jobs.py:554
#: instances/jobs.py:200 instances/jobs.py:586
#, python-format
msgid "Instance '%(clone_name)s' already exists!"
msgstr "'%(clone_name)s' sanal makinesi zaten mevcut!"
@ -2844,19 +2844,33 @@ msgstr "libvirt anlık görüntüyü hâlâ alıyor"
msgid "The snapshot was not taken"
msgstr "Anlık görüntü alınmadı"
#: instances/jobs.py:440
#: instances/jobs.py:445
#, python-format
msgid ""
"Snapshot %(name)s does not record the VM's vCPUs and memory: the quota cannot"
" be checked."
msgstr ""
"%(name)s anlık görüntüsü sanal makinenin vCPU ve bellek değerlerini "
"içermiyor: kota denetlenemiyor."
#: instances/jobs.py:455
#, python-format
msgid "%(quota)s: cannot revert to snapshot %(name)s."
msgstr "%(quota)s: %(name)s anlık görüntüsüne dönülemiyor."
#: instances/jobs.py:462
msgid "Revert snapshot"
msgstr "Snapshota dön"
#: instances/jobs.py:471
#: instances/jobs.py:503
msgid "Reverting the VM"
msgstr "Sanal makine geri döndürülüyor"
#: instances/jobs.py:482
#: instances/jobs.py:514
msgid "libvirt is still reverting the VM"
msgstr "libvirt sanal makineyi hâlâ geri döndürüyor"
#: instances/jobs.py:486
#: instances/jobs.py:518
msgid ""
"It cannot be told whether the VM was reverted; check it, then revert again or"
" resolve this job."
@ -2864,12 +2878,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:503
#: instances/jobs.py:535
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:504
#: instances/jobs.py:536
#, python-format
msgid ""
"The guest's file systems could not be thawed (%(error)s); thaw them with: "
@ -2878,19 +2892,19 @@ msgstr ""
"Konuğun dosya sistemleri çözülemedi (%(error)s); şununla çözün: virsh "
"domfsthaw %(vm)s"
#: instances/jobs.py:523
#: instances/jobs.py:555
msgid "Clone VM"
msgstr "Sanal makineyi klonla"
#: instances/jobs.py:564 instances/jobs.py:571 instances/jobs.py:849
#: instances/jobs.py:596 instances/jobs.py:603 instances/jobs.py:881
msgid "Copying disks"
msgstr "Diskler kopyalanıyor"
#: instances/jobs.py:623
#: instances/jobs.py:655
msgid "libvirt has no result of the copy"
msgstr "libvirt'te kopyanın sonucu yok"
#: instances/jobs.py:634
#: instances/jobs.py:666
#, python-format
msgid ""
"The copy failed: %(error)s. The guest's file systems could not be thawed "
@ -2899,11 +2913,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:637 instances/jobs.py:724
#: instances/jobs.py:669 instances/jobs.py:756
msgid "The copy failed"
msgstr "Kopyalama başarısız oldu"
#: instances/jobs.py:662
#: instances/jobs.py:694
#, python-format
msgid ""
"The VM is no longer on the compute the job ran on; check these volumes, then "
@ -2912,7 +2926,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:700
#: instances/jobs.py:732
#, python-format
msgid ""
"A volume of the clone was being created; check these volumes, then resolve "
@ -2921,15 +2935,15 @@ msgstr ""
"Klonun bir birimi oluşturuluyordu; şu birimleri kontrol edin, sonra bu işi "
"çözün: %(volumes)s"
#: instances/jobs.py:704
#: instances/jobs.py:736
msgid "The job stopped before the copy started"
msgstr "İş, kopyalama başlamadan durdu"
#: instances/jobs.py:711
#: instances/jobs.py:743
msgid "libvirt is still copying the disks"
msgstr "libvirt diskleri hâlâ kopyalıyor"
#: instances/jobs.py:714
#: instances/jobs.py:746
#, python-format
msgid ""
"Another backup runs on the VM; check these volumes, then resolve this job: "
@ -2938,7 +2952,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:719
#: instances/jobs.py:751
#, python-format
msgid ""
"libvirt has no result of the copy; check these volumes, then resolve this "
@ -2947,7 +2961,7 @@ msgstr ""
"libvirt'te kopyanın sonucu yok; şu birimleri kontrol edin, sonra bu işi "
"çözün: %(volumes)s"
#: instances/jobs.py:731
#: instances/jobs.py:763
#, python-format
msgid ""
"The disks were copied, but the clone could not be defined: %(error)s; check "
@ -2956,74 +2970,74 @@ msgstr ""
"Diskler kopyalandı, ancak klon tanımlanamadı: %(error)s; şu birimleri kontrol"
" edin, sonra bu işi çözün: %(volumes)s"
#: instances/jobs.py:754 instances/templates/create_instance_w1.html:60
#: instances/jobs.py:786 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:778 instances/jobs.py:823 instances/views.py:2474
#: instances/jobs.py:810 instances/jobs.py:855 instances/views.py:2474
msgid "A virtual machine with this name already exists"
msgstr "Bu isimde bir sanal makine mevcut"
#: instances/jobs.py:780 instances/jobs.py:820
#: instances/jobs.py:812 instances/jobs.py:852
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:783
#: instances/jobs.py:815
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:785
#: instances/jobs.py:817
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:787
#: instances/jobs.py:819
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:789
#: instances/jobs.py:821
msgid "Invalid cache mode"
msgstr "Geçersiz önbellek modu"
#: instances/jobs.py:791
#: instances/jobs.py:823
#, 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:793
#: instances/jobs.py:825
#, python-format
msgid "A CD-ROM cannot use the %(bus)s bus"
msgstr "CD-ROM %(bus)s veri yolunu kullanamaz"
#: instances/jobs.py:796
#: instances/jobs.py:828
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:799
#: instances/jobs.py:831
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:805
#: instances/jobs.py:837
#, python-format
msgid "This host offers no firmware %(firmware)s"
msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor"
#: instances/jobs.py:807 vrtManager/instance.py:3010
#: instances/jobs.py:839 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:868
#: instances/jobs.py:900
msgid "Writing the cloud-init seed"
msgstr "cloud-init tohumu yazılıyor"
#: instances/jobs.py:875
#: instances/jobs.py:907
msgid "Defining the VM"
msgstr "Sanal makine tanımlanıyor"
#: instances/jobs.py:908
#: instances/jobs.py:940
#, python-format
msgid ""
"The panel failed unexpectedly; its log has the details. Remove these volumes "
@ -3032,114 +3046,114 @@ msgstr ""
"Panel beklenmedik bir hatayla karşılaştı; ayrıntılar panelin günlüğünde. Bu "
"birimleri elle kaldırın: %(volumes)s"
#: instances/jobs.py:912
#: instances/jobs.py:944
#, python-format
msgid "%(error)s. Remove these volumes by hand: %(volumes)s"
msgstr "%(error)s. Şu birimleri elle silin: %(volumes)s"
#: instances/jobs.py:937
#: instances/jobs.py:969
#, python-format
msgid "The instance is already on %(compute)s"
msgstr "Sanal makine zaten %(compute)s üzerinde"
#: instances/jobs.py:939
#: instances/jobs.py:971
#, python-format
msgid "%(compute)s is not reachable"
msgstr "%(compute)s sunucusuna ulaşılamıyor"
#: instances/jobs.py:941
#: instances/jobs.py:973
#, 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:981
#: instances/jobs.py:1013
#, python-format
msgid ""
"The VM migrated, but autostart could not be set on the destination: %(error)s"
msgstr ""
"Sanal makine taşındı, ancak hedefte otomatik başlatma ayarlanamadı: %(error)s"
#: instances/jobs.py:988
#: instances/jobs.py:1020
#, python-format
msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s"
msgstr "Sanal makine taşındı, ancak NVRAM dosyası kaynakta kaldı: %(error)s"
#: instances/jobs.py:996
#: instances/jobs.py:1028
msgid "Migrate VM"
msgstr "Sanal makineyi taşı"
#: instances/jobs.py:1043
#: instances/jobs.py:1075
msgid "Migrating the VM"
msgstr "Sanal makine taşınıyor"
#: instances/jobs.py:1093
#: instances/jobs.py:1125
msgid "libvirt is still migrating the VM"
msgstr "libvirt sanal makineyi hâlâ taşıyor"
#: instances/jobs.py:1104
#: instances/jobs.py:1136
msgid "The VM stayed on the source host"
msgstr "Sanal makine kaynak sunucuda kaldı"
#: instances/jobs.py:1106
#: instances/jobs.py:1138
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:1154
#: instances/jobs.py:1186
msgid "Move disk"
msgstr "Diski taşı"
#: instances/jobs.py:1161
#: instances/jobs.py:1193
msgid "Choose a disk of the VM"
msgstr "Sanal makinenin bir diskini seçin"
#: instances/jobs.py:1185
#: instances/jobs.py:1217
msgid "Copying the disk"
msgstr "Disk kopyalanıyor"
#: instances/jobs.py:1228
#: instances/jobs.py:1260
#, 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:1234
#: instances/jobs.py:1266
#, 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:1236
#: instances/jobs.py:1268
#, 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"
#: instances/jobs.py:1243
#: instances/jobs.py:1275
#, python-format
msgid "Disk %(path)s was kept: a disk file that stays needs it"
msgstr "%(path)s diski korundu: kalan bir disk dosyası ona ihtiyaç duyuyor"
#: instances/jobs.py:1244
#: instances/jobs.py:1276
#, python-format
msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s"
msgstr "Sanal makine kaldırıldı, ancak %(path)s diski silinmedi: %(err)s"
#: instances/jobs.py:1245
#: instances/jobs.py:1277
#, python-format
msgid "Disk %(path)s is used by another VM and was kept"
msgstr ""
"%(path)s diski başka bir sanal makine tarafından kullanılıyor ve korundu"
#: instances/jobs.py:1299
#: instances/jobs.py:1331
msgid "Delete VM"
msgstr "Sanal makineyi sil"
#: instances/jobs.py:1329
#: instances/jobs.py:1361
msgid "Deleting the VM"
msgstr "Sanal makine siliniyor"
#: instances/jobs.py:1341
#: instances/jobs.py:1373
msgid "The VM was not deleted; it may have been shut off"
msgstr "Sanal makine silinmedi; kapatılmış olabilir"

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-07 12:30+0000\n"
"POT-Creation-Date: 2026-11-07 13:19+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@ -2653,7 +2653,7 @@ msgstr "请从可用名称列表中选择克隆的名称"
msgid "%(quota)s: cannot create '%(clone_name)s'."
msgstr "%(quota)s:无法创建“%(clone_name)s”。"
#: instances/jobs.py:200 instances/jobs.py:554
#: instances/jobs.py:200 instances/jobs.py:586
#, python-format
msgid "Instance '%(clone_name)s' already exists!"
msgstr "虚拟机 '%(clone_name)s' 已存在!"
@ -2742,275 +2742,287 @@ msgstr "libvirt 仍在创建快照"
msgid "The snapshot was not taken"
msgstr "未创建快照"
#: instances/jobs.py:440
#: instances/jobs.py:445
#, python-format
msgid ""
"Snapshot %(name)s does not record the VM's vCPUs and memory: the quota cannot"
" be checked."
msgstr "快照“%(name)s”未记录虚拟机的 vCPU 和内存:无法检查配额。"
#: instances/jobs.py:455
#, python-format
msgid "%(quota)s: cannot revert to snapshot %(name)s."
msgstr "%(quota)s:无法恢复到快照“%(name)s”。"
#: instances/jobs.py:462
msgid "Revert snapshot"
msgstr "恢复快照"
#: instances/jobs.py:471
#: instances/jobs.py:503
msgid "Reverting the VM"
msgstr "正在恢复虚拟机"
#: instances/jobs.py:482
#: instances/jobs.py:514
msgid "libvirt is still reverting the VM"
msgstr "libvirt 仍在恢复虚拟机"
#: instances/jobs.py:486
#: instances/jobs.py:518
msgid ""
"It cannot be told whether the VM was reverted; check it, then revert again or"
" resolve this job."
msgstr "无法判断虚拟机是否已恢复;请检查后再次恢复,或处理此任务。"
#: instances/jobs.py:503
#: instances/jobs.py:535
msgid "The VM must be running, paused or shut off to be cloned"
msgstr "虚拟机必须处于运行、暂停或关机状态才能克隆"
#: instances/jobs.py:504
#: instances/jobs.py:536
#, 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:523
#: instances/jobs.py:555
msgid "Clone VM"
msgstr "克隆虚拟机"
#: instances/jobs.py:564 instances/jobs.py:571 instances/jobs.py:849
#: instances/jobs.py:596 instances/jobs.py:603 instances/jobs.py:881
msgid "Copying disks"
msgstr "正在复制磁盘"
#: instances/jobs.py:623
#: instances/jobs.py:655
msgid "libvirt has no result of the copy"
msgstr "libvirt 没有复制的结果"
#: instances/jobs.py:634
#: instances/jobs.py:666
#, 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:637 instances/jobs.py:724
#: instances/jobs.py:669 instances/jobs.py:756
msgid "The copy failed"
msgstr "复制失败"
#: instances/jobs.py:662
#: instances/jobs.py:694
#, 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:700
#: instances/jobs.py:732
#, 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:704
#: instances/jobs.py:736
msgid "The job stopped before the copy started"
msgstr "任务在复制开始前停止"
#: instances/jobs.py:711
#: instances/jobs.py:743
msgid "libvirt is still copying the disks"
msgstr "libvirt 仍在复制磁盘"
#: instances/jobs.py:714
#: instances/jobs.py:746
#, python-format
msgid ""
"Another backup runs on the VM; check these volumes, then resolve this job: "
"%(volumes)s"
msgstr "虚拟机上正在运行另一个备份;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:719
#: instances/jobs.py:751
#, 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:731
#: instances/jobs.py:763
#, python-format
msgid ""
"The disks were copied, but the clone could not be defined: %(error)s; check "
"these volumes, then resolve this job: %(volumes)s"
msgstr "磁盘已复制,但无法定义克隆:%(error)s;请检查这些卷,然后处理此任务:%(volumes)s"
#: instances/jobs.py:754 instances/templates/create_instance_w1.html:60
#: instances/jobs.py:786 instances/templates/create_instance_w1.html:60
#: instances/templates/create_instance_w2.html:356
msgid "Create VM"
msgstr "创建虚拟机"
#: instances/jobs.py:778 instances/jobs.py:823 instances/views.py:2474
#: instances/jobs.py:810 instances/jobs.py:855 instances/views.py:2474
msgid "A virtual machine with this name already exists"
msgstr "同名虚拟机已存在"
#: instances/jobs.py:780 instances/jobs.py:820
#: instances/jobs.py:812 instances/jobs.py:852
msgid "There is an instance with same name. Remove it and try again!"
msgstr "已存在同名虚拟机。请删除后重试!"
#: instances/jobs.py:783
#: instances/jobs.py:815
msgid "First you need to create or select an image"
msgstr "请先创建或选择一个镜像"
#: instances/jobs.py:785
#: instances/jobs.py:817
msgid "Choose one disk source: a new disk, existing volumes or a template"
msgstr "请选择一种磁盘来源:新磁盘、现有卷或模板"
#: instances/jobs.py:787
#: instances/jobs.py:819
msgid "Image has already exist. Please check volumes or change instance name"
msgstr "镜像已存在。请检查卷或更改虚拟机名称"
#: instances/jobs.py:789
#: instances/jobs.py:821
msgid "Invalid cache mode"
msgstr "无效的缓存模式"
#: instances/jobs.py:791
#: instances/jobs.py:823
#, python-format
msgid "This host offers no CPU mode or model %(mode)s"
msgstr "此主机不提供 CPU 模式或型号 %(mode)s"
#: instances/jobs.py:793
#: instances/jobs.py:825
#, python-format
msgid "A CD-ROM cannot use the %(bus)s bus"
msgstr "CD-ROM 不能使用 %(bus)s 总线"
#: instances/jobs.py:796
#: instances/jobs.py:828
msgid "The cloud-init seed is read from a CD-ROM: choose its bus"
msgstr "cloud-init 种子从 CD-ROM 读取:请选择其总线"
#: instances/jobs.py:799
#: instances/jobs.py:831
msgid "The cloud-init seed needs a directory storage pool for the disk"
msgstr "cloud-init 种子需要为磁盘使用目录类型的存储池"
#: instances/jobs.py:805
#: instances/jobs.py:837
#, python-format
msgid "This host offers no firmware %(firmware)s"
msgstr "此主机不提供固件 %(firmware)s"
#: instances/jobs.py:807 vrtManager/instance.py:3010
#: instances/jobs.py:839 vrtManager/instance.py:3010
msgid "This host offers no TPM 2.0: install swtpm on it"
msgstr "此主机不提供 TPM 2.0:请在主机上安装 swtpm"
#: instances/jobs.py:868
#: instances/jobs.py:900
msgid "Writing the cloud-init seed"
msgstr "正在写入 cloud-init 种子"
#: instances/jobs.py:875
#: instances/jobs.py:907
msgid "Defining the VM"
msgstr "正在定义虚拟机"
#: instances/jobs.py:908
#: instances/jobs.py:940
#, python-format
msgid ""
"The panel failed unexpectedly; its log has the details. Remove these volumes "
"by hand: %(volumes)s"
msgstr "面板发生意外错误;详细信息见面板日志。请手动删除这些卷:%(volumes)s"
#: instances/jobs.py:912
#: instances/jobs.py:944
#, python-format
msgid "%(error)s. Remove these volumes by hand: %(volumes)s"
msgstr "%(error)s。请手动删除这些卷:%(volumes)s"
#: instances/jobs.py:937
#: instances/jobs.py:969
#, python-format
msgid "The instance is already on %(compute)s"
msgstr "虚拟机已在 %(compute)s 上"
#: instances/jobs.py:939
#: instances/jobs.py:971
#, python-format
msgid "%(compute)s is not reachable"
msgstr "无法连接 %(compute)s"
#: instances/jobs.py:941
#: instances/jobs.py:973
#, python-format
msgid "%(compute)s is the VM's own host under another name"
msgstr "%(compute)s 是该虚拟机所在主机的另一个名称"
#: instances/jobs.py:981
#: instances/jobs.py:1013
#, python-format
msgid ""
"The VM migrated, but autostart could not be set on the destination: %(error)s"
msgstr "虚拟机已迁移,但无法在目标主机上设置自动启动:%(error)s"
#: instances/jobs.py:988
#: instances/jobs.py:1020
#, python-format
msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s"
msgstr "虚拟机已迁移,但其 NVRAM 文件仍留在源主机上:%(error)s"
#: instances/jobs.py:996
#: instances/jobs.py:1028
msgid "Migrate VM"
msgstr "迁移虚拟机"
#: instances/jobs.py:1043
#: instances/jobs.py:1075
msgid "Migrating the VM"
msgstr "正在迁移虚拟机"
#: instances/jobs.py:1093
#: instances/jobs.py:1125
msgid "libvirt is still migrating the VM"
msgstr "libvirt 仍在迁移虚拟机"
#: instances/jobs.py:1104
#: instances/jobs.py:1136
msgid "The VM stayed on the source host"
msgstr "虚拟机留在了源主机上"
#: instances/jobs.py:1106
#: instances/jobs.py:1138
msgid ""
"The VM is on both computes or on neither; check them, then resolve this job."
msgstr "虚拟机同时位于两个计算节点上,或都不在;请检查后处理此任务。"
#: instances/jobs.py:1154
#: instances/jobs.py:1186
msgid "Move disk"
msgstr "移动磁盘"
#: instances/jobs.py:1161
#: instances/jobs.py:1193
msgid "Choose a disk of the VM"
msgstr "请选择虚拟机的一个磁盘"
#: instances/jobs.py:1185
#: instances/jobs.py:1217
msgid "Copying the disk"
msgstr "正在复制磁盘"
#: instances/jobs.py:1228
#: instances/jobs.py:1260
#, python-format
msgid "Another block job runs on %(dev)s; check the VM"
msgstr "%(dev)s 上正在运行另一个块任务;请检查虚拟机"
#: instances/jobs.py:1234
#: instances/jobs.py:1266
#, python-format
msgid "The disk stayed in %(path)s; remove %(dest)s if it is there"
msgstr "磁盘仍在 %(path)s;如果 %(dest)s 存在,请将其删除"
#: instances/jobs.py:1236
#: instances/jobs.py:1268
#, python-format
msgid "The definitions of %(dev)s name %(paths)s; check the VM"
msgstr "%(dev)s 的定义指向 %(paths)s;请检查虚拟机"
#: instances/jobs.py:1243
#: instances/jobs.py:1275
#, python-format
msgid "Disk %(path)s was kept: a disk file that stays needs it"
msgstr "磁盘 %(path)s 已保留:某个保留的磁盘文件依赖它"
#: instances/jobs.py:1244
#: instances/jobs.py:1276
#, python-format
msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s"
msgstr "虚拟机已删除,但磁盘 %(path)s 未被删除:%(err)s"
#: instances/jobs.py:1245
#: instances/jobs.py:1277
#, python-format
msgid "Disk %(path)s is used by another VM and was kept"
msgstr "磁盘 %(path)s 正被另一台虚拟机使用,已保留"
#: instances/jobs.py:1299
#: instances/jobs.py:1331
msgid "Delete VM"
msgstr "删除虚拟机"
#: instances/jobs.py:1329
#: instances/jobs.py:1361
msgid "Deleting the VM"
msgstr "正在删除虚拟机"
#: instances/jobs.py:1341
#: instances/jobs.py:1373
msgid "The VM was not deleted; it may have been shut off"
msgstr "虚拟机未被删除;它可能已被关闭"