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:
parent
aab8dbf1df
commit
d8a184eff7
7 changed files with 231 additions and 123 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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 "虚拟机未被删除;它可能已被关闭"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue