diff --git a/instances/templates/instances/migrate_plan.html b/instances/templates/instances/migrate_plan.html
index 454b9ce9..3ef0a71e 100644
--- a/instances/templates/instances/migrate_plan.html
+++ b/instances/templates/instances/migrate_plan.html
@@ -25,7 +25,8 @@
{% if disk.same_name %}
{% trans "Another disk of the VM has this file name: copy them to different pools." %}
{% endif %}
{% elif disk.refusal %}
- {% blocktrans with reason=disk.refusal %}Cannot be copied: {{ reason }}{% endblocktrans %}
+ {% trans disk.refusal as reason %}
+ {% blocktrans %}Cannot be copied: {{ reason }}{% endblocktrans %}
{% elif pools %}
{% trans "Every pool of the destination host has a file of this name" %}
{% else %}
diff --git a/instances/views.py b/instances/views.py
index 3b5f86ed..42cd6cc1 100755
--- a/instances/views.py
+++ b/instances/views.py
@@ -2074,14 +2074,18 @@ def _host_devices(instance):
else:
managed = {row["address"] for row in own_pci if row["managed"] and row["next"]}
by_address = {device["address"]: device for device in pci}
+ # the problems in the page's language (vrtManager.nodedevs gives messages)
+ def shown(problems):
+ return [util.render(m["text"], m["values"], _) for m in problems]
+
for device in pci:
- device["problems"] = pci_problems(instance.name, device, by_address, managed | {device["address"]})
+ device["problems"] = shown(pci_problems(instance.name, device, by_address, managed | {device["address"]}))
if device["address"] in {row["address"] for row in own_pci}:
device["problems"].append(_("The VM has it"))
for device in usb:
address = (device["bus"], device["device"]) if device["same_model"] else None
device["selector"] = (*device["model"], address)
- device["problems"] = hostdev_problems(instance.name, pci, [device], set(), [device["selector"]])
+ device["problems"] = shown(hostdev_problems(instance.name, pci, [device], set(), [device["selector"]]))
if instance.name in device["vms"]:
device["problems"].append(_("The VM has it"))
return pci, usb
diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo
index e854198e..6a9c4234 100644
Binary files a/locale/tr/LC_MESSAGES/django.mo and b/locale/tr/LC_MESSAGES/django.mo differ
diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po
index c653da1d..b8a256f9 100644
--- a/locale/tr/LC_MESSAGES/django.po
+++ b/locale/tr/LC_MESSAGES/django.po
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-02 20:42+0000\n"
+"POT-Creation-Date: 2026-11-02 21:03+0000\n"
"PO-Revision-Date: 2020-11-04 12:00+0000\n"
"Last-Translator: catborise , 2020\n"
"Language-Team: Turkish (https://www.transifex.com/catborise/teams/110663/"
@@ -1720,8 +1720,8 @@ msgstr "Kaydedildi: %(settings)s"
msgid "Nothing changed"
msgstr "Hiçbir şey değişmedi"
-#: appsettings/views.py:122 vrtManager/nodedevs.py:195
-#: vrtManager/nodedevs.py:234
+#: appsettings/views.py:122 vrtManager/nodedevs.py:219
+#: vrtManager/nodedevs.py:258
msgid "Other"
msgstr "Diğer"
@@ -2718,7 +2718,7 @@ msgstr ""
msgid "Delete snapshot"
msgstr "Anlık görüntüyü sil"
-#: instances/jobs.py:303
+#: instances/jobs.py:303 vrtManager/instance.py:2304
#, 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"
@@ -2753,7 +2753,7 @@ msgstr "Sanal makine artık işin çalıştığı sunucuda değil"
msgid "Take snapshot"
msgstr "Anlık görüntü al"
-#: instances/jobs.py:371
+#: instances/jobs.py:371 vrtManager/instance.py:1900
#, python-format
msgid "A snapshot named %(name)s already exists"
msgstr "%(name)s adında bir anlık görüntü zaten var"
@@ -2808,15 +2808,15 @@ msgstr ""
msgid "Clone VM"
msgstr "Sanal makineyi klonla"
-#: instances/jobs.py:542 instances/jobs.py:549 instances/jobs.py:805
+#: instances/jobs.py:542 instances/jobs.py:549 instances/jobs.py:806
msgid "Copying disks"
msgstr "Diskler kopyalanıyor"
-#: instances/jobs.py:600
+#: instances/jobs.py:601
msgid "libvirt has no result of the copy"
msgstr "libvirt'te kopyanın sonucu yok"
-#: instances/jobs.py:611
+#: instances/jobs.py:612
#, python-format
msgid ""
"The copy failed: %(error)s. The guest's file systems could not be thawed "
@@ -2825,11 +2825,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:614 instances/jobs.py:698
+#: instances/jobs.py:615 instances/jobs.py:699
msgid "The copy failed"
msgstr "Kopyalama başarısız oldu"
-#: instances/jobs.py:639
+#: instances/jobs.py:640
#, python-format
msgid ""
"The VM is no longer on the compute the job ran on; check these volumes, then "
@@ -2838,7 +2838,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:674
+#: instances/jobs.py:675
#, python-format
msgid ""
"A volume of the clone was being created; check these volumes, then resolve "
@@ -2847,15 +2847,15 @@ msgstr ""
"Klonun bir birimi oluşturuluyordu; şu birimleri kontrol edin, sonra bu işi "
"çözün: %(volumes)s"
-#: instances/jobs.py:678
+#: instances/jobs.py:679
msgid "The job stopped before the copy started"
msgstr "İş, kopyalama başlamadan durdu"
-#: instances/jobs.py:685
+#: instances/jobs.py:686
msgid "libvirt is still copying the disks"
msgstr "libvirt diskleri hâlâ kopyalıyor"
-#: instances/jobs.py:688
+#: instances/jobs.py:689
#, python-format
msgid ""
"Another backup runs on the VM; check these volumes, then resolve this job: "
@@ -2864,7 +2864,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:693
+#: instances/jobs.py:694
#, python-format
msgid ""
"libvirt has no result of the copy; check these volumes, then resolve this "
@@ -2873,140 +2873,140 @@ msgstr ""
"libvirt'te kopyanın sonucu yok; şu birimleri kontrol edin, sonra bu işi "
"çözün: %(volumes)s"
-#: instances/jobs.py:721 instances/templates/create_instance_w1.html:60
+#: instances/jobs.py:722 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:744 instances/views.py:2477
+#: instances/jobs.py:745 instances/views.py:2481
msgid "A virtual machine with this name already exists"
msgstr "Bu isimde bir sanal makine mevcut"
-#: instances/jobs.py:746
+#: instances/jobs.py:747
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:749
+#: instances/jobs.py:750
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:751
+#: instances/jobs.py:752
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:753
+#: instances/jobs.py:754
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:755
+#: instances/jobs.py:756
msgid "Invalid cache mode"
msgstr "Geçersiz önbellek modu"
-#: instances/jobs.py:757
+#: instances/jobs.py:758
#, 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:759
+#: instances/jobs.py:760
#, python-format
msgid "A CD-ROM cannot use the %(bus)s bus"
msgstr "CD-ROM %(bus)s veri yolunu kullanamaz"
-#: instances/jobs.py:762
+#: instances/jobs.py:763
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:765
+#: instances/jobs.py:766
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:771
+#: instances/jobs.py:772
#, python-format
msgid "This host offers no firmware %(firmware)s"
msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor"
-#: instances/jobs.py:773
+#: instances/jobs.py:774 vrtManager/instance.py:3023
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:824
+#: instances/jobs.py:825
msgid "Writing the cloud-init seed"
msgstr "cloud-init tohumu yazılıyor"
-#: instances/jobs.py:831
+#: instances/jobs.py:832
msgid "Defining the VM"
msgstr "Sanal makine tanımlanıyor"
-#: instances/jobs.py:864
+#: instances/jobs.py:865
#, python-format
msgid "%(error)s. Remove these volumes by hand: %(volumes)s"
msgstr "%(error)s. Şu birimleri elle silin: %(volumes)s"
-#: instances/jobs.py:889
+#: instances/jobs.py:890
#, python-format
msgid "The instance is already on %(compute)s"
msgstr "Sanal makine zaten %(compute)s üzerinde"
-#: instances/jobs.py:891
+#: instances/jobs.py:892
#, python-format
msgid "%(compute)s is not reachable"
msgstr "%(compute)s sunucusuna ulaşılamıyor"
-#: instances/jobs.py:893
+#: instances/jobs.py:894
#, 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:911
+#: instances/jobs.py:912
msgid "Migrate VM"
msgstr "Sanal makineyi taşı"
-#: instances/jobs.py:955
+#: instances/jobs.py:956
msgid "Migrating the VM"
msgstr "Sanal makine taşınıyor"
-#: instances/jobs.py:989
+#: instances/jobs.py:993
msgid "libvirt is still migrating the VM"
msgstr "libvirt sanal makineyi hâlâ taşıyor"
-#: instances/jobs.py:996
+#: instances/jobs.py:1000
msgid "The VM stayed on the source host"
msgstr "Sanal makine kaynak sunucuda kaldı"
-#: instances/jobs.py:998
+#: instances/jobs.py:1002
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:1030
+#: instances/jobs.py:1034
msgid "Move disk"
msgstr "Diski taşı"
-#: instances/jobs.py:1037
+#: instances/jobs.py:1041
msgid "Choose a disk of the VM"
msgstr "Sanal makinenin bir diskini seçin"
-#: instances/jobs.py:1061
+#: instances/jobs.py:1065
msgid "Copying the disk"
msgstr "Disk kopyalanıyor"
-#: instances/jobs.py:1104
+#: instances/jobs.py:1108
#, 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:1110
+#: instances/jobs.py:1114
#, 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:1112
+#: instances/jobs.py:1116
#, 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"
@@ -3246,7 +3246,7 @@ msgid "Disk source"
msgstr "Disk kaynağı"
#: instances/templates/add_instance_volume.html:21
-#: instances/templates/create_instance_w2.html:134 instances/views.py:2496
+#: instances/templates/create_instance_w2.html:134 instances/views.py:2500
msgid "New disk"
msgstr "Yeni disk"
@@ -3618,7 +3618,7 @@ msgstr "Özel CPU modelleri"
msgid "Size (GiB)"
msgstr "Boyut (GiB)"
-#: instances/templates/create_instance_w2.html:161 instances/views.py:2497
+#: instances/templates/create_instance_w2.html:161 instances/views.py:2501
msgid "Existing volumes"
msgstr "Mevcut birimler"
@@ -3653,7 +3653,7 @@ msgstr "Aygıt"
msgid "Remove"
msgstr "Kaldır"
-#: instances/templates/create_instance_w2.html:194 instances/views.py:2498
+#: instances/templates/create_instance_w2.html:194 instances/views.py:2502
msgid "Clone from template"
msgstr "Şablondan klonla"
@@ -3694,7 +3694,7 @@ msgstr "NIC tipi"
msgid "Network filter"
msgstr "Ağ filtresi"
-#: instances/templates/create_instance_w2.html:293 vrtManager/nodedevs.py:29
+#: instances/templates/create_instance_w2.html:293 vrtManager/nodedevs.py:30
msgid "Video"
msgstr "Görüntü"
@@ -4275,33 +4275,33 @@ msgstr ""
"Sanal makinenin başka bir diski de bu dosya adını taşıyor: onları farklı "
"havuzlara kopyalayın."
-#: instances/templates/instances/migrate_plan.html:28
+#: instances/templates/instances/migrate_plan.html:29
#, python-format
msgid "Cannot be copied: %(reason)s"
msgstr "Kopyalanamaz: %(reason)s"
-#: instances/templates/instances/migrate_plan.html:30
+#: instances/templates/instances/migrate_plan.html:31
msgid "Every pool of the destination host has a file of this name"
msgstr "Hedef sunucunun her havuzunda bu adda bir dosya var"
-#: instances/templates/instances/migrate_plan.html:32
+#: instances/templates/instances/migrate_plan.html:33
msgid "No pool of the destination host can take it"
msgstr "Hedef sunucunun hiçbir havuzu bunu alamıyor"
-#: instances/templates/instances/migrate_plan.html:38
+#: instances/templates/instances/migrate_plan.html:39
msgid "Disks of the next start"
msgstr "Sonraki başlatmanın diskleri"
-#: instances/templates/instances/migrate_plan.html:39
+#: instances/templates/instances/migrate_plan.html:40
msgid "The VM gets these at its next start; the migration does not copy them."
msgstr ""
"Sanal makine bunları bir sonraki başlatmada alır; taşıma bunları kopyalamaz."
-#: instances/templates/instances/migrate_plan.html:43
+#: instances/templates/instances/migrate_plan.html:44
msgid "on storage both hosts share"
msgstr "iki sunucunun paylaştığı depolamada"
-#: instances/templates/instances/migrate_plan.html:44
+#: instances/templates/instances/migrate_plan.html:45
msgid ""
"not on storage both hosts share: detach it, or move it to shared storage "
"first"
@@ -4309,7 +4309,7 @@ msgstr ""
"iki sunucunun paylaştığı depolamada değil: ayırın ya da önce paylaşılan "
"depolamaya taşıyın"
-#: instances/templates/instances/migrate_plan.html:51
+#: instances/templates/instances/migrate_plan.html:52
msgid ""
"A copy is made during the migration and takes as long as the disk's data "
"needs. Disks on network storage stay where they are."
@@ -4317,7 +4317,7 @@ msgstr ""
"Taşıma sırasında bir kopya oluşturulur ve diskteki verinin gerektirdiği "
"kadar sürer. Ağ depolamadaki diskler oldukları yerde kalır."
-#: instances/templates/instances/migrate_plan.html:52
+#: instances/templates/instances/migrate_plan.html:53
msgid ""
"A shut-off VM is started paused for a copy, its CPUs never run, and is shut "
"off on the destination."
@@ -6121,7 +6121,7 @@ msgstr "Ön yükleme sırası başarıyla değiştirildi."
msgid "The XML is empty"
msgstr "XML boş"
-#: instances/views.py:1870 instances/views.py:2487
+#: instances/views.py:1870 instances/views.py:2491
#, python-format
msgid "The XML is not well-formed: %(error)s"
msgstr "XML düzgün biçimlendirilmemiş: %(error)s"
@@ -6177,21 +6177,21 @@ msgstr ""
"aygıtı sonra ekleyin; Diskler sekmesindeki PCIe yuvaları ekle, sonraki "
"başlatmadan itibaren ona boş yuvalar verir."
-#: instances/views.py:2080 instances/views.py:2086
+#: instances/views.py:2084 instances/views.py:2090
msgid "The VM has it"
msgstr "Sanal makinede var"
-#: instances/views.py:2117
+#: instances/views.py:2121
#, python-format
msgid "The host has no USB device %(device)s"
msgstr "Sunucuda %(device)s USB aygıtı yok"
-#: instances/views.py:2138
+#: instances/views.py:2142
#, python-format
msgid "The host has no PCI device %(device)s"
msgstr "Sunucuda %(device)s PCI aygıtı yok"
-#: instances/views.py:2156
+#: instances/views.py:2160
#, python-format
msgid ""
"Removal of PCI device %(address)s requested: the guest has not let it go "
@@ -6201,20 +6201,20 @@ msgstr ""
"bırakmadı; bırakana ya da sanal makine durana kadar çalışan sanal makinede "
"kalır."
-#: instances/views.py:2201 instances/views.py:2231
+#: instances/views.py:2205 instances/views.py:2235
msgid "Select a network interface and a QoS direction"
msgstr "Bir ağ arabirimi ve bir QoS yönü seçin"
-#: instances/views.py:2204
+#: instances/views.py:2208 vrtManager/network.py:459
msgid "QoS rates must be whole numbers"
msgstr "QoS hızları tam sayı olmalıdır"
-#: instances/views.py:2211 networks/views.py:229
+#: instances/views.py:2215 networks/views.py:229
#, python-format
msgid "%(qos_dir)s QoS is set"
msgstr "%(qos_dir)s QoS ayarlandı"
-#: instances/views.py:2216
+#: instances/views.py:2220
#, python-format
msgid ""
"%(qos_dir)s QoS is set. A running VM gets it after it is powered off and on."
@@ -6222,12 +6222,12 @@ msgstr ""
"%(qos_dir)s QoS ayarlandı. Çalışan bir sanal makine, kapatılıp açıldıktan "
"sonra bunu alır."
-#: instances/views.py:2238 networks/views.py:255
+#: instances/views.py:2242 networks/views.py:255
#, python-format
msgid "%(qos_dir)s QoS is deleted"
msgstr "%(qos_dir)s QoS silindi"
-#: instances/views.py:2243
+#: instances/views.py:2247
#, python-format
msgid ""
"%(qos_dir)s QoS is deleted. A running VM keeps the old limit until it is "
@@ -6236,21 +6236,21 @@ msgstr ""
"%(qos_dir)s QoS silindi. Çalışan bir sanal makine, kapatılıp açılana kadar "
"eski sınırı korur."
-#: instances/views.py:2271
+#: instances/views.py:2275
msgid "Only one owner is allowed and the one already added"
msgstr "Sadece bir sahibe izin verilmiştir ve bir sahip zaten eklenmiş"
-#: instances/views.py:2273
+#: instances/views.py:2277
#, python-format
msgid "%(user)s is already an owner"
msgstr "%(user)s zaten bir sahip"
-#: instances/views.py:2317
+#: instances/views.py:2321
#, python-format
msgid "Console settings were not saved: %(errors)s"
msgstr "Konsol ayarları kaydedilmedi: %(errors)s"
-#: instances/views.py:2339
+#: instances/views.py:2343
msgid ""
"Error setting console password. You should check that your instance have an "
"graphic device."
@@ -6258,67 +6258,67 @@ msgstr ""
"Konsol parolası ayarlanırken hata oluştu. Sanal makinenin bir grafik "
"aygıtına sahip olduğundan emin olunuz."
-#: instances/views.py:2386
+#: instances/views.py:2390
msgid "Shut the VM down to change whether it is a template."
msgstr "Şablon olup olmadığını değiştirmek için sanal makineyi kapatın."
-#: instances/views.py:2393
+#: instances/views.py:2397
#, python-format
msgid "User %(quota_msg)s quota reached, '%(instance_name)s' stays a template!"
msgstr ""
"Kullanıcının %(quota_msg)s kotası doldu, '%(instance_name)s' şablon olarak "
"kalıyor!"
-#: instances/views.py:2475
+#: instances/views.py:2479
msgid "The XML has no name"
msgstr "XML'de ad yok"
-#: instances/views.py:2489 webvirtcloud/middleware.py:79
+#: instances/views.py:2493 webvirtcloud/middleware.py:79
#, python-format
msgid "libvirt Error - %(exception)s"
msgstr "libvirt Hatası - %(exception)s"
-#: instances/views.py:2496
+#: instances/views.py:2500
msgid "An empty disk of the size you set"
msgstr "Belirlediğiniz boyutta boş bir disk"
-#: instances/views.py:2497
+#: instances/views.py:2501
msgid "Volumes already in a pool"
msgstr "Bir havuzda zaten bulunan birimler"
-#: instances/views.py:2498
+#: instances/views.py:2502
msgid "A copy of a template volume"
msgstr "Bir şablon biriminin kopyası"
-#: instances/views.py:2587
+#: instances/views.py:2591
msgid "You haven't defined any storage pools"
msgstr "Hiç bir depolama havuzu tanımlamadınız"
-#: instances/views.py:2589
+#: instances/views.py:2593
msgid "You haven't defined any network pools"
msgstr "Hiç bir ağ havuzu tanımlamadınız"
-#: instances/views.py:2652
+#: instances/views.py:2656
msgid "Flavor Created"
msgstr "Flavor Oluşturuldu"
-#: instances/views.py:2660
+#: instances/views.py:2664
msgid "Create Flavor"
msgstr "Kullanıcı Oluştur"
-#: instances/views.py:2675
+#: instances/views.py:2679
msgid "Flavor Updated"
msgstr "Flavor Güncellendi"
-#: instances/views.py:2681
+#: instances/views.py:2685
msgid "Update Flavor"
msgstr "Flavor Güncelle"
-#: instances/views.py:2693
+#: instances/views.py:2697
msgid "Flavor Deleted"
msgstr "Flavor Silindi"
-#: instances/views.py:2699
+#: instances/views.py:2703
msgid "Only the size template goes; VMs made from it keep their size."
msgstr ""
"Yalnızca boyut şablonu silinir; ondan oluşturulan sanal makineler "
@@ -6427,7 +6427,7 @@ msgstr "%(host)s üzerinde"
#: networks/templates/create_net_block.html:34
#: networks/templates/create_net_block.html:90
#: networks/templates/network.html:62 networks/views.py:25
-#: vrtManager/nodedevs.py:14
+#: vrtManager/nodedevs.py:15
msgid "Bridge"
msgstr "Köprü"
@@ -7815,12 +7815,12 @@ msgstr "İptal edilemedi: sunucuya ulaşılamadı."
msgid "The host of this virtual machine cannot be reached right now."
msgstr "Bu sanal makinenin sunucusuna şu anda ulaşılamıyor."
-#: operations/utils.py:146
+#: operations/utils.py:157
msgid ""
"A short operation is in progress on this resource; try again in a moment."
msgstr "Bu kaynakta kısa bir işlem sürüyor; birazdan yeniden deneyin."
-#: operations/utils.py:149
+#: operations/utils.py:160
#, python-format
msgid ""
"%(label)s is %(state)s on this resource (started by %(user)s, %(since)s "
@@ -7829,7 +7829,7 @@ msgstr ""
"%(label)s bu kaynakta %(state)s (%(user)s başlattı, %(since)s önce); İşler "
"sayfasına bakın."
-#: operations/utils.py:151
+#: operations/utils.py:162
#, python-format
msgid "%(label)s is %(state)s on this resource (since %(since)s)."
msgstr "%(label)s bu kaynakta %(state)s (%(since)s beri)."
@@ -8770,70 +8770,1030 @@ msgstr ""
"Eski değerin yerini hemen alır; bir parolanın şifrelediği veriler yeniden "
"şifrelenmez."
-#: vrtManager/nodedevs.py:10
+#: vrtManager/connection.py:200
+msgid "the connection closed right after opening"
+msgstr "bağlantı açıldıktan hemen sonra kapandı"
+
+#: vrtManager/instance.py:134
+msgid "A running or paused VM cannot be migrated offline"
+msgstr "Çalışan veya duraklatılmış bir sanal makine çevrimdışı taşınamaz"
+
+#: vrtManager/instance.py:137
+msgid "The VM can be migrated when it is running, paused or shut off"
+msgstr "Sanal makine çalışırken, duraklatılmışken veya kapalıyken taşınabilir"
+
+#: vrtManager/instance.py:160
+msgid "A VM with snapshots cannot be migrated"
+msgstr "Anlık görüntüleri olan bir sanal makine taşınamaz"
+
+#: vrtManager/instance.py:163
+msgid "A VM with a saved state cannot be migrated"
+msgstr "Kaydedilmiş durumu olan bir sanal makine taşınamaz"
+
+#: vrtManager/instance.py:170
+msgid "A VM with host devices cannot be migrated; remove them first"
+msgstr "Sunucu aygıtları olan bir sanal makine taşınamaz; önce onları kaldırın"
+
+#: vrtManager/instance.py:176
+#, python-format
+msgid "The destination host does not have these disks of the VM: %(disks)s"
+msgstr "Hedef sunucuda sanal makinenin şu diskleri yok: %(disks)s"
+
+#: vrtManager/instance.py:177
+#, python-format
+msgid "%(target)s (%(source)s)"
+msgstr "%(target)s (%(source)s)"
+
+#: vrtManager/instance.py:185
+msgid "Destination host emulator is different. Cannot be migrated"
+msgstr "Hedef sunucunun emülatörü farklı. Taşınamaz"
+
+#: vrtManager/instance.py:316
+msgid "an image is not copied; eject it first"
+msgstr "bir imaj kopyalanmaz; önce onu çıkarın"
+
+#: vrtManager/instance.py:319
+msgid "a read-only or encrypted disk is not copied"
+msgstr "salt okunur veya şifreli bir disk kopyalanmaz"
+
+#: vrtManager/instance.py:322
+msgid "only raw and qcow2 disks are copied"
+msgstr "yalnızca raw ve qcow2 diskler kopyalanır"
+
+#: vrtManager/instance.py:324
+msgid "no storage pool of the source host has it"
+msgstr "kaynak sunucunun hiçbir depolama havuzunda yok"
+
+#: vrtManager/instance.py:326
+msgid "a disk with a backing file is not copied"
+msgstr "temel (backing) dosyası olan bir disk kopyalanmaz"
+
+#: vrtManager/instance.py:346
+#, python-format
+msgid ""
+"%(target)s (%(path)s): the destination host does not share it; choose a pool "
+"to copy it to"
+msgstr ""
+"%(target)s (%(path)s): hedef sunucu bunu paylaşmıyor; kopyalanacağı bir "
+"havuz seçin"
+
+#: vrtManager/instance.py:350
+#, python-format
+msgid "%(target)s (%(path)s): %(refusal)s"
+msgstr "%(target)s (%(path)s): %(refusal)s"
+
+#: vrtManager/instance.py:355
+#, python-format
+msgid ""
+"%(target)s (%(path)s): the destination host has no pool %(pool)s for disk "
+"files"
+msgstr ""
+"%(target)s (%(path)s): hedef sunucuda disk dosyaları için %(pool)s havuzu yok"
+
+#: vrtManager/instance.py:362
+#, python-format
+msgid ""
+"%(target)s (%(path)s): pool %(pool)s of the destination host already has a "
+"file %(file)s"
+msgstr ""
+"%(target)s (%(path)s): hedef sunucunun %(pool)s havuzunda zaten %(file)s "
+"dosyası var"
+
+#: vrtManager/instance.py:366
+#, python-format
+msgid "%(target)s (%(path)s): another disk is copied to %(dest)s"
+msgstr "%(target)s (%(path)s): başka bir disk %(dest)s konumuna kopyalanıyor"
+
+#: vrtManager/instance.py:371
+#, python-format
+msgid "%(target)s: the VM has no disk file to copy"
+msgstr "%(target)s: sanal makinenin kopyalanacak disk dosyası yok"
+
+#: vrtManager/instance.py:375
+#, python-format
+msgid ""
+"%(target)s (%(path)s) of the next start: the destination host does not share "
+"it"
+msgstr ""
+"Sonraki başlatmanın %(target)s (%(path)s) diski: hedef sunucu bunu "
+"paylaşmıyor"
+
+#: vrtManager/instance.py:379
+#, python-format
+msgid "The disks cannot be migrated: %(problems)s"
+msgstr "Diskler taşınamaz: %(problems)s"
+
+#: vrtManager/instance.py:392
+#, python-format
+msgid ""
+"%(error)s; remove the partial disk copies from the destination host: "
+"%(copies)s"
+msgstr ""
+"%(error)s; yarım kalan disk kopyalarını hedef sunucudan kaldırın: %(copies)s"
+
+#: vrtManager/instance.py:409
+#, python-format
+msgid ""
+"%(error)s; remove the partial disk copies from the destination host: %(left)s"
+msgstr ""
+"%(error)s; yarım kalan disk kopyalarını hedef sunucudan kaldırın: %(left)s"
+
+#: vrtManager/instance.py:426
+#, python-format
+msgid "The destination host cannot see the NVRAM file %(path)s"
+msgstr "Hedef sunucu %(path)s NVRAM dosyasını göremiyor"
+
+#: vrtManager/instance.py:494
+#, python-format
+msgid "%(type)s disk, cannot be checked"
+msgstr "%(type)s disk, kontrol edilemiyor"
+
+#: vrtManager/instance.py:534
+#, python-format
+msgid "The destination host already has a VM %(key)s"
+msgstr "Hedef sunucuda zaten %(key)s sanal makinesi var"
+
+#: vrtManager/instance.py:556
+msgid "The VM was started on the source host during the migration"
+msgstr "Sanal makine taşıma sırasında kaynak sunucuda başlatıldı"
+
+#: vrtManager/instance.py:572
+#, python-format
+msgid ""
+"%(error)s; the VM may now be defined only on the destination host: check "
+"both hosts"
+msgstr ""
+"%(error)s; sanal makine artık yalnızca hedef sunucuda tanımlı olabilir: iki "
+"sunucuyu da kontrol edin"
+
+#: vrtManager/instance.py:578
+#, python-format
+msgid ""
+"%(error)s; the VM is now defined on both hosts, remove it from the "
+"destination: %(undo_error)s"
+msgstr ""
+"%(error)s; sanal makine artık iki sunucuda da tanımlı, hedeften kaldırın: "
+"%(undo_error)s"
+
+#: vrtManager/instance.py:584
+msgid ""
+"Snapshot names are 1-64 letters, digits, '.', '_' or '-' and do not start "
+"with '.'"
+msgstr ""
+"Anlık görüntü adları 1-64 harf, rakam, '.', '_' veya '-' karakterinden "
+"oluşur ve '.' ile başlamaz"
+
+#: vrtManager/instance.py:1100
+msgid ""
+"A device other than a disk or NIC is in the boot order: change it in the XML "
+"editor"
+msgstr ""
+"Önyükleme sırasında disk veya NIC dışında bir aygıt var: bunu XML "
+"düzenleyicide değiştirin"
+
+#: vrtManager/instance.py:1112
+msgid "Invalid Device Type for boot order"
+msgstr "Önyükleme sırası için geçersiz aygıt türü"
+
+#: vrtManager/instance.py:1115
+#, python-format
+msgid "Boot device %(dev)s is not in the definition"
+msgstr "Önyükleme aygıtı %(dev)s tanımda yok"
+
+#: vrtManager/instance.py:1135
+#, python-format
+msgid "CD-ROM %(dev)s is not in the running definition"
+msgstr "CD-ROM %(dev)s çalışan tanımda yok"
+
+#: vrtManager/instance.py:1136
+#, python-format
+msgid "CD-ROM %(dev)s is not in the persistent definition"
+msgstr "CD-ROM %(dev)s kalıcı tanımda yok"
+
+#: vrtManager/instance.py:1148
+#, python-format
+msgid "Media of %(dev)s changed in the running VM only: %(err)s"
+msgstr "%(dev)s ortamı yalnızca çalışan sanal makinede değişti: %(err)s"
+
+#: vrtManager/instance.py:1159
+#, python-format
+msgid "ISO image %(image)s was not found"
+msgstr "ISO imajı %(image)s bulunamadı"
+
+#: vrtManager/instance.py:1161
+#, python-format
+msgid "ISO image %(image)s is in more than one storage pool"
+msgstr "ISO imajı %(image)s birden fazla depolama havuzunda var"
+
+#: vrtManager/instance.py:1268
+#, python-format
+msgid "Disk %(target_dev)s is not in the VM's definition"
+msgstr "Disk %(target_dev)s sanal makinenin tanımında yok"
+
+#: vrtManager/instance.py:1396
+msgid "Please shutdown the instance then try to enable vCPU hotplug"
+msgstr ""
+"vCPU hotplug özelliğini etkinleştirmek için önce sanal makineyi kapatın"
+
+#: vrtManager/instance.py:1859
+msgid ""
+"The VM has external snapshots; internal snapshots cannot be mixed with them"
+msgstr ""
+"Sanal makinenin harici anlık görüntüleri var; dahili anlık görüntüler "
+"bunlarla karıştırılamaz"
+
+#: vrtManager/instance.py:1891
+msgid "A snapshot with memory is not quiesced; it keeps the whole guest state"
+msgstr ""
+"Bellekli bir anlık görüntüde dosya sistemleri dondurulmaz; tüm konuk "
+"durumunu saklar"
+
+#: vrtManager/instance.py:1893
+msgid "Only a running VM's file systems can be frozen"
+msgstr "Yalnızca çalışan bir sanal makinenin dosya sistemleri dondurulabilir"
+
+#: vrtManager/instance.py:1895
+msgid "The QEMU guest agent is not connected; it freezes the file systems"
+msgstr "QEMU konuk aracısı bağlı değil; dosya sistemlerini o dondurur"
+
+#: vrtManager/instance.py:1898
+msgid ""
+"The VM has internal snapshots; external snapshots cannot be mixed with them"
+msgstr ""
+"Sanal makinenin dahili anlık görüntüleri var; harici anlık görüntüler "
+"bunlarla karıştırılamaz"
+
+#: vrtManager/instance.py:1903
+msgid "Newer snapshots exist after the last revert; delete them first"
+msgstr ""
+"Son geri dönüşten sonra daha yeni anlık görüntüler var; önce onları silin"
+
+#: vrtManager/instance.py:1906
+msgid "No snapshot is current; revert to a snapshot first"
+msgstr "Geçerli anlık görüntü yok; önce bir anlık görüntüye geri dönün"
+
+#: vrtManager/instance.py:1912
+#, python-format
+msgid "Eject the media from %(dev)s first"
+msgstr "Önce %(dev)s içindeki ortamı çıkarın"
+
+#: vrtManager/instance.py:1914
+#, python-format
+msgid "Disk %(dev)s is not a file; external snapshots need file disks"
+msgstr ""
+"Disk %(dev)s bir dosya değil; harici anlık görüntüler dosya diskleri "
+"gerektirir"
+
+#: vrtManager/instance.py:1922
+#, python-format
+msgid ""
+"The storage pools could not be refreshed, so their free space is unknown: "
+"%(err)s"
+msgstr ""
+"Depolama havuzları yenilenemedi, bu yüzden boş alanları bilinmiyor: %(err)s"
+
+#: vrtManager/instance.py:1943
+msgid "Memory is saved only from a running VM"
+msgstr "Bellek yalnızca çalışan bir sanal makineden kaydedilir"
+
+#: vrtManager/instance.py:1945
+msgid "The VM has no disk to keep its memory next to"
+msgstr "Sanal makinenin, belleğinin yanına kaydedileceği bir diski yok"
+
+#: vrtManager/instance.py:1949
+#, python-format
+msgid "%(disk)s is not in a storage pool; memory is saved only into one"
+msgstr ""
+"%(disk)s bir depolama havuzunda değil; bellek yalnızca bir havuza kaydedilir"
+
+#: vrtManager/instance.py:1980
+#, python-format
+msgid ""
+"Storage pool %(pool)s has %(free).1f GiB free; the snapshot needs %(size).1f "
+"GiB (%(percent)g%% of the VM's disks in it, at least %(minimum)g GiB, and "
+"the memory if it is saved)"
+msgstr ""
+"%(pool)s depolama havuzunda %(free).1f GiB boş alan var; anlık görüntü "
+"%(size).1f GiB gerektiriyor (sanal makinenin oradaki disklerinin %%"
+"%(percent)g kadarı, en az %(minimum)g GiB, ve kaydediliyorsa bellek)"
+
+#: vrtManager/instance.py:1998
+msgid ""
+"This snapshot was taken by an older version on a UEFI VM and cannot be "
+"reverted; delete it instead"
+msgstr ""
+"Bu anlık görüntü UEFI bir sanal makinede eski bir sürümle alındı ve geri "
+"dönülemez; bunun yerine silin"
+
+#: vrtManager/instance.py:2028
+#, python-format
+msgid ""
+"The revert failed twice; try again in a moment (%(error)s). The VM is "
+"running."
+msgstr ""
+"Geri dönüş iki kez başarısız oldu; biraz sonra tekrar deneyin (%(error)s). "
+"Sanal makine çalışıyor."
+
+#: vrtManager/instance.py:2030
+#, python-format
+msgid ""
+"The revert failed twice; try again in a moment (%(error)s). The VM is shut "
+"off."
+msgstr ""
+"Geri dönüş iki kez başarısız oldu; biraz sonra tekrar deneyin (%(error)s). "
+"Sanal makine kapalı."
+
+#: vrtManager/instance.py:2193
+msgid "A disk moves when its VM is defined and running, paused or shut off"
+msgstr ""
+"Bir disk, sanal makinesi tanımlıyken ve çalışırken, duraklatılmışken veya "
+"kapalıyken taşınır"
+
+#: vrtManager/instance.py:2195
+msgid ""
+"A disk of a VM with snapshots, a saved state or a running block job does not "
+"move"
+msgstr ""
+"Anlık görüntüleri, kaydedilmiş durumu veya çalışan bir blok işi olan sanal "
+"makinenin diski taşınmaz"
+
+#: vrtManager/instance.py:2203
+#, python-format
+msgid ""
+"%(dev)s is not a disk file of the VM, the same in its running and next "
+"start's definition"
+msgstr ""
+"%(dev)s, sanal makinenin çalışan ve sonraki başlatma tanımlarında aynı olan "
+"bir disk dosyası değil"
+
+#: vrtManager/instance.py:2208
+#, python-format
+msgid "%(dev)s does not move: only writable raw and qcow2 disks do"
+msgstr "%(dev)s taşınmaz: yalnızca yazılabilir raw ve qcow2 diskler taşınır"
+
+#: vrtManager/instance.py:2215
+#, python-format
+msgid "%(dev)s does not move: no storage pool has %(path)s"
+msgstr "%(dev)s taşınmaz: hiçbir depolama havuzunda %(path)s yok"
+
+#: vrtManager/instance.py:2217
+#, python-format
+msgid "%(dev)s does not move: it has a backing file"
+msgstr "%(dev)s taşınmaz: temel (backing) dosyası var"
+
+#: vrtManager/instance.py:2220
+#, python-format
+msgid "%(pool_name)s is not an active pool of disk files of this host"
+msgstr "%(pool_name)s bu sunucunun etkin bir disk dosyası havuzu değil"
+
+#: vrtManager/instance.py:2223
+#, python-format
+msgid "%(dev)s is in pool %(pool_name)s already"
+msgstr "%(dev)s zaten %(pool_name)s havuzunda"
+
+#: vrtManager/instance.py:2227
+#, python-format
+msgid "Pool %(pool_name)s already has a file %(name)s"
+msgstr "%(pool_name)s havuzunda zaten %(name)s dosyası var"
+
+#: vrtManager/instance.py:2251
+#, python-format
+msgid "The copy of %(dev)s ended before it was complete"
+msgstr "%(dev)s kopyası tamamlanmadan sona erdi"
+
+#: vrtManager/instance.py:2296
+#, python-format
+msgid "%(error)s; remove %(dest)s if it is there: %(cleanup_error)s"
+msgstr "%(error)s; varsa %(dest)s dosyasını kaldırın: %(cleanup_error)s"
+
+#: vrtManager/instance.py:2298
+#, python-format
+msgid "%(error)s; the VM may use %(dest)s now: check its disk %(dev)s"
+msgstr ""
+"%(error)s; sanal makine şu anda %(dest)s dosyasını kullanıyor olabilir: "
+"%(dev)s diskini kontrol edin"
+
+#: vrtManager/instance.py:2305
+#, python-format
+msgid "The VM has no internal snapshot named %(name)s"
+msgstr "Sanal makinenin %(name)s adlı dahili anlık görüntüsü yok"
+
+#: vrtManager/instance.py:2314
+msgid "The VM has a saved state; start it or remove the saved state first"
+msgstr ""
+"Sanal makinenin kaydedilmiş durumu var; önce başlatın veya kaydedilmiş "
+"durumu kaldırın"
+
+#: vrtManager/instance.py:2316
+msgid "A snapshot job is still running on this VM; try again when it ends"
+msgstr ""
+"Bu sanal makinede hâlâ bir anlık görüntü işi çalışıyor; bittiğinde tekrar "
+"deneyin"
+
+#: vrtManager/instance.py:2326
+msgid "Shut the VM down to revert to an internal snapshot"
+msgstr "Dahili bir anlık görüntüye geri dönmek için sanal makineyi kapatın"
+
+#: vrtManager/instance.py:2415
+msgid "Cannot clone a VM whose NVRAM is not a file"
+msgstr "NVRAM'i dosya olmayan bir sanal makine klonlanamaz"
+
+#: vrtManager/instance.py:2483
+#, python-format
+msgid "Clone failed (%(err)s); remove these copies by hand: %(path)s"
+msgstr ""
+"Klonlama başarısız oldu (%(err)s); bu kopyaları elle kaldırın: %(path)s"
+
+#: vrtManager/instance.py:2493
+msgid "Shut the VM down before cloning it"
+msgstr "Klonlamadan önce sanal makineyi kapatın"
+
+#: vrtManager/instance.py:2547
+#, python-format
+msgid "Clone failed (%(err)s); remove these copies by hand: %(leftovers)s"
+msgstr ""
+"Klonlama başarısız oldu (%(err)s); bu kopyaları elle kaldırın: %(leftovers)s"
+
+#: vrtManager/instance.py:2558
+msgid "Live clone needs libvirt 11.10 or later; shut the VM down to clone it"
+msgstr ""
+"Canlı klonlama libvirt 11.10 veya sonrasını gerektirir; klonlamak için sanal "
+"makineyi kapatın"
+
+#: vrtManager/instance.py:2561
+msgid "A VM with a TPM is cloned only shut down"
+msgstr "TPM'i olan bir sanal makine yalnızca kapalıyken klonlanır"
+
+#: vrtManager/instance.py:2564
+#, python-format
+msgid "Disk %(dev)s is not a local file; shut the VM down to clone it"
+msgstr ""
+"Disk %(dev)s yerel bir dosya değil; klonlamak için sanal makineyi kapatın"
+
+#: vrtManager/instance.py:2566
+#, python-format
+msgid "Disk %(dev)s is shareable; shut the VM down to clone it"
+msgstr "Disk %(dev)s paylaşılabilir; klonlamak için sanal makineyi kapatın"
+
+#: vrtManager/instance.py:2571
+#, python-format
+msgid "Disk %(dev)s changes at the next start; shut the VM down to clone it"
+msgstr ""
+"Disk %(dev)s sonraki başlatmada değişiyor; klonlamak için sanal makineyi "
+"kapatın"
+
+#: vrtManager/instance.py:2573
+msgid "Another job is running on the VM; clone it when that ends"
+msgstr "Sanal makinede başka bir iş çalışıyor; o bittiğinde klonlayın"
+
+#: vrtManager/instance.py:2586
+#, python-format
+msgid "Volume %(name)s already exists in storage pool %(pool)s"
+msgstr "%(name)s birimi %(pool)s depolama havuzunda zaten var"
+
+#: vrtManager/instance.py:2605
+#, python-format
+msgid ""
+"Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f "
+"GiB (the copied disks, plus %(percent)g%% of them, at least %(minimum)g GiB)"
+msgstr ""
+"%(pool)s depolama havuzunda %(free).1f GiB boş alan var; klon %(size).1f GiB "
+"gerektiriyor (kopyalanan diskler, artı bunların %%%(percent)g kadarı, en az "
+"%(minimum)g GiB)"
+
+#: vrtManager/instance.py:2744
+msgid "A network interface that is a host device is removed in the XML editor"
+msgstr "Sunucu aygıtı olan bir ağ arabirimi XML düzenleyicide kaldırılır"
+
+#: vrtManager/instance.py:2760
+#, python-format
+msgid "No network interface with MAC %(old_mac)s"
+msgstr "MAC adresi %(old_mac)s olan ağ arabirimi yok"
+
+#: vrtManager/instance.py:2763
+msgid "A network interface that is a host device is changed in the XML editor"
+msgstr "Sunucu aygıtı olan bir ağ arabirimi XML düzenleyicide değiştirilir"
+
+#: vrtManager/instance.py:2854
+msgid ""
+"A VM changes its PCI devices while it runs, or is shut off without a saved "
+"state"
+msgstr ""
+"Sanal makine PCI aygıtlarını çalışırken veya kaydedilmiş durumu olmadan "
+"kapalıyken değiştirir"
+
+#: vrtManager/instance.py:2866
+#, python-format
+msgid "%(err)s; whether the VM has PCI device %(address)s now is not known"
+msgstr ""
+"%(err)s; sanal makinede şu anda %(address)s PCI aygıtının olup olmadığı "
+"bilinmiyor"
+
+#: vrtManager/instance.py:2868
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the VM is not running, its next start has it"
+msgstr ""
+"%(err)s; PCI aygıtı %(address)s: sanal makine çalışmıyor, sonraki başlatmada "
+"var"
+
+#: vrtManager/instance.py:2870
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the VM is not running, its next start does "
+"not have it"
+msgstr ""
+"%(err)s; PCI aygıtı %(address)s: sanal makine çalışmıyor, sonraki başlatmada "
+"yok"
+
+#: vrtManager/instance.py:2871
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM has it, its next start has it"
+msgstr ""
+"%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede var, sonraki "
+"başlatmada var"
+
+#: vrtManager/instance.py:2873
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM has it, its next start does "
+"not have it"
+msgstr ""
+"%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede var, sonraki "
+"başlatmada yok"
+
+#: vrtManager/instance.py:2875
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM does not have it, its next "
+"start has it"
+msgstr ""
+"%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede yok, sonraki "
+"başlatmada var"
+
+#: vrtManager/instance.py:2877
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM does not have it, its next "
+"start does not have it"
+msgstr ""
+"%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede yok, sonraki "
+"başlatmada yok"
+
+#: vrtManager/instance.py:2911
+#, python-format
+msgid "The VM has no PCI device %(address)s"
+msgstr "Sanal makinede %(address)s PCI aygıtı yok"
+
+#: vrtManager/instance.py:2929
+msgid "The VM has no USB controller"
+msgstr "Sanal makinede USB denetleyicisi yok"
+
+#: vrtManager/instance.py:2948
+msgid "The VM has no such USB device"
+msgstr "Sanal makinede böyle bir USB aygıtı yok"
+
+#: vrtManager/instance.py:2989
+msgid "The VM has its own RNG device: change it in the XML editor"
+msgstr ""
+"Sanal makinenin kendi RNG aygıtı var: bunu XML düzenleyicide değiştirin"
+
+#: vrtManager/instance.py:3016
+msgid "The VM has its own TPM: change it in the XML editor"
+msgstr "Sanal makinenin kendi TPM'i var: bunu XML düzenleyicide değiştirin"
+
+#: vrtManager/instance.py:3035
+#, python-format
+msgid "Unknown crash action %(action)s"
+msgstr "Bilinmeyen çökme eylemi %(action)s"
+
+#: vrtManager/interface.py:35
+#, python-format
+msgid ""
+"%(err)s; rolling the change back failed too, the host's configuration may be "
+"changed: %(rollback_err)s"
+msgstr ""
+"%(err)s; değişikliğin geri alınması da başarısız oldu, sunucunun "
+"yapılandırması değişmiş olabilir: %(rollback_err)s"
+
+#: vrtManager/interface.py:269
+#, python-format
+msgid "Stop interface %(iface)s before deleting it"
+msgstr "%(iface)s arabirimini silmeden önce durdurun"
+
+#: vrtManager/interface.py:273
+#, python-format
+msgid ""
+"Bridge %(iface)s has addresses and several members: give the addresses to "
+"one of them on the host first"
+msgstr ""
+"%(iface)s köprüsünün adresleri ve birden fazla üyesi var: önce adresleri "
+"sunucuda üyelerden birine verin"
+
+#: vrtManager/network.py:299
+#, python-format
+msgid ""
+"Network %(net)s is in use by the running VMs %(vms)s; shut them down or "
+"remove their network cards first"
+msgstr ""
+"%(net)s ağı çalışan %(vms)s sanal makineleri tarafından kullanılıyor; önce "
+"onları kapatın veya ağ kartlarını kaldırın"
+
+#: vrtManager/network.py:305
+#, python-format
+msgid ""
+"VMs %(vms)s use network %(net)s and would no longer start; remove their "
+"network cards first"
+msgstr ""
+"%(vms)s sanal makineleri %(net)s ağını kullanıyor ve artık başlamaz; önce ağ "
+"kartlarını kaldırın"
+
+#: vrtManager/network.py:314
+#, python-format
+msgid ""
+"Network %(net)s runs with other addresses than it has saved; stop and start "
+"it first"
+msgstr ""
+"%(net)s ağı kayıtlı olandan farklı adreslerle çalışıyor; önce durdurup "
+"başlatın"
+
+#: vrtManager/network.py:339
+#, python-format
+msgid "Network %(net)s has no such address"
+msgstr "%(net)s ağında böyle bir adres yok"
+
+#: vrtManager/network.py:344
+#, python-format
+msgid "The range start %(address)s is not in %(subnet)s"
+msgstr "Aralık başlangıcı %(address)s, %(subnet)s içinde değil"
+
+#: vrtManager/network.py:345
+#, python-format
+msgid "The range start %(address)s is not in the network"
+msgstr "Aralık başlangıcı %(address)s ağın içinde değil"
+
+#: vrtManager/network.py:346
+#, python-format
+msgid "The range end %(address)s is not in %(subnet)s"
+msgstr "Aralık sonu %(address)s, %(subnet)s içinde değil"
+
+#: vrtManager/network.py:347
+#, python-format
+msgid "The range end %(address)s is not in the network"
+msgstr "Aralık sonu %(address)s ağın içinde değil"
+
+#: vrtManager/network.py:348
+#, python-format
+msgid "The fixed address %(address)s is not in %(subnet)s"
+msgstr "Sabit adres %(address)s, %(subnet)s içinde değil"
+
+#: vrtManager/network.py:349
+#, python-format
+msgid "The fixed address %(address)s is not in the network"
+msgstr "Sabit adres %(address)s ağın içinde değil"
+
+#: vrtManager/network.py:358
+#, python-format
+msgid "%(address)s is not an IP address"
+msgstr "%(address)s bir IP adresi değil"
+
+#: vrtManager/network.py:363
+#, python-format
+msgid "%(address)s is the host's own address on the network"
+msgstr "%(address)s sunucunun bu ağdaki kendi adresi"
+
+#: vrtManager/network.py:369
+#, python-format
+msgid "Stop network %(net)s to change its DHCP range"
+msgstr "DHCP aralığını değiştirmek için %(net)s ağını durdurun"
+
+#: vrtManager/network.py:373
+#, python-format
+msgid "This address of network %(net)s has no DHCP range"
+msgstr "%(net)s ağının bu adresinin DHCP aralığı yok"
+
+#: vrtManager/network.py:377
+#, python-format
+msgid "The range starts at %(start)s, after its end %(end)s"
+msgstr ""
+"Aralık, sonu olan %(end)s adresinden sonra, %(start)s adresinde başlıyor"
+
+#: vrtManager/network.py:379
+#, python-format
+msgid "The range takes in %(address)s, the host's own address"
+msgstr "Aralık, sunucunun kendi adresi olan %(address)s adresini içeriyor"
+
+#: vrtManager/network.py:388
+#, python-format
+msgid "Network %(net)s has no fixed address %(address)s"
+msgstr "%(net)s ağında %(address)s sabit adresi yok"
+
+#: vrtManager/network.py:410
+#, python-format
+msgid "The fixed address %(original)s is gone meanwhile; reload the page"
+msgstr ""
+"%(original)s sabit adresi bu arada kaldırılmış; sayfayı yeniden yükleyin"
+
+#: vrtManager/network.py:415
+#, python-format
+msgid "%(address)s is another entry's fixed address"
+msgstr "%(address)s başka bir kaydın sabit adresi"
+
+#: vrtManager/network.py:417
+#, python-format
+msgid "%(mac_duid)s has another fixed address, %(other)s"
+msgstr "%(mac_duid)s için başka bir sabit adres var: %(other)s"
+
+#: vrtManager/network.py:419
+#, python-format
+msgid "The name %(name)s has another fixed address, %(other)s"
+msgstr "%(name)s adı için başka bir sabit adres var: %(other)s"
+
+#: vrtManager/network.py:455
+msgid "Direction must be inbound or outbound"
+msgstr "Yön gelen ya da giden olmalı"
+
+#: vrtManager/network.py:482
+#, python-format
+msgid "The XML must define network %(net)s"
+msgstr "XML, %(net)s ağını tanımlamalıdır"
+
+#: vrtManager/network.py:484
+msgid "The XML must keep the network's UUID"
+msgstr "XML, ağın UUID değerini korumalıdır"
+
+#: vrtManager/nodedevs.py:11
msgid "Storage controller"
msgstr "Depolama denetleyicisi"
-#: vrtManager/nodedevs.py:11
+#: vrtManager/nodedevs.py:12
msgid "Network controller"
msgstr "Ağ denetleyicisi"
-#: vrtManager/nodedevs.py:12
+#: vrtManager/nodedevs.py:13
msgid "Display controller"
msgstr "Görüntü denetleyicisi"
-#: vrtManager/nodedevs.py:13
+#: vrtManager/nodedevs.py:14
msgid "Multimedia controller"
msgstr "Çoklu ortam denetleyicisi"
-#: vrtManager/nodedevs.py:15
+#: vrtManager/nodedevs.py:16
msgid "Serial bus controller"
msgstr "Seri veri yolu denetleyicisi"
-#: vrtManager/nodedevs.py:20
+#: vrtManager/nodedevs.py:21
msgid "Audio"
msgstr "Ses"
-#: vrtManager/nodedevs.py:21
+#: vrtManager/nodedevs.py:22
msgid "Communications"
msgstr "İletişim"
-#: vrtManager/nodedevs.py:22
+#: vrtManager/nodedevs.py:23
msgid "Human interface"
msgstr "İnsan arabirimi"
-#: vrtManager/nodedevs.py:23
+#: vrtManager/nodedevs.py:24
msgid "Imaging"
msgstr "Görüntüleme"
-#: vrtManager/nodedevs.py:24
+#: vrtManager/nodedevs.py:25
msgid "Printer"
msgstr "Yazıcı"
-#: vrtManager/nodedevs.py:25
+#: vrtManager/nodedevs.py:26
msgid "Mass storage"
msgstr "Yığın depolama"
-#: vrtManager/nodedevs.py:26
+#: vrtManager/nodedevs.py:27
msgid "Hub"
msgstr "Hub"
-#: vrtManager/nodedevs.py:27
+#: vrtManager/nodedevs.py:28
msgid "Communications data"
msgstr "İletişim verisi"
-#: vrtManager/nodedevs.py:28
+#: vrtManager/nodedevs.py:29
msgid "Smart card"
msgstr "Akıllı kart"
-#: vrtManager/nodedevs.py:30
+#: vrtManager/nodedevs.py:31
msgid "Wireless"
msgstr "Kablosuz"
-#: vrtManager/nodedevs.py:31
+#: vrtManager/nodedevs.py:32
msgid "Vendor specific"
msgstr "Üreticiye özgü"
+#: vrtManager/nodedevs.py:111
+#, python-format
+msgid "PCI device %(address)s"
+msgstr "PCI aygıtı %(address)s"
+
+#: vrtManager/nodedevs.py:115
+#, python-format
+msgid "USB device %(bus)s/%(device)s (%(product)s)"
+msgstr "USB aygıtı %(bus)s/%(device)s (%(product)s)"
+
+#: vrtManager/nodedevs.py:124
+#, python-format
+msgid "%(device)s is given to %(vms)s"
+msgstr "%(device)s, %(vms)s sanal makinesine verilmiş"
+
+#: vrtManager/nodedevs.py:126
+#, python-format
+msgid "%(device)s holds the host's %(resources)s"
+msgstr "%(device)s sunucunun %(resources)s kaynaklarını tutuyor"
+
+#: vrtManager/nodedevs.py:149
+#, python-format
+msgid "%(device)s is a bridge"
+msgstr "%(device)s bir köprü"
+
+#: vrtManager/nodedevs.py:153
+#, python-format
+msgid ""
+"bind %(address)s to vfio-pci on the host first if it is not needed there"
+msgstr ""
+"orada gerekmiyorsa önce sunucuda %(address)s aygıtını vfio-pci'ye bağlayın"
+
+#: vrtManager/nodedevs.py:155
+#, python-format
+msgid "%(device)s is in no IOMMU group"
+msgstr "%(device)s hiçbir IOMMU grubunda değil"
+
+#: vrtManager/nodedevs.py:163
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with %(member)s, which cannot be read"
+msgstr ""
+"%(device)s, %(group)s numaralı IOMMU grubunu okunamayan %(member)s ile "
+"paylaşıyor"
+
+#: vrtManager/nodedevs.py:166
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with %(member)s (%(product)s), which "
+"%(vms)s uses"
+msgstr ""
+"%(device)s, %(group)s numaralı IOMMU grubunu %(vms)s tarafından kullanılan "
+"%(member)s (%(product)s) ile paylaşıyor"
+
+#: vrtManager/nodedevs.py:171
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with the bridge %(member)s (driver "
+"%(driver)s)"
+msgstr ""
+"%(device)s, %(group)s numaralı IOMMU grubunu %(member)s köprüsüyle (sürücü "
+"%(driver)s) paylaşıyor"
+
+#: vrtManager/nodedevs.py:175
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with %(member)s (%(product)s), which "
+"the host (driver %(driver)s) uses"
+msgstr ""
+"%(device)s, %(group)s numaralı IOMMU grubunu sunucunun kullandığı %(member)s "
+"(%(product)s, sürücü %(driver)s) ile paylaşıyor"
+
+#: vrtManager/nodedevs.py:187
+#, python-format
+msgid "%(device)s is not on the host"
+msgstr "%(device)s sunucuda yok"
+
+#: vrtManager/nodedevs.py:198
+#, python-format
+msgid "%(device)s is a hub"
+msgstr "%(device)s bir hub"
+
+#: vrtManager/nodedevs.py:202
+#, python-format
+msgid "%(device)s is on the PCI controller %(controller)s, given to %(vms)s"
+msgstr ""
+"%(device)s, %(vms)s sanal makinesine verilmiş %(controller)s PCI "
+"denetleyicisinde"
+
+#: vrtManager/nodedevs.py:332
+#, python-format
+msgid "%(problems)s"
+msgstr "%(problems)s"
+
+#: vrtManager/nwfilters.py:62
+msgid "Enter one element"
+msgstr "Tek bir öğesi girin"
+
+#: vrtManager/nwfilters.py:64 vrtManager/nwfilters.py:76
+msgid ""
+"The filter's name may have letters, digits, dots, dashes and underscores"
+msgstr "Filtre adı harf, rakam, nokta, tire ve alt çizgi içerebilir"
+
+#: vrtManager/nwfilters.py:67
+msgid "A network filter with this name already exists"
+msgstr "Bu adla bir ağ filtresi zaten var"
+
+#: vrtManager/nwfilters.py:69
+msgid "A network filter with this UUID already exists"
+msgstr "Bu UUID ile bir ağ filtresi zaten var"
+
+#: vrtManager/nwfilters.py:78
+#, python-format
+msgid "A network filter named %(clone_name)s already exists"
+msgstr "%(clone_name)s adlı bir ağ filtresi zaten var"
+
+#: vrtManager/nwfilters.py:90
+#, python-format
+msgid "Filter %(name)s is used by VM %(vms)s"
+msgstr "%(name)s filtresi %(vms)s sanal makinesi tarafından kullanılıyor"
+
+#: vrtManager/nwfilters.py:93
+#, python-format
+msgid "Filter %(name)s is referenced by filter %(referencing)s"
+msgstr "%(name)s filtresine %(referencing)s filtresi başvuruyor"
+
+#: vrtManager/nwfilters.py:159
+msgid "The filter has changed; reload the page and try again"
+msgstr "Filtre değişti; sayfayı yeniden yükleyip tekrar deneyin"
+
+#: vrtManager/nwfilters.py:169
+#, python-format
+msgid "The XML must define filter %(name)s"
+msgstr "XML, %(name)s filtresini tanımlamalıdır"
+
+#: vrtManager/nwfilters.py:171
+msgid "The XML must keep the filter's UUID"
+msgstr "XML, filtrenin UUID değerini korumalıdır"
+
+#: vrtManager/nwfilters.py:183
+#, python-format
+msgid ""
+"Filter %(name)s cannot be referenced: it is this one, referenced already or "
+"not on the host"
+msgstr ""
+"%(name)s filtresine başvurulamaz: bu filtrenin kendisi, zaten başvurulmuş "
+"veya sunucuda yok"
+
+#: vrtManager/nwfilters.py:193
+msgid "Enter one element"
+msgstr "Tek bir öğesi girin"
+
+#: vrtManager/storage.py:192
+#, python-format
+msgid ""
+"Pool %(name)s holds disks of the running VMs %(vms)s; shut them down first"
+msgstr ""
+"%(name)s havuzu çalışan %(vms)s sanal makinelerinin disklerini tutuyor; önce "
+"onları kapatın"
+
+#: vrtManager/storage.py:200
+#, python-format
+msgid ""
+"VMs %(vms)s have disks in pool %(name)s by its name and would no longer "
+"start; detach those disks first"
+msgstr ""
+"%(vms)s sanal makinelerinin %(name)s havuzunda adıyla bağlı diskleri var ve "
+"artık başlamaz; önce bu diskleri ayırın"
+
+#: vrtManager/storage.py:279
+#, python-format
+msgid ""
+"Volume %(name)s is in use: a VM's disk, a backing file or a snapshot's file"
+msgstr ""
+"%(name)s birimi kullanımda: bir sanal makinenin diski, bir temel (backing) "
+"dosya ya da bir anlık görüntünün dosyası"
+
+#: vrtManager/util.py:140
+#, python-format
+msgid "The XML is not valid: %(err)s"
+msgstr "XML geçerli değil: %(err)s"
+
+#: vrtManager/util.py:142
+msgid "The XML may not declare a DTD"
+msgstr "XML bir DTD bildiremez"
+
+#: vrtManager/virtsecrets.py:26
+msgid "A Ceph key is base64 text, as `ceph auth get-key` prints it"
+msgstr ""
+"Ceph anahtarı, `ceph auth get-key` komutunun yazdırdığı gibi base64 metindir"
+
+#: vrtManager/virtsecrets.py:115
+#, python-format
+msgid "VMs %(names)s"
+msgstr "%(names)s sanal makineleri"
+
+#: vrtManager/virtsecrets.py:117
+#, python-format
+msgid "pools %(names)s"
+msgstr "%(names)s havuzları"
+
+#: vrtManager/virtsecrets.py:118
+#, python-format
+msgid "Secret %(uuid)s is used by %(named)s; remove it from them first"
+msgstr ""
+"%(uuid)s parolası %(named)s tarafından kullanılıyor; önce onlardan kaldırın"
+
#: webvirtcloud/settings.py:325
msgid "Localhost"
msgstr "Yerel sunucu"
diff --git a/locale/zh/LC_MESSAGES/django.mo b/locale/zh/LC_MESSAGES/django.mo
index 06264dc3..4367d5e7 100644
Binary files a/locale/zh/LC_MESSAGES/django.mo and b/locale/zh/LC_MESSAGES/django.mo differ
diff --git a/locale/zh/LC_MESSAGES/django.po b/locale/zh/LC_MESSAGES/django.po
index 8aa01419..c09c7aef 100644
--- a/locale/zh/LC_MESSAGES/django.po
+++ b/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-02 20:42+0000\n"
+"POT-Creation-Date: 2026-11-02 21:03+0000\n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
@@ -1671,8 +1671,8 @@ msgstr "已保存:%(settings)s"
msgid "Nothing changed"
msgstr "没有任何更改"
-#: appsettings/views.py:122 vrtManager/nodedevs.py:195
-#: vrtManager/nodedevs.py:234
+#: appsettings/views.py:122 vrtManager/nodedevs.py:219
+#: vrtManager/nodedevs.py:258
msgid "Other"
msgstr "其他"
@@ -2633,7 +2633,7 @@ msgstr "虚拟机未定义;请检查这些卷,然后处理此任务:%(volu
msgid "Delete snapshot"
msgstr "删除快照"
-#: instances/jobs.py:303
+#: instances/jobs.py:303 vrtManager/instance.py:2304
#, python-format
msgid "The VM has no external snapshot named %(name)s"
msgstr "虚拟机没有名为 %(name)s 的外部快照"
@@ -2667,7 +2667,7 @@ msgstr "虚拟机已不在运行该任务的计算节点上"
msgid "Take snapshot"
msgstr "创建快照"
-#: instances/jobs.py:371
+#: instances/jobs.py:371 vrtManager/instance.py:1900
#, python-format
msgid "A snapshot named %(name)s already exists"
msgstr "名为 %(name)s 的快照已存在"
@@ -2719,15 +2719,15 @@ msgstr ""
msgid "Clone VM"
msgstr "克隆虚拟机"
-#: instances/jobs.py:542 instances/jobs.py:549 instances/jobs.py:805
+#: instances/jobs.py:542 instances/jobs.py:549 instances/jobs.py:806
msgid "Copying disks"
msgstr "正在复制磁盘"
-#: instances/jobs.py:600
+#: instances/jobs.py:601
msgid "libvirt has no result of the copy"
msgstr "libvirt 没有复制的结果"
-#: instances/jobs.py:611
+#: instances/jobs.py:612
#, python-format
msgid ""
"The copy failed: %(error)s. The guest's file systems could not be thawed "
@@ -2736,11 +2736,11 @@ msgstr ""
"复制失败:%(error)s。无法解冻客户机的文件系统(%(thaw)s);请用以下命令解冻:"
"virsh domfsthaw %(vm)s"
-#: instances/jobs.py:614 instances/jobs.py:698
+#: instances/jobs.py:615 instances/jobs.py:699
msgid "The copy failed"
msgstr "复制失败"
-#: instances/jobs.py:639
+#: instances/jobs.py:640
#, python-format
msgid ""
"The VM is no longer on the compute the job ran on; check these volumes, then "
@@ -2748,163 +2748,163 @@ msgid ""
msgstr ""
"虚拟机已不在运行该任务的计算节点上;请检查这些卷,然后处理此任务:%(volumes)s"
-#: instances/jobs.py:674
+#: instances/jobs.py:675
#, 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:678
+#: instances/jobs.py:679
msgid "The job stopped before the copy started"
msgstr "任务在复制开始前停止"
-#: instances/jobs.py:685
+#: instances/jobs.py:686
msgid "libvirt is still copying the disks"
msgstr "libvirt 仍在复制磁盘"
-#: instances/jobs.py:688
+#: instances/jobs.py:689
#, python-format
msgid ""
"Another backup runs on the VM; check these volumes, then resolve this job: "
"%(volumes)s"
msgstr "虚拟机上正在运行另一个备份;请检查这些卷,然后处理此任务:%(volumes)s"
-#: instances/jobs.py:693
+#: instances/jobs.py:694
#, 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:721 instances/templates/create_instance_w1.html:60
+#: instances/jobs.py:722 instances/templates/create_instance_w1.html:60
#: instances/templates/create_instance_w2.html:356
msgid "Create VM"
msgstr "创建虚拟机"
-#: instances/jobs.py:744 instances/views.py:2477
+#: instances/jobs.py:745 instances/views.py:2481
msgid "A virtual machine with this name already exists"
msgstr "同名虚拟机已存在"
-#: instances/jobs.py:746
+#: instances/jobs.py:747
msgid "There is an instance with same name. Remove it and try again!"
msgstr "已存在同名虚拟机。请删除后重试!"
-#: instances/jobs.py:749
+#: instances/jobs.py:750
msgid "First you need to create or select an image"
msgstr "请先创建或选择一个镜像"
-#: instances/jobs.py:751
+#: instances/jobs.py:752
msgid "Choose one disk source: a new disk, existing volumes or a template"
msgstr "请选择一种磁盘来源:新磁盘、现有卷或模板"
-#: instances/jobs.py:753
+#: instances/jobs.py:754
msgid "Image has already exist. Please check volumes or change instance name"
msgstr "镜像已存在。请检查卷或更改虚拟机名称"
-#: instances/jobs.py:755
+#: instances/jobs.py:756
msgid "Invalid cache mode"
msgstr "无效的缓存模式"
-#: instances/jobs.py:757
+#: instances/jobs.py:758
#, python-format
msgid "This host offers no CPU mode or model %(mode)s"
msgstr "此主机不提供 CPU 模式或型号 %(mode)s"
-#: instances/jobs.py:759
+#: instances/jobs.py:760
#, python-format
msgid "A CD-ROM cannot use the %(bus)s bus"
msgstr "CD-ROM 不能使用 %(bus)s 总线"
-#: instances/jobs.py:762
+#: instances/jobs.py:763
msgid "The cloud-init seed is read from a CD-ROM: choose its bus"
msgstr "cloud-init 种子从 CD-ROM 读取:请选择其总线"
-#: instances/jobs.py:765
+#: instances/jobs.py:766
msgid "The cloud-init seed needs a directory storage pool for the disk"
msgstr "cloud-init 种子需要为磁盘使用目录类型的存储池"
-#: instances/jobs.py:771
+#: instances/jobs.py:772
#, python-format
msgid "This host offers no firmware %(firmware)s"
msgstr "此主机不提供固件 %(firmware)s"
-#: instances/jobs.py:773
+#: instances/jobs.py:774 vrtManager/instance.py:3023
msgid "This host offers no TPM 2.0: install swtpm on it"
msgstr "此主机不提供 TPM 2.0:请在主机上安装 swtpm"
-#: instances/jobs.py:824
+#: instances/jobs.py:825
msgid "Writing the cloud-init seed"
msgstr "正在写入 cloud-init 种子"
-#: instances/jobs.py:831
+#: instances/jobs.py:832
msgid "Defining the VM"
msgstr "正在定义虚拟机"
-#: instances/jobs.py:864
+#: instances/jobs.py:865
#, python-format
msgid "%(error)s. Remove these volumes by hand: %(volumes)s"
msgstr "%(error)s。请手动删除这些卷:%(volumes)s"
-#: instances/jobs.py:889
+#: instances/jobs.py:890
#, python-format
msgid "The instance is already on %(compute)s"
msgstr "虚拟机已在 %(compute)s 上"
-#: instances/jobs.py:891
+#: instances/jobs.py:892
#, python-format
msgid "%(compute)s is not reachable"
msgstr "无法连接 %(compute)s"
-#: instances/jobs.py:893
+#: instances/jobs.py:894
#, python-format
msgid "%(compute)s is the VM's own host under another name"
msgstr "%(compute)s 是该虚拟机所在主机的另一个名称"
-#: instances/jobs.py:911
+#: instances/jobs.py:912
msgid "Migrate VM"
msgstr "迁移虚拟机"
-#: instances/jobs.py:955
+#: instances/jobs.py:956
msgid "Migrating the VM"
msgstr "正在迁移虚拟机"
-#: instances/jobs.py:989
+#: instances/jobs.py:993
msgid "libvirt is still migrating the VM"
msgstr "libvirt 仍在迁移虚拟机"
-#: instances/jobs.py:996
+#: instances/jobs.py:1000
msgid "The VM stayed on the source host"
msgstr "虚拟机留在了源主机上"
-#: instances/jobs.py:998
+#: instances/jobs.py:1002
msgid ""
"The VM is on both computes or on neither; check them, then resolve this job."
msgstr "虚拟机同时位于两个计算节点上,或都不在;请检查后处理此任务。"
-#: instances/jobs.py:1030
+#: instances/jobs.py:1034
msgid "Move disk"
msgstr "移动磁盘"
-#: instances/jobs.py:1037
+#: instances/jobs.py:1041
msgid "Choose a disk of the VM"
msgstr "请选择虚拟机的一个磁盘"
-#: instances/jobs.py:1061
+#: instances/jobs.py:1065
msgid "Copying the disk"
msgstr "正在复制磁盘"
-#: instances/jobs.py:1104
+#: instances/jobs.py:1108
#, python-format
msgid "Another block job runs on %(dev)s; check the VM"
msgstr "%(dev)s 上正在运行另一个块任务;请检查虚拟机"
-#: instances/jobs.py:1110
+#: instances/jobs.py:1114
#, python-format
msgid "The disk stayed in %(path)s; remove %(dest)s if it is there"
msgstr "磁盘仍在 %(path)s;如果 %(dest)s 存在,请将其删除"
-#: instances/jobs.py:1112
+#: instances/jobs.py:1116
#, python-format
msgid "The definitions of %(dev)s name %(paths)s; check the VM"
msgstr "%(dev)s 的定义指向 %(paths)s;请检查虚拟机"
@@ -3142,7 +3142,7 @@ msgid "Disk source"
msgstr "磁盘来源"
#: instances/templates/add_instance_volume.html:21
-#: instances/templates/create_instance_w2.html:134 instances/views.py:2496
+#: instances/templates/create_instance_w2.html:134 instances/views.py:2500
msgid "New disk"
msgstr "新磁盘"
@@ -3506,7 +3506,7 @@ msgstr "自定义 CPU 型号"
msgid "Size (GiB)"
msgstr "大小(GiB)"
-#: instances/templates/create_instance_w2.html:161 instances/views.py:2497
+#: instances/templates/create_instance_w2.html:161 instances/views.py:2501
msgid "Existing volumes"
msgstr "已有卷"
@@ -3541,7 +3541,7 @@ msgstr "设备"
msgid "Remove"
msgstr "移除"
-#: instances/templates/create_instance_w2.html:194 instances/views.py:2498
+#: instances/templates/create_instance_w2.html:194 instances/views.py:2502
msgid "Clone from template"
msgstr "从模板克隆"
@@ -3582,7 +3582,7 @@ msgstr "网卡类型"
msgid "Network filter"
msgstr "网络过滤器"
-#: instances/templates/create_instance_w2.html:293 vrtManager/nodedevs.py:29
+#: instances/templates/create_instance_w2.html:293 vrtManager/nodedevs.py:30
msgid "Video"
msgstr "显卡"
@@ -4136,44 +4136,44 @@ msgid ""
"Another disk of the VM has this file name: copy them to different pools."
msgstr "虚拟机的另一块磁盘使用相同文件名:请将它们复制到不同的存储池。"
-#: instances/templates/instances/migrate_plan.html:28
+#: instances/templates/instances/migrate_plan.html:29
#, python-format
msgid "Cannot be copied: %(reason)s"
msgstr "无法复制:%(reason)s"
-#: instances/templates/instances/migrate_plan.html:30
+#: instances/templates/instances/migrate_plan.html:31
msgid "Every pool of the destination host has a file of this name"
msgstr "目标主机的每个存储池都有同名文件"
-#: instances/templates/instances/migrate_plan.html:32
+#: instances/templates/instances/migrate_plan.html:33
msgid "No pool of the destination host can take it"
msgstr "目标主机没有能容纳它的存储池"
-#: instances/templates/instances/migrate_plan.html:38
+#: instances/templates/instances/migrate_plan.html:39
msgid "Disks of the next start"
msgstr "下次启动时的磁盘"
-#: instances/templates/instances/migrate_plan.html:39
+#: instances/templates/instances/migrate_plan.html:40
msgid "The VM gets these at its next start; the migration does not copy them."
msgstr "虚拟机会在下次启动时使用这些磁盘;迁移不会复制它们。"
-#: instances/templates/instances/migrate_plan.html:43
+#: instances/templates/instances/migrate_plan.html:44
msgid "on storage both hosts share"
msgstr "位于两台主机共享的存储上"
-#: instances/templates/instances/migrate_plan.html:44
+#: instances/templates/instances/migrate_plan.html:45
msgid ""
"not on storage both hosts share: detach it, or move it to shared storage "
"first"
msgstr "不在两台主机共享的存储上:请先分离它,或将其移到共享存储"
-#: instances/templates/instances/migrate_plan.html:51
+#: instances/templates/instances/migrate_plan.html:52
msgid ""
"A copy is made during the migration and takes as long as the disk's data "
"needs. Disks on network storage stay where they are."
msgstr "迁移期间会复制一份,耗时取决于磁盘数据量。网络存储上的磁盘保留在原处。"
-#: instances/templates/instances/migrate_plan.html:52
+#: instances/templates/instances/migrate_plan.html:53
msgid ""
"A shut-off VM is started paused for a copy, its CPUs never run, and is shut "
"off on the destination."
@@ -5859,7 +5859,7 @@ msgstr "启动顺序已成功更改。"
msgid "The XML is empty"
msgstr "XML 为空"
-#: instances/views.py:1870 instances/views.py:2487
+#: instances/views.py:1870 instances/views.py:2491
#, python-format
msgid "The XML is not well-formed: %(error)s"
msgstr "XML 格式不正确:%(error)s"
@@ -5908,21 +5908,21 @@ msgstr ""
"运行中的虚拟机没有可用 PCI 插槽来添加网络设备:请关机后再添加设备。“磁盘”选项"
"卡上的“添加 PCIe 插槽”可使其从下次启动起获得可用插槽。"
-#: instances/views.py:2080 instances/views.py:2086
+#: instances/views.py:2084 instances/views.py:2090
msgid "The VM has it"
msgstr "虚拟机已有此设备"
-#: instances/views.py:2117
+#: instances/views.py:2121
#, python-format
msgid "The host has no USB device %(device)s"
msgstr "主机上没有 USB 设备 %(device)s"
-#: instances/views.py:2138
+#: instances/views.py:2142
#, python-format
msgid "The host has no PCI device %(device)s"
msgstr "主机上没有 PCI 设备 %(device)s"
-#: instances/views.py:2156
+#: instances/views.py:2160
#, python-format
msgid ""
"Removal of PCI device %(address)s requested: the guest has not let it go "
@@ -5931,116 +5931,116 @@ msgstr ""
"已请求移除 PCI 设备 %(address)s:客户机尚未释放它;在客户机释放或虚拟机停止之"
"前,它仍保留在运行中的虚拟机中。"
-#: instances/views.py:2201 instances/views.py:2231
+#: instances/views.py:2205 instances/views.py:2235
msgid "Select a network interface and a QoS direction"
msgstr "请选择网络接口和 QoS 方向"
-#: instances/views.py:2204
+#: instances/views.py:2208 vrtManager/network.py:459
msgid "QoS rates must be whole numbers"
msgstr "QoS 速率必须为整数"
-#: instances/views.py:2211 networks/views.py:229
+#: instances/views.py:2215 networks/views.py:229
#, python-format
msgid "%(qos_dir)s QoS is set"
msgstr "%(qos_dir)s QoS 已设置"
-#: instances/views.py:2216
+#: instances/views.py:2220
#, python-format
msgid ""
"%(qos_dir)s QoS is set. A running VM gets it after it is powered off and on."
msgstr "%(qos_dir)s QoS 已设置。运行中的虚拟机需关机再开机后才能生效。"
-#: instances/views.py:2238 networks/views.py:255
+#: instances/views.py:2242 networks/views.py:255
#, python-format
msgid "%(qos_dir)s QoS is deleted"
msgstr "%(qos_dir)s QoS 已删除"
-#: instances/views.py:2243
+#: instances/views.py:2247
#, python-format
msgid ""
"%(qos_dir)s QoS is deleted. A running VM keeps the old limit until it is "
"powered off and on."
msgstr "%(qos_dir)s QoS 已删除。运行中的虚拟机在关机再开机之前保留旧的限制。"
-#: instances/views.py:2271
+#: instances/views.py:2275
msgid "Only one owner is allowed and the one already added"
msgstr "只允许一个所有者,且已添加"
-#: instances/views.py:2273
+#: instances/views.py:2277
#, python-format
msgid "%(user)s is already an owner"
msgstr "%(user)s 已是所有者"
-#: instances/views.py:2317
+#: instances/views.py:2321
#, python-format
msgid "Console settings were not saved: %(errors)s"
msgstr "控制台设置未保存:%(errors)s"
-#: instances/views.py:2339
+#: instances/views.py:2343
msgid ""
"Error setting console password. You should check that your instance have an "
"graphic device."
msgstr "设置控制台密码出错。请检查虚拟机是否有图形设备。"
-#: instances/views.py:2386
+#: instances/views.py:2390
msgid "Shut the VM down to change whether it is a template."
msgstr "请关闭虚拟机以更改其是否为模板。"
-#: instances/views.py:2393
+#: instances/views.py:2397
#, python-format
msgid "User %(quota_msg)s quota reached, '%(instance_name)s' stays a template!"
msgstr "已达到用户 %(quota_msg)s 配额,“%(instance_name)s”仍为模板!"
-#: instances/views.py:2475
+#: instances/views.py:2479
msgid "The XML has no name"
msgstr "XML 中没有名称"
-#: instances/views.py:2489 webvirtcloud/middleware.py:79
+#: instances/views.py:2493 webvirtcloud/middleware.py:79
#, python-format
msgid "libvirt Error - %(exception)s"
msgstr "libvirt 错误 - %(exception)s"
-#: instances/views.py:2496
+#: instances/views.py:2500
msgid "An empty disk of the size you set"
msgstr "按设定大小创建的空磁盘"
-#: instances/views.py:2497
+#: instances/views.py:2501
msgid "Volumes already in a pool"
msgstr "存储池中已有的卷"
-#: instances/views.py:2498
+#: instances/views.py:2502
msgid "A copy of a template volume"
msgstr "模板卷的副本"
-#: instances/views.py:2587
+#: instances/views.py:2591
msgid "You haven't defined any storage pools"
msgstr "您尚未定义任何存储池"
-#: instances/views.py:2589
+#: instances/views.py:2593
msgid "You haven't defined any network pools"
msgstr "您尚未定义任何网络池"
-#: instances/views.py:2652
+#: instances/views.py:2656
msgid "Flavor Created"
msgstr "规格已创建"
-#: instances/views.py:2660
+#: instances/views.py:2664
msgid "Create Flavor"
msgstr "创建规格"
-#: instances/views.py:2675
+#: instances/views.py:2679
msgid "Flavor Updated"
msgstr "规格已更新"
-#: instances/views.py:2681
+#: instances/views.py:2685
msgid "Update Flavor"
msgstr "更新规格"
-#: instances/views.py:2693
+#: instances/views.py:2697
msgid "Flavor Deleted"
msgstr "规格已删除"
-#: instances/views.py:2699
+#: instances/views.py:2703
msgid "Only the size template goes; VMs made from it keep their size."
msgstr "仅删除此规格;由它创建的虚拟机保持其大小。"
@@ -6143,7 +6143,7 @@ msgstr "位于 %(host)s"
#: networks/templates/create_net_block.html:34
#: networks/templates/create_net_block.html:90
#: networks/templates/network.html:62 networks/views.py:25
-#: vrtManager/nodedevs.py:14
+#: vrtManager/nodedevs.py:15
msgid "Bridge"
msgstr "网桥"
@@ -7481,12 +7481,12 @@ msgstr "取消失败:无法连接到服务器。"
msgid "The host of this virtual machine cannot be reached right now."
msgstr "当前无法连接此虚拟机所在的主机。"
-#: operations/utils.py:146
+#: operations/utils.py:157
msgid ""
"A short operation is in progress on this resource; try again in a moment."
msgstr "此资源上正在进行一项短时操作,请稍后重试。"
-#: operations/utils.py:149
+#: operations/utils.py:160
#, python-format
msgid ""
"%(label)s is %(state)s on this resource (started by %(user)s, %(since)s "
@@ -7495,7 +7495,7 @@ msgstr ""
"此资源上的%(label)s%(state)s(由 %(user)s 于 %(since)s前发起),请查看任务页"
"面。"
-#: operations/utils.py:151
+#: operations/utils.py:162
#, python-format
msgid "%(label)s is %(state)s on this resource (since %(since)s)."
msgstr "此资源上的%(label)s%(state)s(自 %(since)s起)。"
@@ -8403,70 +8403,931 @@ msgid ""
"encrypted anew."
msgstr "新值会立即替换旧值;之前用口令加密的内容不会重新加密。"
-#: vrtManager/nodedevs.py:10
+#: vrtManager/connection.py:200
+msgid "the connection closed right after opening"
+msgstr "连接在打开后立即关闭"
+
+#: vrtManager/instance.py:134
+msgid "A running or paused VM cannot be migrated offline"
+msgstr "运行中或已暂停的虚拟机不能离线迁移"
+
+#: vrtManager/instance.py:137
+msgid "The VM can be migrated when it is running, paused or shut off"
+msgstr "虚拟机在运行、暂停或关机时可以迁移"
+
+#: vrtManager/instance.py:160
+msgid "A VM with snapshots cannot be migrated"
+msgstr "有快照的虚拟机不能迁移"
+
+#: vrtManager/instance.py:163
+msgid "A VM with a saved state cannot be migrated"
+msgstr "有保存的状态的虚拟机不能迁移"
+
+#: vrtManager/instance.py:170
+msgid "A VM with host devices cannot be migrated; remove them first"
+msgstr "有主机设备的虚拟机不能迁移;请先移除它们"
+
+#: vrtManager/instance.py:176
+#, python-format
+msgid "The destination host does not have these disks of the VM: %(disks)s"
+msgstr "目标主机没有该虚拟机的这些磁盘:%(disks)s"
+
+#: vrtManager/instance.py:177
+#, python-format
+msgid "%(target)s (%(source)s)"
+msgstr "%(target)s(%(source)s)"
+
+#: vrtManager/instance.py:185
+msgid "Destination host emulator is different. Cannot be migrated"
+msgstr "目标主机的模拟器不同,无法迁移"
+
+#: vrtManager/instance.py:316
+msgid "an image is not copied; eject it first"
+msgstr "镜像不会被复制;请先弹出它"
+
+#: vrtManager/instance.py:319
+msgid "a read-only or encrypted disk is not copied"
+msgstr "只读或加密的磁盘不会被复制"
+
+#: vrtManager/instance.py:322
+msgid "only raw and qcow2 disks are copied"
+msgstr "只复制 raw 和 qcow2 磁盘"
+
+#: vrtManager/instance.py:324
+msgid "no storage pool of the source host has it"
+msgstr "源主机的任何存储池中都没有它"
+
+#: vrtManager/instance.py:326
+msgid "a disk with a backing file is not copied"
+msgstr "有后备文件的磁盘不会被复制"
+
+#: vrtManager/instance.py:346
+#, python-format
+msgid ""
+"%(target)s (%(path)s): the destination host does not share it; choose a pool "
+"to copy it to"
+msgstr "%(target)s (%(path)s):目标主机不共享它;请选择要复制到的存储池"
+
+#: vrtManager/instance.py:350
+#, python-format
+msgid "%(target)s (%(path)s): %(refusal)s"
+msgstr "%(target)s (%(path)s):%(refusal)s"
+
+#: vrtManager/instance.py:355
+#, python-format
+msgid ""
+"%(target)s (%(path)s): the destination host has no pool %(pool)s for disk "
+"files"
+msgstr "%(target)s (%(path)s):目标主机没有用于磁盘文件的存储池 %(pool)s"
+
+#: vrtManager/instance.py:362
+#, python-format
+msgid ""
+"%(target)s (%(path)s): pool %(pool)s of the destination host already has a "
+"file %(file)s"
+msgstr "%(target)s (%(path)s):目标主机的存储池 %(pool)s 中已有文件 %(file)s"
+
+#: vrtManager/instance.py:366
+#, python-format
+msgid "%(target)s (%(path)s): another disk is copied to %(dest)s"
+msgstr "%(target)s (%(path)s):另一个磁盘已复制到 %(dest)s"
+
+#: vrtManager/instance.py:371
+#, python-format
+msgid "%(target)s: the VM has no disk file to copy"
+msgstr "%(target)s:虚拟机没有可复制的磁盘文件"
+
+#: vrtManager/instance.py:375
+#, python-format
+msgid ""
+"%(target)s (%(path)s) of the next start: the destination host does not share "
+"it"
+msgstr "下次启动的 %(target)s (%(path)s):目标主机不共享它"
+
+#: vrtManager/instance.py:379
+#, python-format
+msgid "The disks cannot be migrated: %(problems)s"
+msgstr "磁盘无法迁移:%(problems)s"
+
+#: vrtManager/instance.py:392
+#, python-format
+msgid ""
+"%(error)s; remove the partial disk copies from the destination host: "
+"%(copies)s"
+msgstr "%(error)s;请从目标主机删除不完整的磁盘副本:%(copies)s"
+
+#: vrtManager/instance.py:409
+#, python-format
+msgid ""
+"%(error)s; remove the partial disk copies from the destination host: %(left)s"
+msgstr "%(error)s;请从目标主机删除不完整的磁盘副本:%(left)s"
+
+#: vrtManager/instance.py:426
+#, python-format
+msgid "The destination host cannot see the NVRAM file %(path)s"
+msgstr "目标主机看不到 NVRAM 文件 %(path)s"
+
+#: vrtManager/instance.py:494
+#, python-format
+msgid "%(type)s disk, cannot be checked"
+msgstr "%(type)s 磁盘,无法检查"
+
+#: vrtManager/instance.py:534
+#, python-format
+msgid "The destination host already has a VM %(key)s"
+msgstr "目标主机上已有虚拟机 %(key)s"
+
+#: vrtManager/instance.py:556
+msgid "The VM was started on the source host during the migration"
+msgstr "迁移期间虚拟机在源主机上被启动"
+
+#: vrtManager/instance.py:572
+#, python-format
+msgid ""
+"%(error)s; the VM may now be defined only on the destination host: check "
+"both hosts"
+msgstr "%(error)s;虚拟机现在可能只在目标主机上定义:请检查两台主机"
+
+#: vrtManager/instance.py:578
+#, python-format
+msgid ""
+"%(error)s; the VM is now defined on both hosts, remove it from the "
+"destination: %(undo_error)s"
+msgstr ""
+"%(error)s;虚拟机现在在两台主机上都有定义,请从目标主机删除它:%(undo_error)s"
+
+#: vrtManager/instance.py:584
+msgid ""
+"Snapshot names are 1-64 letters, digits, '.', '_' or '-' and do not start "
+"with '.'"
+msgstr "快照名称为 1-64 个字母、数字、'.'、'_' 或 '-',且不能以 '.' 开头"
+
+#: vrtManager/instance.py:1100
+msgid ""
+"A device other than a disk or NIC is in the boot order: change it in the XML "
+"editor"
+msgstr "启动顺序中有磁盘或网卡以外的设备:请在 XML 编辑器中修改"
+
+#: vrtManager/instance.py:1112
+msgid "Invalid Device Type for boot order"
+msgstr "启动顺序的设备类型无效"
+
+#: vrtManager/instance.py:1115
+#, python-format
+msgid "Boot device %(dev)s is not in the definition"
+msgstr "启动设备 %(dev)s 不在定义中"
+
+#: vrtManager/instance.py:1135
+#, python-format
+msgid "CD-ROM %(dev)s is not in the running definition"
+msgstr "CD-ROM %(dev)s 不在运行中的定义中"
+
+#: vrtManager/instance.py:1136
+#, python-format
+msgid "CD-ROM %(dev)s is not in the persistent definition"
+msgstr "CD-ROM %(dev)s 不在持久定义中"
+
+#: vrtManager/instance.py:1148
+#, python-format
+msgid "Media of %(dev)s changed in the running VM only: %(err)s"
+msgstr "%(dev)s 的介质仅在运行中的虚拟机中更改:%(err)s"
+
+#: vrtManager/instance.py:1159
+#, python-format
+msgid "ISO image %(image)s was not found"
+msgstr "未找到 ISO 镜像 %(image)s"
+
+#: vrtManager/instance.py:1161
+#, python-format
+msgid "ISO image %(image)s is in more than one storage pool"
+msgstr "ISO 镜像 %(image)s 位于多个存储池中"
+
+#: vrtManager/instance.py:1268
+#, python-format
+msgid "Disk %(target_dev)s is not in the VM's definition"
+msgstr "磁盘 %(target_dev)s 不在虚拟机的定义中"
+
+#: vrtManager/instance.py:1396
+msgid "Please shutdown the instance then try to enable vCPU hotplug"
+msgstr "请先关闭虚拟机,再启用 vCPU 热插拔"
+
+#: vrtManager/instance.py:1859
+msgid ""
+"The VM has external snapshots; internal snapshots cannot be mixed with them"
+msgstr "虚拟机有外部快照;内部快照不能与其混用"
+
+#: vrtManager/instance.py:1891
+msgid "A snapshot with memory is not quiesced; it keeps the whole guest state"
+msgstr "含内存的快照不会静默文件系统;它保存整个客户机状态"
+
+#: vrtManager/instance.py:1893
+msgid "Only a running VM's file systems can be frozen"
+msgstr "只有运行中的虚拟机的文件系统可以冻结"
+
+#: vrtManager/instance.py:1895
+msgid "The QEMU guest agent is not connected; it freezes the file systems"
+msgstr "QEMU 客户机代理未连接;文件系统由它冻结"
+
+#: vrtManager/instance.py:1898
+msgid ""
+"The VM has internal snapshots; external snapshots cannot be mixed with them"
+msgstr "虚拟机有内部快照;外部快照不能与其混用"
+
+#: vrtManager/instance.py:1903
+msgid "Newer snapshots exist after the last revert; delete them first"
+msgstr "上次还原后存在更新的快照;请先删除它们"
+
+#: vrtManager/instance.py:1906
+msgid "No snapshot is current; revert to a snapshot first"
+msgstr "没有当前快照;请先还原到某个快照"
+
+#: vrtManager/instance.py:1912
+#, python-format
+msgid "Eject the media from %(dev)s first"
+msgstr "请先弹出 %(dev)s 中的介质"
+
+#: vrtManager/instance.py:1914
+#, python-format
+msgid "Disk %(dev)s is not a file; external snapshots need file disks"
+msgstr "磁盘 %(dev)s 不是文件;外部快照需要文件磁盘"
+
+#: vrtManager/instance.py:1922
+#, python-format
+msgid ""
+"The storage pools could not be refreshed, so their free space is unknown: "
+"%(err)s"
+msgstr "无法刷新存储池,其可用空间未知:%(err)s"
+
+#: vrtManager/instance.py:1943
+msgid "Memory is saved only from a running VM"
+msgstr "只能保存运行中的虚拟机的内存"
+
+#: vrtManager/instance.py:1945
+msgid "The VM has no disk to keep its memory next to"
+msgstr "虚拟机没有可存放其内存的磁盘"
+
+#: vrtManager/instance.py:1949
+#, python-format
+msgid "%(disk)s is not in a storage pool; memory is saved only into one"
+msgstr "%(disk)s 不在存储池中;内存只能保存到存储池中"
+
+#: vrtManager/instance.py:1980
+#, python-format
+msgid ""
+"Storage pool %(pool)s has %(free).1f GiB free; the snapshot needs %(size).1f "
+"GiB (%(percent)g%% of the VM's disks in it, at least %(minimum)g GiB, and "
+"the memory if it is saved)"
+msgstr ""
+"存储池 %(pool)s 可用 %(free).1f GiB;快照需要 %(size).1f GiB(其中虚拟机磁盘"
+"的 %(percent)g%%,至少 %(minimum)g GiB,若保存内存则另加内存)"
+
+#: vrtManager/instance.py:1998
+msgid ""
+"This snapshot was taken by an older version on a UEFI VM and cannot be "
+"reverted; delete it instead"
+msgstr "此快照由旧版本在 UEFI 虚拟机上创建,无法还原;请将其删除"
+
+#: vrtManager/instance.py:2028
+#, python-format
+msgid ""
+"The revert failed twice; try again in a moment (%(error)s). The VM is "
+"running."
+msgstr "还原两次失败;请稍后重试(%(error)s)。虚拟机正在运行。"
+
+#: vrtManager/instance.py:2030
+#, python-format
+msgid ""
+"The revert failed twice; try again in a moment (%(error)s). The VM is shut "
+"off."
+msgstr "还原两次失败;请稍后重试(%(error)s)。虚拟机已关机。"
+
+#: vrtManager/instance.py:2193
+msgid "A disk moves when its VM is defined and running, paused or shut off"
+msgstr "仅当虚拟机已定义且处于运行、暂停或关机状态时才能移动磁盘"
+
+#: vrtManager/instance.py:2195
+msgid ""
+"A disk of a VM with snapshots, a saved state or a running block job does not "
+"move"
+msgstr "有快照、保存的状态或正在运行块任务的虚拟机的磁盘不能移动"
+
+#: vrtManager/instance.py:2203
+#, python-format
+msgid ""
+"%(dev)s is not a disk file of the VM, the same in its running and next "
+"start's definition"
+msgstr "%(dev)s 不是该虚拟机在运行中的定义和下次启动定义中相同的磁盘文件"
+
+#: vrtManager/instance.py:2208
+#, python-format
+msgid "%(dev)s does not move: only writable raw and qcow2 disks do"
+msgstr "%(dev)s 不能移动:只能移动可写的 raw 和 qcow2 磁盘"
+
+#: vrtManager/instance.py:2215
+#, python-format
+msgid "%(dev)s does not move: no storage pool has %(path)s"
+msgstr "%(dev)s 不能移动:没有存储池包含 %(path)s"
+
+#: vrtManager/instance.py:2217
+#, python-format
+msgid "%(dev)s does not move: it has a backing file"
+msgstr "%(dev)s 不能移动:它有后备文件"
+
+#: vrtManager/instance.py:2220
+#, python-format
+msgid "%(pool_name)s is not an active pool of disk files of this host"
+msgstr "%(pool_name)s 不是此主机上活动的磁盘文件存储池"
+
+#: vrtManager/instance.py:2223
+#, python-format
+msgid "%(dev)s is in pool %(pool_name)s already"
+msgstr "%(dev)s 已在存储池 %(pool_name)s 中"
+
+#: vrtManager/instance.py:2227
+#, python-format
+msgid "Pool %(pool_name)s already has a file %(name)s"
+msgstr "存储池 %(pool_name)s 中已有文件 %(name)s"
+
+#: vrtManager/instance.py:2251
+#, python-format
+msgid "The copy of %(dev)s ended before it was complete"
+msgstr "%(dev)s 的复制在完成前结束"
+
+#: vrtManager/instance.py:2296
+#, python-format
+msgid "%(error)s; remove %(dest)s if it is there: %(cleanup_error)s"
+msgstr "%(error)s;如果 %(dest)s 存在请将其删除:%(cleanup_error)s"
+
+#: vrtManager/instance.py:2298
+#, python-format
+msgid "%(error)s; the VM may use %(dest)s now: check its disk %(dev)s"
+msgstr "%(error)s;虚拟机现在可能在使用 %(dest)s:请检查其磁盘 %(dev)s"
+
+#: vrtManager/instance.py:2305
+#, python-format
+msgid "The VM has no internal snapshot named %(name)s"
+msgstr "虚拟机没有名为 %(name)s 的内部快照"
+
+#: vrtManager/instance.py:2314
+msgid "The VM has a saved state; start it or remove the saved state first"
+msgstr "虚拟机有保存的状态;请先启动它或删除保存的状态"
+
+#: vrtManager/instance.py:2316
+msgid "A snapshot job is still running on this VM; try again when it ends"
+msgstr "此虚拟机上仍有快照任务在运行;请在其结束后重试"
+
+#: vrtManager/instance.py:2326
+msgid "Shut the VM down to revert to an internal snapshot"
+msgstr "请关闭虚拟机后再还原到内部快照"
+
+#: vrtManager/instance.py:2415
+msgid "Cannot clone a VM whose NVRAM is not a file"
+msgstr "NVRAM 不是文件的虚拟机无法克隆"
+
+#: vrtManager/instance.py:2483
+#, python-format
+msgid "Clone failed (%(err)s); remove these copies by hand: %(path)s"
+msgstr "克隆失败(%(err)s);请手动删除这些副本:%(path)s"
+
+#: vrtManager/instance.py:2493
+msgid "Shut the VM down before cloning it"
+msgstr "请先关闭虚拟机再克隆"
+
+#: vrtManager/instance.py:2547
+#, python-format
+msgid "Clone failed (%(err)s); remove these copies by hand: %(leftovers)s"
+msgstr "克隆失败(%(err)s);请手动删除这些副本:%(leftovers)s"
+
+#: vrtManager/instance.py:2558
+msgid "Live clone needs libvirt 11.10 or later; shut the VM down to clone it"
+msgstr "在线克隆需要 libvirt 11.10 或更高版本;请关闭虚拟机后再克隆"
+
+#: vrtManager/instance.py:2561
+msgid "A VM with a TPM is cloned only shut down"
+msgstr "带 TPM 的虚拟机只能在关机时克隆"
+
+#: vrtManager/instance.py:2564
+#, python-format
+msgid "Disk %(dev)s is not a local file; shut the VM down to clone it"
+msgstr "磁盘 %(dev)s 不是本地文件;请关闭虚拟机后再克隆"
+
+#: vrtManager/instance.py:2566
+#, python-format
+msgid "Disk %(dev)s is shareable; shut the VM down to clone it"
+msgstr "磁盘 %(dev)s 是可共享的;请关闭虚拟机后再克隆"
+
+#: vrtManager/instance.py:2571
+#, python-format
+msgid "Disk %(dev)s changes at the next start; shut the VM down to clone it"
+msgstr "磁盘 %(dev)s 在下次启动时会改变;请关闭虚拟机后再克隆"
+
+#: vrtManager/instance.py:2573
+msgid "Another job is running on the VM; clone it when that ends"
+msgstr "虚拟机上有另一个任务正在运行;请在其结束后再克隆"
+
+#: vrtManager/instance.py:2586
+#, python-format
+msgid "Volume %(name)s already exists in storage pool %(pool)s"
+msgstr "卷 %(name)s 已存在于存储池 %(pool)s 中"
+
+#: vrtManager/instance.py:2605
+#, python-format
+msgid ""
+"Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f "
+"GiB (the copied disks, plus %(percent)g%% of them, at least %(minimum)g GiB)"
+msgstr ""
+"存储池 %(pool)s 可用 %(free).1f GiB;克隆需要 %(size).1f GiB(复制的磁盘,再"
+"加其 %(percent)g%%,至少 %(minimum)g GiB)"
+
+#: vrtManager/instance.py:2744
+msgid "A network interface that is a host device is removed in the XML editor"
+msgstr "作为主机设备的网络接口需在 XML 编辑器中删除"
+
+#: vrtManager/instance.py:2760
+#, python-format
+msgid "No network interface with MAC %(old_mac)s"
+msgstr "没有 MAC 为 %(old_mac)s 的网络接口"
+
+#: vrtManager/instance.py:2763
+msgid "A network interface that is a host device is changed in the XML editor"
+msgstr "作为主机设备的网络接口需在 XML 编辑器中修改"
+
+#: vrtManager/instance.py:2854
+msgid ""
+"A VM changes its PCI devices while it runs, or is shut off without a saved "
+"state"
+msgstr "虚拟机只能在运行时或无保存的状态的关机时更改其 PCI 设备"
+
+#: vrtManager/instance.py:2866
+#, python-format
+msgid "%(err)s; whether the VM has PCI device %(address)s now is not known"
+msgstr "%(err)s;虚拟机现在是否有 PCI 设备 %(address)s 未知"
+
+#: vrtManager/instance.py:2868
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the VM is not running, its next start has it"
+msgstr "%(err)s;PCI 设备 %(address)s:虚拟机未运行,下次启动时有它"
+
+#: vrtManager/instance.py:2870
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the VM is not running, its next start does "
+"not have it"
+msgstr "%(err)s;PCI 设备 %(address)s:虚拟机未运行,下次启动时没有它"
+
+#: vrtManager/instance.py:2871
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM has it, its next start has it"
+msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机有它,下次启动时有它"
+
+#: vrtManager/instance.py:2873
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM has it, its next start does "
+"not have it"
+msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机有它,下次启动时没有它"
+
+#: vrtManager/instance.py:2875
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM does not have it, its next "
+"start has it"
+msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机没有它,下次启动时有它"
+
+#: vrtManager/instance.py:2877
+#, python-format
+msgid ""
+"%(err)s; PCI device %(address)s: the running VM does not have it, its next "
+"start does not have it"
+msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机没有它,下次启动时没有它"
+
+#: vrtManager/instance.py:2911
+#, python-format
+msgid "The VM has no PCI device %(address)s"
+msgstr "虚拟机没有 PCI 设备 %(address)s"
+
+#: vrtManager/instance.py:2929
+msgid "The VM has no USB controller"
+msgstr "虚拟机没有 USB 控制器"
+
+#: vrtManager/instance.py:2948
+msgid "The VM has no such USB device"
+msgstr "虚拟机没有此 USB 设备"
+
+#: vrtManager/instance.py:2989
+msgid "The VM has its own RNG device: change it in the XML editor"
+msgstr "虚拟机有自己的 RNG 设备:请在 XML 编辑器中修改"
+
+#: vrtManager/instance.py:3016
+msgid "The VM has its own TPM: change it in the XML editor"
+msgstr "虚拟机有自己的 TPM:请在 XML 编辑器中修改"
+
+#: vrtManager/instance.py:3035
+#, python-format
+msgid "Unknown crash action %(action)s"
+msgstr "未知的崩溃操作 %(action)s"
+
+#: vrtManager/interface.py:35
+#, python-format
+msgid ""
+"%(err)s; rolling the change back failed too, the host's configuration may be "
+"changed: %(rollback_err)s"
+msgstr "%(err)s;回滚更改也失败了,主机的配置可能已被更改:%(rollback_err)s"
+
+#: vrtManager/interface.py:269
+#, python-format
+msgid "Stop interface %(iface)s before deleting it"
+msgstr "删除接口 %(iface)s 前请先停止它"
+
+#: vrtManager/interface.py:273
+#, python-format
+msgid ""
+"Bridge %(iface)s has addresses and several members: give the addresses to "
+"one of them on the host first"
+msgstr "网桥 %(iface)s 有地址和多个成员:请先在主机上把地址分配给其中一个成员"
+
+#: vrtManager/network.py:299
+#, python-format
+msgid ""
+"Network %(net)s is in use by the running VMs %(vms)s; shut them down or "
+"remove their network cards first"
+msgstr "网络 %(net)s 正被运行中的虚拟机 %(vms)s 使用;请先关闭它们或移除其网卡"
+
+#: vrtManager/network.py:305
+#, python-format
+msgid ""
+"VMs %(vms)s use network %(net)s and would no longer start; remove their "
+"network cards first"
+msgstr "虚拟机 %(vms)s 使用网络 %(net)s,将无法再启动;请先移除其网卡"
+
+#: vrtManager/network.py:314
+#, python-format
+msgid ""
+"Network %(net)s runs with other addresses than it has saved; stop and start "
+"it first"
+msgstr "网络 %(net)s 运行时的地址与其保存的地址不同;请先停止再启动它"
+
+#: vrtManager/network.py:339
+#, python-format
+msgid "Network %(net)s has no such address"
+msgstr "网络 %(net)s 没有此地址"
+
+#: vrtManager/network.py:344
+#, python-format
+msgid "The range start %(address)s is not in %(subnet)s"
+msgstr "范围起始 %(address)s 不在 %(subnet)s 中"
+
+#: vrtManager/network.py:345
+#, python-format
+msgid "The range start %(address)s is not in the network"
+msgstr "范围起始 %(address)s 不在网络中"
+
+#: vrtManager/network.py:346
+#, python-format
+msgid "The range end %(address)s is not in %(subnet)s"
+msgstr "范围结束 %(address)s 不在 %(subnet)s 中"
+
+#: vrtManager/network.py:347
+#, python-format
+msgid "The range end %(address)s is not in the network"
+msgstr "范围结束 %(address)s 不在网络中"
+
+#: vrtManager/network.py:348
+#, python-format
+msgid "The fixed address %(address)s is not in %(subnet)s"
+msgstr "固定地址 %(address)s 不在 %(subnet)s 中"
+
+#: vrtManager/network.py:349
+#, python-format
+msgid "The fixed address %(address)s is not in the network"
+msgstr "固定地址 %(address)s 不在网络中"
+
+#: vrtManager/network.py:358
+#, python-format
+msgid "%(address)s is not an IP address"
+msgstr "%(address)s 不是 IP 地址"
+
+#: vrtManager/network.py:363
+#, python-format
+msgid "%(address)s is the host's own address on the network"
+msgstr "%(address)s 是主机自己在该网络上的地址"
+
+#: vrtManager/network.py:369
+#, python-format
+msgid "Stop network %(net)s to change its DHCP range"
+msgstr "请停止网络 %(net)s 后再更改其 DHCP 范围"
+
+#: vrtManager/network.py:373
+#, python-format
+msgid "This address of network %(net)s has no DHCP range"
+msgstr "网络 %(net)s 的此地址没有 DHCP 范围"
+
+#: vrtManager/network.py:377
+#, python-format
+msgid "The range starts at %(start)s, after its end %(end)s"
+msgstr "范围起始 %(start)s 在其结束 %(end)s 之后"
+
+#: vrtManager/network.py:379
+#, python-format
+msgid "The range takes in %(address)s, the host's own address"
+msgstr "范围包含主机自己的地址 %(address)s"
+
+#: vrtManager/network.py:388
+#, python-format
+msgid "Network %(net)s has no fixed address %(address)s"
+msgstr "网络 %(net)s 没有固定地址 %(address)s"
+
+#: vrtManager/network.py:410
+#, python-format
+msgid "The fixed address %(original)s is gone meanwhile; reload the page"
+msgstr "固定地址 %(original)s 已不存在;请重新加载页面"
+
+#: vrtManager/network.py:415
+#, python-format
+msgid "%(address)s is another entry's fixed address"
+msgstr "%(address)s 是另一条目的固定地址"
+
+#: vrtManager/network.py:417
+#, python-format
+msgid "%(mac_duid)s has another fixed address, %(other)s"
+msgstr "%(mac_duid)s 已有另一个固定地址 %(other)s"
+
+#: vrtManager/network.py:419
+#, python-format
+msgid "The name %(name)s has another fixed address, %(other)s"
+msgstr "名称 %(name)s 已有另一个固定地址 %(other)s"
+
+#: vrtManager/network.py:455
+msgid "Direction must be inbound or outbound"
+msgstr "方向必须是入站或出站"
+
+#: vrtManager/network.py:482
+#, python-format
+msgid "The XML must define network %(net)s"
+msgstr "XML 必须定义网络 %(net)s"
+
+#: vrtManager/network.py:484
+msgid "The XML must keep the network's UUID"
+msgstr "XML 必须保留网络的 UUID"
+
+#: vrtManager/nodedevs.py:11
msgid "Storage controller"
msgstr "存储控制器"
-#: vrtManager/nodedevs.py:11
+#: vrtManager/nodedevs.py:12
msgid "Network controller"
msgstr "网络控制器"
-#: vrtManager/nodedevs.py:12
+#: vrtManager/nodedevs.py:13
msgid "Display controller"
msgstr "显示控制器"
-#: vrtManager/nodedevs.py:13
+#: vrtManager/nodedevs.py:14
msgid "Multimedia controller"
msgstr "多媒体控制器"
-#: vrtManager/nodedevs.py:15
+#: vrtManager/nodedevs.py:16
msgid "Serial bus controller"
msgstr "串行总线控制器"
-#: vrtManager/nodedevs.py:20
+#: vrtManager/nodedevs.py:21
msgid "Audio"
msgstr "音频"
-#: vrtManager/nodedevs.py:21
+#: vrtManager/nodedevs.py:22
msgid "Communications"
msgstr "通信"
-#: vrtManager/nodedevs.py:22
+#: vrtManager/nodedevs.py:23
msgid "Human interface"
msgstr "人机接口"
-#: vrtManager/nodedevs.py:23
+#: vrtManager/nodedevs.py:24
msgid "Imaging"
msgstr "图像"
-#: vrtManager/nodedevs.py:24
+#: vrtManager/nodedevs.py:25
msgid "Printer"
msgstr "打印机"
-#: vrtManager/nodedevs.py:25
+#: vrtManager/nodedevs.py:26
msgid "Mass storage"
msgstr "大容量存储"
-#: vrtManager/nodedevs.py:26
+#: vrtManager/nodedevs.py:27
msgid "Hub"
msgstr "集线器"
-#: vrtManager/nodedevs.py:27
+#: vrtManager/nodedevs.py:28
msgid "Communications data"
msgstr "通信数据"
-#: vrtManager/nodedevs.py:28
+#: vrtManager/nodedevs.py:29
msgid "Smart card"
msgstr "智能卡"
-#: vrtManager/nodedevs.py:30
+#: vrtManager/nodedevs.py:31
msgid "Wireless"
msgstr "无线"
-#: vrtManager/nodedevs.py:31
+#: vrtManager/nodedevs.py:32
msgid "Vendor specific"
msgstr "厂商自定义"
+#: vrtManager/nodedevs.py:111
+#, python-format
+msgid "PCI device %(address)s"
+msgstr "PCI 设备 %(address)s"
+
+#: vrtManager/nodedevs.py:115
+#, python-format
+msgid "USB device %(bus)s/%(device)s (%(product)s)"
+msgstr "USB 设备 %(bus)s/%(device)s (%(product)s)"
+
+#: vrtManager/nodedevs.py:124
+#, python-format
+msgid "%(device)s is given to %(vms)s"
+msgstr "%(device)s 已分配给 %(vms)s"
+
+#: vrtManager/nodedevs.py:126
+#, python-format
+msgid "%(device)s holds the host's %(resources)s"
+msgstr "%(device)s 承载着主机的 %(resources)s"
+
+#: vrtManager/nodedevs.py:149
+#, python-format
+msgid "%(device)s is a bridge"
+msgstr "%(device)s 是网桥"
+
+#: vrtManager/nodedevs.py:153
+#, python-format
+msgid ""
+"bind %(address)s to vfio-pci on the host first if it is not needed there"
+msgstr "如果主机上不需要 %(address)s,请先在主机上将其绑定到 vfio-pci"
+
+#: vrtManager/nodedevs.py:155
+#, python-format
+msgid "%(device)s is in no IOMMU group"
+msgstr "%(device)s 不在任何 IOMMU 组中"
+
+#: vrtManager/nodedevs.py:163
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with %(member)s, which cannot be read"
+msgstr "%(device)s 与 %(member)s 共享 IOMMU 组 %(group)s,后者无法读取"
+
+#: vrtManager/nodedevs.py:166
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with %(member)s (%(product)s), which "
+"%(vms)s uses"
+msgstr ""
+"%(device)s 与 %(member)s (%(product)s) 共享 IOMMU 组 %(group)s,后者正被 "
+"%(vms)s 使用"
+
+#: vrtManager/nodedevs.py:171
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with the bridge %(member)s (driver "
+"%(driver)s)"
+msgstr "%(device)s 与网桥 %(member)s(驱动 %(driver)s)共享 IOMMU 组 %(group)s"
+
+#: vrtManager/nodedevs.py:175
+#, python-format
+msgid ""
+"%(device)s shares IOMMU group %(group)s with %(member)s (%(product)s), which "
+"the host (driver %(driver)s) uses"
+msgstr ""
+"%(device)s 与 %(member)s (%(product)s) 共享 IOMMU 组 %(group)s,后者正被主机"
+"(驱动 %(driver)s)使用"
+
+#: vrtManager/nodedevs.py:187
+#, python-format
+msgid "%(device)s is not on the host"
+msgstr "%(device)s 不在主机上"
+
+#: vrtManager/nodedevs.py:198
+#, python-format
+msgid "%(device)s is a hub"
+msgstr "%(device)s 是集线器"
+
+#: vrtManager/nodedevs.py:202
+#, python-format
+msgid "%(device)s is on the PCI controller %(controller)s, given to %(vms)s"
+msgstr "%(device)s 位于已分配给 %(vms)s 的 PCI 控制器 %(controller)s 上"
+
+#: vrtManager/nodedevs.py:332
+#, python-format
+msgid "%(problems)s"
+msgstr "%(problems)s"
+
+#: vrtManager/nwfilters.py:62
+msgid "Enter one element"
+msgstr "请输入一个 元素"
+
+#: vrtManager/nwfilters.py:64 vrtManager/nwfilters.py:76
+msgid ""
+"The filter's name may have letters, digits, dots, dashes and underscores"
+msgstr "过滤器名称只能包含字母、数字、点、连字符和下划线"
+
+#: vrtManager/nwfilters.py:67
+msgid "A network filter with this name already exists"
+msgstr "已存在同名的网络过滤器"
+
+#: vrtManager/nwfilters.py:69
+msgid "A network filter with this UUID already exists"
+msgstr "已存在相同 UUID 的网络过滤器"
+
+#: vrtManager/nwfilters.py:78
+#, python-format
+msgid "A network filter named %(clone_name)s already exists"
+msgstr "已存在名为 %(clone_name)s 的网络过滤器"
+
+#: vrtManager/nwfilters.py:90
+#, python-format
+msgid "Filter %(name)s is used by VM %(vms)s"
+msgstr "过滤器 %(name)s 正被虚拟机 %(vms)s 使用"
+
+#: vrtManager/nwfilters.py:93
+#, python-format
+msgid "Filter %(name)s is referenced by filter %(referencing)s"
+msgstr "过滤器 %(name)s 被过滤器 %(referencing)s 引用"
+
+#: vrtManager/nwfilters.py:159
+msgid "The filter has changed; reload the page and try again"
+msgstr "过滤器已更改;请重新加载页面后重试"
+
+#: vrtManager/nwfilters.py:169
+#, python-format
+msgid "The XML must define filter %(name)s"
+msgstr "XML 必须定义过滤器 %(name)s"
+
+#: vrtManager/nwfilters.py:171
+msgid "The XML must keep the filter's UUID"
+msgstr "XML 必须保留过滤器的 UUID"
+
+#: vrtManager/nwfilters.py:183
+#, python-format
+msgid ""
+"Filter %(name)s cannot be referenced: it is this one, referenced already or "
+"not on the host"
+msgstr "无法引用过滤器 %(name)s:它就是本过滤器、已被引用或不在主机上"
+
+#: vrtManager/nwfilters.py:193
+msgid "Enter one element"
+msgstr "请输入一个 元素"
+
+#: vrtManager/storage.py:192
+#, python-format
+msgid ""
+"Pool %(name)s holds disks of the running VMs %(vms)s; shut them down first"
+msgstr "存储池 %(name)s 中有运行中的虚拟机 %(vms)s 的磁盘;请先关闭它们"
+
+#: vrtManager/storage.py:200
+#, python-format
+msgid ""
+"VMs %(vms)s have disks in pool %(name)s by its name and would no longer "
+"start; detach those disks first"
+msgstr ""
+"虚拟机 %(vms)s 通过名称使用存储池 %(name)s 中的磁盘,将无法再启动;请先分离这"
+"些磁盘"
+
+#: vrtManager/storage.py:279
+#, python-format
+msgid ""
+"Volume %(name)s is in use: a VM's disk, a backing file or a snapshot's file"
+msgstr "卷 %(name)s 正在使用中:它是虚拟机的磁盘、后备文件或快照文件"
+
+#: vrtManager/util.py:140
+#, python-format
+msgid "The XML is not valid: %(err)s"
+msgstr "XML 无效:%(err)s"
+
+#: vrtManager/util.py:142
+msgid "The XML may not declare a DTD"
+msgstr "XML 不能声明 DTD"
+
+#: vrtManager/virtsecrets.py:26
+msgid "A Ceph key is base64 text, as `ceph auth get-key` prints it"
+msgstr "Ceph 密钥是 base64 文本,即 `ceph auth get-key` 输出的内容"
+
+#: vrtManager/virtsecrets.py:115
+#, python-format
+msgid "VMs %(names)s"
+msgstr "虚拟机 %(names)s"
+
+#: vrtManager/virtsecrets.py:117
+#, python-format
+msgid "pools %(names)s"
+msgstr "存储池 %(names)s"
+
+#: vrtManager/virtsecrets.py:118
+#, python-format
+msgid "Secret %(uuid)s is used by %(named)s; remove it from them first"
+msgstr "机密 %(uuid)s 正被 %(named)s 使用;请先从中移除它"
+
#: webvirtcloud/settings.py:325
msgid "Localhost"
msgstr "本机"
diff --git a/logs/audit.py b/logs/audit.py
index 02d08e0b..f780f294 100644
--- a/logs/audit.py
+++ b/logs/audit.py
@@ -64,7 +64,7 @@ def reason(exc, user):
if isinstance(exc, claims.Busy):
return busy_message(exc, user)
if isinstance(exc, OperationError):
- return str(exc)
+ return exc.args[0] # English: str() is the reader's language
if isinstance(exc, libvirt.libvirtError):
return safe_text(exc.get_error_message() or str(exc))[:500]
return type(exc).__name__
@@ -90,6 +90,10 @@ def audit(request, compute, subject, action, instance="", uuid=""):
def failed(request, compute, subject, action, why, instance="", uuid=""):
"""Write the failure row of an action that was refused before it started."""
+ from vrtManager.util import OperationError
+
+ if isinstance(why, OperationError):
+ why = why.args[0] # English: str() is the reader's language
write(request.user.get_username(), host_of(compute), instance, f"{subject}: {action}: {why}", False,
request_ip(request), uuid)
diff --git a/nodedevs/tests/test_nodedevs.py b/nodedevs/tests/test_nodedevs.py
index d05e6cb9..e872b929 100644
--- a/nodedevs/tests/test_nodedevs.py
+++ b/nodedevs/tests/test_nodedevs.py
@@ -14,6 +14,7 @@ from django.urls import reverse
from instances.models import Instance
from lxml import html
from vrtManager.connection import wvmConnect
+from vrtManager import util
from vrtManager.nodedevs import hostdev_problems, node_devices, pci_address
@@ -116,7 +117,8 @@ class NodeDevicesTests(SimpleTestCase):
def test_a_usb_device_on_a_controller_given_to_a_vm_is_not_given_to_another(self):
pci_devices, usb_devices = devices(domains=[vm("db-1", PCI_01)])
- reasons = hostdev_problems("kiosk-1", pci_devices, usb_devices, set(), [("0x0627", "0x0001", (1, 2))])
+ reasons = [util.render(m["text"], m["values"], str) for m in hostdev_problems(
+ "kiosk-1", pci_devices, usb_devices, set(), [("0x0627", "0x0001", (1, 2))])]
self.assertIn("USB device 1/2 (QEMU Tablet) is on the PCI controller 0000:00:01.0, given to db-1", reasons)
def test_a_usb_device_is_named_as_libvirt_picks_one(self):
diff --git a/nodedevs/tests/test_pci_rules.py b/nodedevs/tests/test_pci_rules.py
index 62fe4944..76f1e7c3 100644
--- a/nodedevs/tests/test_pci_rules.py
+++ b/nodedevs/tests/test_pci_rules.py
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock
from django.test import SimpleTestCase
from lxml import etree
+from vrtManager import util
from vrtManager.nodedevs import _pci, check_hostdevs, hostdev_problems, pci_problems
from vrtManager.util import OperationError
@@ -60,7 +61,8 @@ def inventory(vms=()):
def problems(address, managed=(), vms=()):
devices = {d["address"]: d for d in inventory(vms)}
- return " ".join(pci_problems("web-1", devices[address], devices, {address, *managed}))
+ return " ".join(util.render(m["text"], m["values"], str)
+ for m in pci_problems("web-1", devices[address], devices, {address, *managed}))
def hostdev(slot, managed="yes"):
@@ -107,7 +109,8 @@ class PciRulesTests(SimpleTestCase):
self.assertEqual(problems("0000:00:07.0", vms=[("web-1", {"0000:00:07.0"})]), "") # its own
def test_a_device_the_host_does_not_have_can_not(self):
- reasons = hostdev_problems("web-1", inventory(), [], {"0000:00:1f.0"}, [])
+ reasons = [util.render(m["text"], m["values"], str)
+ for m in hostdev_problems("web-1", inventory(), [], {"0000:00:1f.0"}, [])]
self.assertEqual(reasons, ["PCI device 0000:00:1f.0 is not on the host"])
diff --git a/operations/utils.py b/operations/utils.py
index cd7c1108..4a950703 100644
--- a/operations/utils.py
+++ b/operations/utils.py
@@ -19,7 +19,7 @@ from libvirt import libvirtError
from accounts.models import UserInstance
from appsettings.models import AppSettings
from instances.models import Instance
-from vrtManager.util import ConnectionFailed, OperationError
+from vrtManager.util import ConnectionFailed, OperationError, render
from . import jobs
@@ -66,7 +66,18 @@ def safe_text(text):
# words() puts them in the language of whoever reads them. A text that is not
# ours (libvirt's) is a message without values and stays as it is.
def say(text, **values):
- return [{"text": safe_text(text), "values": {key: safe_text(value) for key, value in values.items()}}]
+ return [{"text": safe_text(text), "values": {key: _stored(value) for key, value in values.items()}}]
+
+
+def _stored(value):
+ """A value as a job keeps it: messages stay messages (vrtManager.util.message)."""
+ if isinstance(value, list):
+ return [say(m["text"], **m["values"])[0] for m in value]
+ if isinstance(value, OperationError):
+ return [say(value.text, **value.values)[0]]
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
+ return value # for %(x).1f and the like
+ return safe_text(value)
def said(exception):
@@ -78,7 +89,7 @@ def said(exception):
def words(messages):
"""messages in the active language, each a sentence of its own."""
- parts = [_(m["text"]) % m["values"] if m["values"] else _(m["text"]) for m in messages]
+ parts = [render(m["text"], m["values"], _) for m in messages]
return " ".join(part if last or part.endswith((".", "。", "!", "?")) else part + "."
for last, part in ((i == len(parts) - 1, part) for i, part in enumerate(parts)))
diff --git a/vrtManager/connection.py b/vrtManager/connection.py
index ccd2191f..a00392df 100644
--- a/vrtManager/connection.py
+++ b/vrtManager/connection.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import contextlib
import io
import os
@@ -196,7 +197,7 @@ class wvmConnection(object):
try:
connection = self._open_transport()
if not connection.isAlive(): # closed again before it was published
- raise util.OperationError("the connection closed right after opening")
+ raise util.OperationError(gettext_noop("the connection closed right after opening"))
# Drivers without keepalive or close callbacks work without them
try:
connection.setKeepAlive(connection_manager.keepalive_interval, connection_manager.keepalive_count)
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index 0b43e673..327f6822 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import base64
import contextlib
import ipaddress
@@ -130,10 +131,10 @@ class wvmInstances(wvmConnect):
mode = ["offline"]
elif state in (1, 3):
if offline:
- raise util.OperationError("A running or paused VM cannot be migrated offline")
+ raise util.OperationError(gettext_noop("A running or paused VM cannot be migrated offline"))
mode = ["live" if live else "non-live"]
else:
- raise util.OperationError("The VM can be migrated when it is running, paused or shut off")
+ raise util.OperationError(gettext_noop("The VM can be migrated when it is running, paused or shut off"))
# The source definition always goes: one left behind points at the
# same (shared) disks as the migrated VM and could be started again.
@@ -156,33 +157,32 @@ class wvmInstances(wvmConnect):
# libvirt keeps snapshot metadata on the source host: shared disks do
# not carry it to the destination.
if dom.snapshotNum(0):
- raise util.OperationError("A VM with snapshots cannot be migrated")
+ raise util.OperationError(gettext_noop("A VM with snapshots cannot be migrated"))
# a shut-off VM moves as its definition or started paused: either loses the saved memory
if state == 5 and dom.hasManagedSaveImage(0):
- raise util.OperationError("A VM with a saved state cannot be migrated")
+ raise util.OperationError(gettext_noop("A VM with a saved state cannot be migrated"))
# the definition the migration carries first: it has the disks a migration copies
definitions = [dom.XMLDesc(0)]
if state != 5:
definitions.append(dom.XMLDesc(VIR_DOMAIN_XML_INACTIVE))
# a host device is of the source host: the destination may have none, or another one
if any(etree.fromstring(xml).xpath(HOST_DEVICES) for xml in definitions):
- raise util.OperationError("A VM with host devices cannot be migrated; remove them first")
+ raise util.OperationError(gettext_noop("A VM with host devices cannot be migrated; remove them first"))
copies = self.disk_copies(conn, definitions, pools or {})
if copies:
mode.append("disks copied: " + ", ".join(f"{copy['target']} to {copy['pool'].name()}" for copy in copies))
missing = self.missing_disks(definitions, copied={copy["path"] for copy in copies})
if missing:
- raise util.OperationError(
- "The destination host does not have these disks of the VM: "
- + ", ".join(f"{target} ({source})" for target, source in missing)
- )
+ raise util.OperationError(gettext_noop("The destination host does not have these disks of the VM: %(disks)s"),
+ disks=[util.message(gettext_noop("%(target)s (%(source)s)"), target=target,
+ source=source) for target, source in missing])
dom_arch = conn.get_arch()
dom_emulator = conn.get_dom_emulator()
if dom_emulator != self.get_emulator(dom_arch):
raise util.OperationError(
- "Destination host emulator is different. Cannot be migrated"
+ gettext_noop("Destination host emulator is different. Cannot be migrated")
)
# the TPM state is a file of the source host: a live migration carries it
@@ -313,17 +313,17 @@ class wvmInstances(wvmConnect):
def copy_refusal(disk, volume):
"""Why the disk (its volume on the source, None if no pool has it) cannot be copied, or None."""
if disk.get("device") != "disk":
- return "an image is not copied; eject it first"
+ return gettext_noop("an image is not copied; eject it first")
if disk.find("readonly") is not None or disk.find("encryption") is not None \
or disk.find("source/encryption") is not None:
- return "a read-only or encrypted disk is not copied"
+ return gettext_noop("a read-only or encrypted disk is not copied")
driver = disk.find("driver")
if driver is None or driver.get("type") not in ("raw", "qcow2"):
- return "only raw and qcow2 disks are copied"
+ return gettext_noop("only raw and qcow2 disks are copied")
if volume is None:
- return "no storage pool of the source host has it"
+ return gettext_noop("no storage pool of the source host has it")
if ElementTree.fromstring(volume.XMLDesc(0)).findtext("backingStore/path"):
- return "a disk with a backing file is not copied"
+ return gettext_noop("a disk with a backing file is not copied")
return None
def disk_copies(self, conn, definitions, pools):
@@ -339,34 +339,44 @@ class wvmInstances(wvmConnect):
targets.add(row["target"])
carried.add(row["path"])
name = pools.get(row["target"])
- label = f"{row['target']} ({row['path']})"
+ disk = {"target": row["target"], "path": row["path"]}
if not name:
if not row["shared"]:
- problems.append(f"{label}: the destination host does not share it; choose a pool to copy it to")
+ problems.append(util.message(gettext_noop(
+ "%(target)s (%(path)s): the destination host does not share it; choose a pool to copy it to"),
+ **disk))
continue
if row["refusal"]:
- problems.append(f"{label}: {row['refusal']}")
+ problems.append(util.message(gettext_noop("%(target)s (%(path)s): %(refusal)s"), **disk,
+ refusal=[util.message(row["refusal"])]))
continue
if name not in here:
- problems.append(f"{label}: the destination host has no pool {name} for disk files")
+ problems.append(util.message(gettext_noop(
+ "%(target)s (%(path)s): the destination host has no pool %(pool)s for disk files"), **disk, pool=name))
continue
pool, directory = here[name]
pool.refresh(0)
file = os.path.basename(row["path"])
if self._find_volume(pool, file) is not None:
- problems.append(f"{label}: pool {name} of the destination host already has a file {file}")
+ problems.append(util.message(gettext_noop(
+ "%(target)s (%(path)s): pool %(pool)s of the destination host already has a file %(file)s"),
+ **disk, pool=name, file=file))
continue
if any(copy["dest"] == f"{directory}/{file}" for copy in copies):
- problems.append(f"{label}: another disk is copied to {directory}/{file}")
+ problems.append(util.message(gettext_noop("%(target)s (%(path)s): another disk is copied to %(dest)s"),
+ **disk, dest=f"{directory}/{file}"))
continue
copies.append({"target": row["target"], "path": row["path"], "dest": f"{directory}/{file}",
"pool": pool})
- problems += [f"{target}: the VM has no disk file to copy" for target in pools.keys() - targets if pools[target]]
+ problems += [util.message(gettext_noop("%(target)s: the VM has no disk file to copy"), target=target)
+ for target in pools.keys() - targets if pools[target]]
for xml in definitions[1:]:
- problems += [f"{row['target']} ({row['path']}) of the next start: the destination host does not share it"
- for row in self.disk_plan(conn, xml) if row["path"] not in carried and not row["shared"]]
+ problems += [util.message(gettext_noop(
+ "%(target)s (%(path)s) of the next start: the destination host does not share it"),
+ target=row["target"], path=row["path"])
+ for row in self.disk_plan(conn, xml) if row["path"] not in carried and not row["shared"]]
if problems:
- raise util.OperationError("The disks cannot be migrated: " + "; ".join(problems))
+ raise util.OperationError(gettext_noop("The disks cannot be migrated: %(problems)s"), problems=problems)
return copies
def remove_partial_copies(self, uuid, copies, error, nvram=None):
@@ -379,9 +389,8 @@ class wvmInstances(wvmConnect):
except libvirtError as lookup_error:
if lookup_error.get_error_code() != VIR_ERR_NO_DOMAIN:
raise util.OperationError(
- f"{error}; remove the partial disk copies from the destination host: "
- + ", ".join(copy["dest"] for copy in copies)
- ) from error
+ gettext_noop("%(error)s; remove the partial disk copies from the destination host: %(copies)s"),
+ error=error, copies=", ".join(copy["dest"] for copy in copies)) from error
left = []
for copy in copies:
copy["pool"].refresh(0)
@@ -397,7 +406,7 @@ class wvmInstances(wvmConnect):
left.append(f"{NVRAM_DIR}/{nvram}")
if left:
raise util.OperationError(
- f"{error}; remove the partial disk copies from the destination host: {', '.join(left)}"
+ gettext_noop("%(error)s; remove the partial disk copies from the destination host: %(left)s"), error=error, left=', '.join(left)
) from error
def copy_nvram(self, conn):
@@ -414,7 +423,7 @@ class wvmInstances(wvmConnect):
return None # never started: libvirt creates it at the first start
if self.same_directory(source, target):
if self._find_volume(target, name) is None:
- raise util.OperationError(f"The destination host cannot see the NVRAM file {path}")
+ raise util.OperationError(gettext_noop("The destination host cannot see the NVRAM file %(path)s"), path=path)
return None
chunks = []
stream = conn.wvm.newStream(0)
@@ -482,7 +491,7 @@ class wvmInstances(wvmConnect):
@staticmethod
def disk_label(pool, name):
if name is None:
- return f"{pool} disk, cannot be checked"
+ return [util.message(gettext_noop("%(type)s disk, cannot be checked"), type=pool)]
return name if pool is None else f"{pool}/{name}"
def find_volume(self, pool, name):
@@ -522,7 +531,7 @@ class wvmInstances(wvmConnect):
if error.get_error_code() != VIR_ERR_NO_DOMAIN:
raise
else:
- raise util.OperationError(f"The destination host already has a VM {key}")
+ raise util.OperationError(gettext_noop("The destination host already has a VM %(key)s"), key=key)
xml = dom.XMLDesc(VIR_DOMAIN_XML_SECURE | VIR_DOMAIN_XML_MIGRATABLE)
autostart = dom.autostart()
@@ -544,7 +553,7 @@ class wvmInstances(wvmConnect):
if started:
# started meanwhile by another client: it stays on the source
dom.connect().defineXML(xml).setAutostart(autostart)
- raise util.OperationError("The VM was started on the source host during the migration")
+ raise util.OperationError(gettext_noop("The VM was started on the source host during the migration"))
except Exception as error:
self.undo_definition(dom, new, error) # raises when the destination keeps the VM
self.delete_nvram(nvram)
@@ -560,19 +569,19 @@ class wvmInstances(wvmConnect):
on_source = False
if not on_source:
raise util.OperationError(
- f"{error}; the VM may now be defined only on the destination host: check both hosts"
+ gettext_noop("%(error)s; the VM may now be defined only on the destination host: check both hosts"), error=error
) from error
try:
new.undefineFlags(VIR_DOMAIN_UNDEFINE_KEEP_NVRAM)
except libvirtError as undo_error:
raise util.OperationError(
- f"{error}; the VM is now defined on both hosts, remove it from the destination: {undo_error}"
+ gettext_noop("%(error)s; the VM is now defined on both hosts, remove it from the destination: %(undo_error)s"), error=error, undo_error=undo_error
) from error
def check_snapshot_name(name):
if not SNAPSHOT_NAME.fullmatch(name):
- raise util.OperationError("Snapshot names are 1-64 letters, digits, '.', '_' or '-' and do not start with '.'")
+ raise util.OperationError(gettext_noop("Snapshot names are 1-64 letters, digits, '.', '_' or '-' and do not start with '.'"))
def agent_command(dom, command, arguments=None):
@@ -1088,7 +1097,7 @@ class wvmInstance(wvmConnect):
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
# the order lists disks, CD-ROM drives and NICs: it does not drop another device's place
if tree.xpath("./devices/*[not(self::disk[@device='disk' or @device='cdrom'] or self::interface)]/boot"):
- raise util.OperationError("A device other than a disk or NIC is in the boot order: change it in the XML editor")
+ raise util.OperationError(gettext_noop("A device other than a disk or NIC is in the boot order: change it in the XML editor"))
# it replaces the order by device type and every device's place
for boot in tree.xpath("./os/boot | ./devices/*/boot"):
boot.getparent().remove(boot)
@@ -1100,10 +1109,10 @@ class wvmInstance(wvmConnect):
elif dev["type"] == "network": # any NIC type: network, bridge, direct
devices = tree.xpath("./devices/interface[mac/@address=$mac]", mac=dev["dev"])
else:
- raise util.OperationError("Invalid Device Type for boot order")
+ raise util.OperationError(gettext_noop("Invalid Device Type for boot order"))
# nothing is defined until every choice is found: the order above is cleared
if not devices:
- raise util.OperationError(f"Boot device {dev['dev']} is not in the definition")
+ raise util.OperationError(gettext_noop("Boot device %(dev)s is not in the definition"), dev=dev['dev'])
for d in devices:
d.append(order)
self._defineXML(etree.tostring(tree).decode())
@@ -1123,7 +1132,9 @@ class wvmInstance(wvmConnect):
tree = etree.fromstring(self._XMLDesc(xml_flags))
found = tree.xpath("./devices/disk[@device='cdrom'][target/@dev=$dev]", dev=dev)
if not found:
- raise util.OperationError(f"CD-ROM {dev} is not in the {label} definition")
+ missing = (gettext_noop("CD-ROM %(dev)s is not in the running definition") if label == "running"
+ else gettext_noop("CD-ROM %(dev)s is not in the persistent definition"))
+ raise util.OperationError(missing, dev=dev)
devices.append(found[0])
for (label, _, affect), disk in zip(definitions, devices):
for source in disk.findall("source"):
@@ -1134,7 +1145,7 @@ class wvmInstance(wvmConnect):
self.instance.updateDeviceFlags(etree.tostring(disk).decode(), affect)
except libvirtError as err:
if label == "persistent" and len(definitions) == 2:
- raise util.OperationError(f"Media of {dev} changed in the running VM only: {err}") from err
+ raise util.OperationError(gettext_noop("Media of %(dev)s changed in the running VM only: %(err)s"), dev=dev, err=err) from err
raise
def get_iso_path(self, image):
@@ -1145,9 +1156,9 @@ class wvmInstance(wvmConnect):
if image in stg.listVolumes():
paths.append(stg.storageVolLookupByName(image).path())
if not paths:
- raise util.OperationError(f"ISO image {image} was not found")
+ raise util.OperationError(gettext_noop("ISO image %(image)s was not found"), image=image)
if len(paths) > 1:
- raise util.OperationError(f"ISO image {image} is in more than one storage pool")
+ raise util.OperationError(gettext_noop("ISO image %(image)s is in more than one storage pool"), image=image)
return paths[0]
def mount_iso(self, dev, image):
@@ -1254,7 +1265,7 @@ class wvmInstance(wvmConnect):
found = True
self.instance.detachDeviceFlags(etree.tostring(disks[0]).decode(), affect)
if not found:
- raise util.OperationError(f"Disk {target_dev} is not in the VM's definition")
+ raise util.OperationError(gettext_noop("Disk %(target_dev)s is not in the VM's definition"), target_dev=target_dev)
def has_disk(self, target_dev):
"""True while the live or the persistent definition still has target_dev."""
@@ -1382,7 +1393,7 @@ class wvmInstance(wvmConnect):
self._defineXML(etree.tostring(tree).decode())
else:
raise util.OperationError(
- "Please shutdown the instance then try to enable vCPU hotplug"
+ gettext_noop("Please shutdown the instance then try to enable vCPU hotplug")
)
def mem_usage(self):
@@ -1845,7 +1856,7 @@ class wvmInstance(wvmConnect):
def create_snapshot(self, name, desc=None):
self._check_snapshot_state()
if self.instance.snapshotListNames(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL):
- raise util.OperationError("The VM has external snapshots; internal snapshots cannot be mixed with them")
+ raise util.OperationError(gettext_noop("The VM has external snapshots; internal snapshots cannot be mixed with them"))
state = "shutoff" if self.get_status() == 5 else "running"
name_esc = util.xml_escape(name)
desc_esc = util.xml_escape(desc) if desc else ""
@@ -1877,30 +1888,30 @@ class wvmInstance(wvmConnect):
check_snapshot_name(name)
if quiesce:
if memory:
- raise util.OperationError("A snapshot with memory is not quiesced; it keeps the whole guest state")
+ raise util.OperationError(gettext_noop("A snapshot with memory is not quiesced; it keeps the whole guest state"))
if self.get_status() != 1:
- raise util.OperationError("Only a running VM's file systems can be frozen")
+ raise util.OperationError(gettext_noop("Only a running VM's file systems can be frozen"))
if not self.is_agent_ready():
- raise util.OperationError("The QEMU guest agent is not connected; it freezes the file systems")
+ raise util.OperationError(gettext_noop("The QEMU guest agent is not connected; it freezes the file systems"))
self._check_snapshot_state()
if self.instance.snapshotListNames(VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL):
- raise util.OperationError("The VM has internal snapshots; external snapshots cannot be mixed with them")
+ raise util.OperationError(gettext_noop("The VM has internal snapshots; external snapshots cannot be mixed with them"))
if name in self.instance.snapshotListNames(0):
- raise util.OperationError(f"A snapshot named {name} already exists")
+ raise util.OperationError(gettext_noop("A snapshot named %(name)s already exists"), name=name)
if self.instance.hasCurrentSnapshot(0):
if self.instance.snapshotCurrent(0).numChildren(0):
- raise util.OperationError("Newer snapshots exist after the last revert; delete them first")
+ raise util.OperationError(gettext_noop("Newer snapshots exist after the last revert; delete them first"))
elif self.instance.snapshotListNames(0):
# only after changes outside WebVirtCloud; a create would start a second root
- raise util.OperationError("No snapshot is current; revert to a snapshot first")
+ raise util.OperationError(gettext_noop("No snapshot is current; revert to a snapshot first"))
tree = etree.fromstring(self._XMLDesc(0))
for disk in tree.xpath("devices/disk"):
dev = disk.xpath("string(target/@dev)")
if disk.get("device") in ("cdrom", "floppy"):
if disk.xpath("source/@file|source/@dev|source/@name"):
- raise util.OperationError(f"Eject the media from {dev} first")
+ raise util.OperationError(gettext_noop("Eject the media from %(dev)s first"), dev=dev)
elif disk.get("type") != "file":
- raise util.OperationError(f"Disk {dev} is not a file; external snapshots need file disks")
+ raise util.OperationError(gettext_noop("Disk %(dev)s is not a file; external snapshots need file disks"), dev=dev)
disks = list(self._disk_sources(tree).values())
# free space, and the top overlays, are known to the pools only after
# a refresh; nothing has changed yet when it fails
@@ -1908,7 +1919,7 @@ class wvmInstance(wvmConnect):
self.refresh_instance_pools()
except libvirtError as err:
raise util.OperationError(
- f"The storage pools could not be refreshed, so their free space is unknown: {err}"
+ gettext_noop("The storage pools could not be refreshed, so their free space is unknown: %(err)s"), err=err
) from None
memory_xml, memory_pool = self._snapshot_memory(name, disks) if memory else ("", None)
self._check_snapshot_space(disks, memory_pool, free_percent, free_min_gib)
@@ -1929,13 +1940,14 @@ class wvmInstance(wvmConnect):
"""The element of a snapshot that saves memory, and the
storage pool its file goes into: the pool of the first disk."""
if self.get_status() != 1:
- raise util.OperationError("Memory is saved only from a running VM")
+ raise util.OperationError(gettext_noop("Memory is saved only from a running VM"))
if not disks:
- raise util.OperationError("The VM has no disk to keep its memory next to")
+ raise util.OperationError(gettext_noop("The VM has no disk to keep its memory next to"))
try:
pool = self.get_volume_by_path(disks[0]).storagePoolLookupByVolume()
except libvirtError:
- raise util.OperationError(f"{disks[0]} is not in a storage pool; memory is saved only into one") from None
+ raise util.OperationError(gettext_noop("%(disk)s is not in a storage pool; memory is saved only into one"),
+ disk=disks[0]) from None
path = os.path.join(os.path.dirname(disks[0]), f"{self.get_uuid()}.{name}.mem")
return f"", pool.name()
@@ -1964,11 +1976,10 @@ class wvmInstance(wvmConnect):
for name, size in needed.items():
free = pools[name].info()[3]
if free < size:
- raise util.OperationError(
- f"Storage pool {name} has {free / 1024**3:.1f} GiB free; the snapshot needs {size / 1024**3:.1f} GiB "
- f"({free_percent:g}% of the VM's disks in it, at least {free_min_gib:g} GiB, "
- "and the memory if it is saved)"
- )
+ raise util.OperationError(gettext_noop(
+ "Storage pool %(pool)s has %(free).1f GiB free; the snapshot needs %(size).1f GiB (%(percent)g%% "
+ "of the VM's disks in it, at least %(minimum)g GiB, and the memory if it is saved)"),
+ pool=name, free=free / 1024**3, size=size / 1024**3, percent=free_percent, minimum=free_min_gib)
def check_external_revert(self, name):
"""Refuses a revert that cannot work, before anything changes; returns
@@ -1984,7 +1995,7 @@ class wvmInstance(wvmConnect):
current_loader = util.get_xml_path(self._XMLDesc(PERSISTENT_XML), "/domain/os/loader/@type")
if saved_loader == "rom" and current_loader == "pflash":
raise util.OperationError(
- "This snapshot was taken by an older version on a UEFI VM and cannot be reverted; delete it instead"
+ gettext_noop("This snapshot was taken by an older version on a UEFI VM and cannot be reverted; delete it instead")
)
return snap
@@ -2014,10 +2025,10 @@ class wvmInstance(wvmConnect):
try:
self.instance.revertToSnapshot(snap, flags)
except libvirtError as retry_err:
- state = "running" if self.instance.isActive() else "shut off"
- raise util.OperationError(
- f"The revert failed twice; try again in a moment ({retry_err}). The VM is {state}."
- ) from None
+ failed = (gettext_noop("The revert failed twice; try again in a moment (%(error)s). The VM is running.")
+ if self.instance.isActive() else
+ gettext_noop("The revert failed twice; try again in a moment (%(error)s). The VM is shut off."))
+ raise util.OperationError(failed, error=retry_err) from None
return self._left_behind(before)
def delete_external_snapshot(self, name):
@@ -2179,9 +2190,9 @@ class wvmInstance(wvmConnect):
dom = self.instance
state = dom.info()[0]
if not dom.isPersistent() or state not in (VIR_DOMAIN_RUNNING, VIR_DOMAIN_PAUSED, VIR_DOMAIN_SHUTOFF):
- raise util.OperationError("A disk moves when its VM is defined and running, paused or shut off")
+ raise util.OperationError(gettext_noop("A disk moves when its VM is defined and running, paused or shut off"))
if dom.snapshotNum(0) or dom.hasManagedSaveImage(0) or self.get_snapshot_jobs():
- raise util.OperationError("A disk of a VM with snapshots, a saved state or a running block job does not move")
+ raise util.OperationError(gettext_noop("A disk of a VM with snapshots, a saved state or a running block job does not move"))
# the running definition (the persistent one of a shut-off VM) first
disks = [ElementTree.fromstring(xml).find(f"devices/disk/target[@dev='{dev}']/..")
for xml in (self._XMLDesc(0), self._XMLDesc(PERSISTENT_XML))]
@@ -2189,32 +2200,31 @@ class wvmInstance(wvmConnect):
disk, source = disks[0], sources[0]
if disk is None or disk.get("type") != "file" or disk.get("device") != "disk" or None in sources \
or len({s.get("file") for s in sources}) != 1:
- raise util.OperationError(f"{dev} is not a disk file of the VM, the same in its running and next start's "
- "definition")
+ raise util.OperationError(gettext_noop("%(dev)s is not a disk file of the VM, the same in its running and next start's definition"), dev=dev)
driver = disk.find("driver")
fmt = driver.get("type") if driver is not None else None
if fmt not in ("raw", "qcow2") or disk.find("readonly") is not None or disk.find("shareable") is not None \
or disk.find("encryption") is not None or source.find("encryption") is not None:
- raise util.OperationError(f"{dev} does not move: only writable raw and qcow2 disks do")
+ raise util.OperationError(gettext_noop("%(dev)s does not move: only writable raw and qcow2 disks do"), dev=dev)
path = source.get("file")
try:
volume = self.wvm.storageVolLookupByPath(path)
except libvirtError as err:
if err.get_error_code() != VIR_ERR_NO_STORAGE_VOL:
raise
- raise util.OperationError(f"{dev} does not move: no storage pool has {path}") from err
+ raise util.OperationError(gettext_noop("%(dev)s does not move: no storage pool has %(path)s"), dev=dev, path=path) from err
if ElementTree.fromstring(volume.XMLDesc(0)).findtext("backingStore/path"):
- raise util.OperationError(f"{dev} does not move: it has a backing file")
+ raise util.OperationError(gettext_noop("%(dev)s does not move: it has a backing file"), dev=dev)
pools = self.file_pools()
if pool_name not in pools:
- raise util.OperationError(f"{pool_name} is not an active pool of disk files of this host")
+ raise util.OperationError(gettext_noop("%(pool_name)s is not an active pool of disk files of this host"), pool_name=pool_name)
pool, directory = pools[pool_name]
if directory == os.path.dirname(path):
- raise util.OperationError(f"{dev} is in pool {pool_name} already")
+ raise util.OperationError(gettext_noop("%(dev)s is in pool %(pool_name)s already"), dev=dev, pool_name=pool_name)
pool.refresh(0)
name = os.path.basename(path)
if self._find_volume(pool, name) is not None:
- raise util.OperationError(f"Pool {pool_name} already has a file {name}")
+ raise util.OperationError(gettext_noop("Pool %(pool_name)s already has a file %(name)s"), pool_name=pool_name, name=name)
return {"dev": dev, "path": path, "dest": f"{directory}/{name}", "format": fmt, "pool": pool,
"volume": volume, "running": state != VIR_DOMAIN_SHUTOFF}
@@ -2238,7 +2248,7 @@ class wvmInstance(wvmConnect):
while True: # mirrored once the copy caught up: cur == end
info = dom.blockJobInfo(dev, 0)
if not info:
- raise util.OperationError(f"The copy of {dev} ended before it was complete")
+ raise util.OperationError(gettext_noop("The copy of %(dev)s ended before it was complete"), dev=dev)
if info["end"] and info["cur"] == info["end"]:
break
time.sleep(1)
@@ -2283,16 +2293,17 @@ class wvmInstance(wvmConnect):
if copy is not None:
copy.delete(0)
except libvirtError as cleanup_error:
- raise util.OperationError(f"{error}; remove {dest} if it is there: {cleanup_error}") from error
+ raise util.OperationError(gettext_noop("%(error)s; remove %(dest)s if it is there: %(cleanup_error)s"), error=error, dest=dest, cleanup_error=cleanup_error) from error
if named:
- raise util.OperationError(f"{error}; the VM may use {dest} now: check its disk {dev}") from error
+ raise util.OperationError(gettext_noop("%(error)s; the VM may use %(dest)s now: check its disk %(dev)s"), error=error, dest=dest, dev=dev) from error
def _snapshot(self, name, external):
"""The named snapshot; a snapshot of the other kind is refused."""
flag = VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL if external else VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL
if name not in self.instance.snapshotListNames(flag):
- kind = "external" if external else "internal"
- raise util.OperationError(f"The VM has no {kind} snapshot named {name}")
+ missing = (gettext_noop("The VM has no external snapshot named %(name)s") if external
+ else gettext_noop("The VM has no internal snapshot named %(name)s"))
+ raise util.OperationError(missing, name=name)
return self.instance.snapshotLookupByName(name, 0)
def _check_snapshot_state(self):
@@ -2300,9 +2311,9 @@ class wvmInstance(wvmConnect):
external. The internal/external mix is checked only on create, so a
VM that is already mixed can still be cleaned up."""
if self.instance.hasManagedSaveImage(0):
- raise util.OperationError("The VM has a saved state; start it or remove the saved state first")
+ raise util.OperationError(gettext_noop("The VM has a saved state; start it or remove the saved state first"))
if self.get_snapshot_jobs():
- raise util.OperationError("A snapshot job is still running on this VM; try again when it ends")
+ raise util.OperationError(gettext_noop("A snapshot job is still running on this VM; try again when it ends"))
def snapshot_delete(self, snapshot):
self._check_snapshot_state()
@@ -2312,7 +2323,7 @@ class wvmInstance(wvmConnect):
"""An internal snapshot is reverted to on a shut-off VM; one of a
running VM starts it again as it was then."""
if self.get_status() != 5:
- raise util.OperationError("Shut the VM down to revert to an internal snapshot")
+ raise util.OperationError(gettext_noop("Shut the VM down to revert to an internal snapshot"))
self._check_snapshot_state()
self.instance.revertToSnapshot(self._snapshot(snapshot, external=False), 0)
@@ -2401,7 +2412,7 @@ class wvmInstance(wvmConnect):
nvram = tree.find("os/nvram")
if nvram is not None and nvram.get("type", "file") != "file":
- raise util.OperationError("Cannot clone a VM whose NVRAM is not a file")
+ raise util.OperationError(gettext_noop("Cannot clone a VM whose NVRAM is not a file"))
return tree, disks
def _clone_volume_xml(self, name, capacity, vol_format, owner_uid, owner_gid):
@@ -2469,7 +2480,7 @@ class wvmInstance(wvmConnect):
copy.delete(0)
except libvirtError:
raise util.OperationError(
- f"Clone failed ({err}); remove these copies by hand: {copy.path()}"
+ gettext_noop("Clone failed (%(err)s); remove these copies by hand: %(path)s"), err=err, path=copy.path()
) from err
raise
@@ -2479,7 +2490,7 @@ class wvmInstance(wvmConnect):
the volumes this call created are removed. Returns the clone's UUID."""
if self.get_status() != 5:
# A running guest keeps writing while its disks are copied.
- raise util.OperationError("Shut the VM down before cloning it")
+ raise util.OperationError(gettext_noop("Shut the VM down before cloning it"))
_, disks = self._clone_definition(clone_data, uuid)
paths = {} # dev: path of its copy
@@ -2533,7 +2544,7 @@ class wvmInstance(wvmConnect):
except libvirtError:
leftovers.append(path)
if leftovers:
- raise util.OperationError(f"Clone failed ({err}); remove these copies by hand: {', '.join(leftovers)}") from err
+ raise util.OperationError(gettext_noop("Clone failed (%(err)s); remove these copies by hand: %(leftovers)s"), err=err, leftovers=', '.join(leftovers)) from err
raise
def live_clone_targets(self, clone_data, free_percent=10, free_min_gib=1):
@@ -2544,22 +2555,22 @@ class wvmInstance(wvmConnect):
free_min_gib GiB, free (the snapshots' rule); checked only, nothing
is reserved."""
if self.wvm.getLibVersion() < LIVE_CLONE_MIN_LIBVIRT:
- raise util.OperationError("Live clone needs libvirt 11.10 or later; shut the VM down to clone it")
+ raise util.OperationError(gettext_noop("Live clone needs libvirt 11.10 or later; shut the VM down to clone it"))
tree, disks = self._clone_definition(clone_data, "")
if tree.find("devices/tpm") is not None:
- raise util.OperationError("A VM with a TPM is cloned only shut down")
+ raise util.OperationError(gettext_noop("A VM with a TPM is cloned only shut down"))
for source, dev in disks:
if not source.get("file"):
- raise util.OperationError(f"Disk {dev} is not a local file; shut the VM down to clone it")
+ raise util.OperationError(gettext_noop("Disk %(dev)s is not a local file; shut the VM down to clone it"), dev=dev)
if source.getparent().find("shareable") is not None:
- raise util.OperationError(f"Disk {dev} is shareable; shut the VM down to clone it")
+ raise util.OperationError(gettext_noop("Disk %(dev)s is shareable; shut the VM down to clone it"), dev=dev)
# the backup copies the running disks: one the next start changes would give the clone the wrong data
live = etree.fromstring(self._XMLDesc(0))
for source, dev in disks:
if live.xpath("devices/disk[target/@dev=$dev]/source/@file", dev=dev) != [source.get("file")]:
- raise util.OperationError(f"Disk {dev} changes at the next start; shut the VM down to clone it")
+ raise util.OperationError(gettext_noop("Disk %(dev)s changes at the next start; shut the VM down to clone it"), dev=dev)
if self.instance.jobInfo()[0] != VIR_DOMAIN_JOB_NONE:
- raise util.OperationError("Another job is running on the VM; clone it when that ends")
+ raise util.OperationError(gettext_noop("Another job is running on the VM; clone it when that ends"))
targets, pools, copied = [], {}, {}
for source, dev in disks:
@@ -2572,7 +2583,7 @@ class wvmInstance(wvmConnect):
if err.get_error_code() != VIR_ERR_NO_STORAGE_VOL:
raise
else:
- raise util.OperationError(f"Volume {name} already exists in storage pool {pool.name()}")
+ raise util.OperationError(gettext_noop("Volume %(name)s already exists in storage pool %(pool)s"), name=name, pool=pool.name())
capacity = vol.info()[1]
pools[pool.name()] = pool
copied[pool.name()] = copied.get(pool.name(), 0) + capacity
@@ -2590,10 +2601,10 @@ class wvmInstance(wvmConnect):
needed = size + max(size * free_percent / 100, free_min_gib * 1024**3)
free = pools[name].info()[3]
if free < needed:
- raise util.OperationError(
- f"Storage pool {name} has {free / 1024**3:.1f} GiB free; the clone needs {needed / 1024**3:.1f} GiB "
- f"(the copied disks, plus {free_percent:g}% of them, at least {free_min_gib:g} GiB)"
- )
+ raise util.OperationError(gettext_noop(
+ "Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f GiB (the copied disks, "
+ "plus %(percent)g%% of them, at least %(minimum)g GiB)"),
+ pool=name, free=free / 1024**3, size=needed / 1024**3, percent=free_percent, minimum=free_min_gib)
return targets
def create_clone_target(self, target, owner_uid, owner_gid):
@@ -2730,7 +2741,7 @@ class wvmInstance(wvmConnect):
for xml_flags, affect in self._definitions():
nic = self._nic(xml_flags, mac_address)
if nic is not None and nic.get("type") == "hostdev":
- raise util.OperationError("A network interface that is a host device is removed in the XML editor")
+ raise util.OperationError(gettext_noop("A network interface that is a host device is removed in the XML editor"))
if nic is not None:
removed = etree.tostring(nic).decode()
self.instance.detachDeviceFlags(removed, affect)
@@ -2746,10 +2757,10 @@ class wvmInstance(wvmConnect):
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
found = tree.xpath("./devices/interface[mac/@address=$mac]", mac=old_mac)
if not found:
- raise util.OperationError(f"No network interface with MAC {old_mac}")
+ raise util.OperationError(gettext_noop("No network interface with MAC %(old_mac)s"), old_mac=old_mac)
iface = found[0]
if iface.get("type") == "hostdev":
- raise util.OperationError("A network interface that is a host device is changed in the XML editor")
+ raise util.OperationError(gettext_noop("A network interface that is a host device is changed in the XML editor"))
if source is not None:
iface_type = {"net": "network", "bridge": "bridge"}.get(source_type, "direct")
@@ -2840,7 +2851,7 @@ class wvmInstance(wvmConnect):
refused."""
status = self.instance.info()[0]
if self.instance.hasManagedSaveImage(0) or status not in (VIR_DOMAIN_RUNNING, VIR_DOMAIN_SHUTOFF):
- raise util.OperationError("A VM changes its PCI devices while it runs, or is shut off without a saved state")
+ raise util.OperationError(gettext_noop("A VM changes its PCI devices while it runs, or is shut off without a saved state"))
return status == VIR_DOMAIN_RUNNING
def _pci_outcome(self, address, err):
@@ -2852,12 +2863,20 @@ class wvmInstance(wvmConnect):
now = running and self._pci_hostdev(address, VIR_DOMAIN_XML_SECURE) is not None
later = self._pci_hostdev(address) is not None
except libvirtError:
- return util.OperationError(f"{err}; whether the VM has PCI device {address} now is not known")
- next_start = "its next start has it" if later else "its next start does not have it"
- if not running:
- return util.OperationError(f"{err}; PCI device {address}: the VM is not running, {next_start}")
- now_text = "the running VM has it" if now else "the running VM does not have it"
- return util.OperationError(f"{err}; PCI device {address}: {now_text}, {next_start}")
+ return util.OperationError(gettext_noop("%(err)s; whether the VM has PCI device %(address)s now is not known"), err=err, address=address)
+ outcome = {
+ (False, True): gettext_noop("%(err)s; PCI device %(address)s: the VM is not running, its next start has it"),
+ (False, False): gettext_noop(
+ "%(err)s; PCI device %(address)s: the VM is not running, its next start does not have it"),
+ (True, True): gettext_noop("%(err)s; PCI device %(address)s: the running VM has it, its next start has it"),
+ (True, False): gettext_noop(
+ "%(err)s; PCI device %(address)s: the running VM has it, its next start does not have it"),
+ (None, True): gettext_noop(
+ "%(err)s; PCI device %(address)s: the running VM does not have it, its next start has it"),
+ (None, False): gettext_noop(
+ "%(err)s; PCI device %(address)s: the running VM does not have it, its next start does not have it"),
+ }[(now or None) if running else False, later]
+ return util.OperationError(outcome, err=err, address=address)
def add_pci_hostdev(self, address):
"""Gives the VM the host's PCI device at address (0000:05:00.0), for
@@ -2889,7 +2908,7 @@ class wvmInstance(wvmConnect):
live = self._pci_hostdev(address, VIR_DOMAIN_XML_SECURE) if running else None
saved = self._pci_hostdev(address)
if live is None and saved is None:
- raise util.OperationError(f"The VM has no PCI device {address}")
+ raise util.OperationError(gettext_noop("The VM has no PCI device %(address)s"), address=address)
try:
if live is not None: # a refusal stops here; a request the guest has not answered goes on
self.instance.detachDeviceFlags(etree.tostring(live, encoding="unicode"), VIR_DOMAIN_AFFECT_LIVE)
@@ -2907,7 +2926,7 @@ class wvmInstance(wvmConnect):
model. Refused without a USB controller."""
for xml_flags, _ in self._definitions():
if not etree.fromstring(self._XMLDesc(xml_flags)).xpath("devices/controller[@type='usb'][not(@model='none')]"):
- raise util.OperationError("The VM has no USB controller")
+ raise util.OperationError(gettext_noop("The VM has no USB controller"))
hostdev = etree.Element("hostdev", mode="subsystem", type="usb", managed="yes")
source = etree.SubElement(hostdev, "source")
etree.SubElement(source, "vendor", id=vendor)
@@ -2926,7 +2945,7 @@ class wvmInstance(wvmConnect):
found = True
self.instance.detachDeviceFlags(etree.tostring(element, encoding="unicode"), affect)
if not found:
- raise util.OperationError("The VM has no such USB device")
+ raise util.OperationError(gettext_noop("The VM has no such USB device"))
def set_link_state(self, mac_address, state):
for xml_flags, affect in self._definitions():
@@ -2967,7 +2986,7 @@ class wvmInstance(wvmConnect):
"""Adds the panel's virtio RNG device to the persistent definition, or
removes it; another RNG device is left to the XML editor."""
if self.get_rng() == "custom":
- raise util.OperationError("The VM has its own RNG device: change it in the XML editor")
+ raise util.OperationError(gettext_noop("The VM has its own RNG device: change it in the XML editor"))
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
for rng in tree.xpath("devices/rng"):
rng.getparent().remove(rng)
@@ -2994,14 +3013,14 @@ class wvmInstance(wvmConnect):
another TPM is left to the XML editor. Its state stays on the host until
the VM is undefined, so a TPM added again has it back."""
if self.get_tpm() == "custom":
- raise util.OperationError("The VM has its own TPM: change it in the XML editor")
+ raise util.OperationError(gettext_noop("The VM has its own TPM: change it in the XML editor"))
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
for tpm in tree.xpath("devices/tpm"):
tpm.getparent().remove(tpm)
if enabled:
model = self.get_tpm_model(self.get_arch(), self.get_machine_type())
if not model:
- raise util.OperationError("This host offers no TPM 2.0: install swtpm on it")
+ raise util.OperationError(gettext_noop("This host offers no TPM 2.0: install swtpm on it"))
tree.find("devices").append(
etree.fromstring(f""))
self._defineXML(etree.tostring(tree).decode())
@@ -3013,7 +3032,7 @@ class wvmInstance(wvmConnect):
def set_on_crash(self, action):
"""restart or destroy (stop) the VM when the guest crashes, from the next start."""
if action not in ("restart", "destroy"):
- raise util.OperationError(f"Unknown crash action {action}")
+ raise util.OperationError(gettext_noop("Unknown crash action %(action)s"), action=action)
tree = etree.fromstring(self._XMLDesc(PERSISTENT_XML))
on_crash = tree.find("on_crash")
if on_crash is None:
diff --git a/vrtManager/interface.py b/vrtManager/interface.py
index 53293326..48a5bcc7 100644
--- a/vrtManager/interface.py
+++ b/vrtManager/interface.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import contextlib
from functools import cached_property
from xml.etree import ElementTree
@@ -31,8 +32,7 @@ def changes(conn):
try:
conn.changeRollback(0)
except libvirtError as rollback_err:
- raise util.OperationError(f"{err}; rolling the change back failed too, "
- f"the host's configuration may be changed: {rollback_err}") from err
+ raise util.OperationError(gettext_noop("%(err)s; rolling the change back failed too, the host's configuration may be changed: %(rollback_err)s"), err=err, rollback_err=rollback_err) from err
raise
@@ -266,12 +266,11 @@ class wvmInterface(wvmConnect):
refused, which of them takes the addresses being the user's choice.
All of it is one change of the host's configuration."""
if self.iface.isActive():
- raise util.OperationError(f"Stop interface {self.iface.name()} before deleting it")
+ raise util.OperationError(gettext_noop("Stop interface %(iface)s before deleting it"), iface=self.iface.name())
tree = ElementTree.fromstring(self.iface.XMLDesc(VIR_INTERFACE_XML_INACTIVE))
members = tree.findall("bridge/interface")
if len(members) > 1 and tree.findall("protocol"):
- raise util.OperationError(f"Bridge {self.iface.name()} has addresses and several members: "
- "give the addresses to one of them on the host first")
+ raise util.OperationError(gettext_noop("Bridge %(iface)s has addresses and several members: give the addresses to one of them on the host first"), iface=self.iface.name())
with changes(self.wvm):
self.iface.undefine()
for member in members:
diff --git a/vrtManager/network.py b/vrtManager/network.py
index 56d2978e..2c35e6ef 100644
--- a/vrtManager/network.py
+++ b/vrtManager/network.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import copy
import ipaddress
from datetime import datetime, timezone
@@ -295,15 +296,13 @@ class wvmNetwork(wvmConnect):
def stop(self):
"""Refused while a running VM is on it: that VM would be cut off."""
if vms := self._vms_on_it(running_only=True):
- raise util.OperationError(f"Network {self.net.name()} is in use by the running VMs {', '.join(vms)}; "
- "shut them down or remove their network cards first")
+ raise util.OperationError(gettext_noop("Network %(net)s is in use by the running VMs %(vms)s; shut them down or remove their network cards first"), net=self.net.name(), vms=', '.join(vms))
self.net.destroy()
def delete(self):
"""Refused while a VM's definition names it: that VM would no longer start."""
if vms := self._vms_on_it(running_only=False):
- raise util.OperationError(f"VMs {', '.join(vms)} use network {self.net.name()} and would no longer "
- "start; remove their network cards first")
+ raise util.OperationError(gettext_noop("VMs %(vms)s use network %(net)s and would no longer start; remove their network cards first"), vms=', '.join(vms), net=self.net.name())
self.net.undefine()
def update(self, command, section, xml, parentIndex):
@@ -312,8 +311,7 @@ class wvmNetwork(wvmConnect):
active = self.is_active()
if active and self._addresses(self.tree) != self._addresses(self.config()):
# parentIndex names the same in both only when they list the same addresses
- raise util.OperationError(f"Network {self.net.name()} runs with other addresses than it has saved; "
- "stop and start it first")
+ raise util.OperationError(gettext_noop("Network %(net)s runs with other addresses than it has saved; stop and start it first"), net=self.net.name())
flags = VIR_NETWORK_UPDATE_AFFECT_CONFIG | (VIR_NETWORK_UPDATE_AFFECT_LIVE if active else 0)
self.net.update(command, section, parentIndex, xml, flags)
self._forget()
@@ -338,36 +336,47 @@ class wvmNetwork(wvmConnect):
tree = self.config()
ips = tree.findall("ip")
if not 0 <= index < len(ips):
- raise util.OperationError(f"Network {self.net.name()} has no such address")
+ raise util.OperationError(gettext_noop("Network %(net)s has no such address"), net=self.net.name())
return tree, ips[index], ip_networks(tree)[index]
+ # an address outside the network: (with its subnet, without one)
+ OUTSIDE = {
+ "range start": (gettext_noop("The range start %(address)s is not in %(subnet)s"),
+ gettext_noop("The range start %(address)s is not in the network")),
+ "range end": (gettext_noop("The range end %(address)s is not in %(subnet)s"),
+ gettext_noop("The range end %(address)s is not in the network")),
+ "fixed address": (gettext_noop("The fixed address %(address)s is not in %(subnet)s"),
+ gettext_noop("The fixed address %(address)s is not in the network")),
+ }
+
def _checked_address(self, ip, address, what):
"""address as an address of the ip (ip_networks): of its family and
subnet, and not the host's own."""
try:
address = ipaddress.ip_address(address)
except ValueError:
- raise util.OperationError(f"{address} is not an IP address") from None
+ raise util.OperationError(gettext_noop("%(address)s is not an IP address"), address=address) from None
if ip["subnet"] is None or address not in ip["subnet"]:
- raise util.OperationError(f"The {what} {address} is not in {ip['subnet'] or 'the network'}")
+ outside = self.OUTSIDE[what][ip["subnet"] is None]
+ raise util.OperationError(outside, address=address, subnet=ip["subnet"])
if address == ipaddress.ip_address(ip["address"]):
- raise util.OperationError(f"{address} is the host's own address on the network")
+ raise util.OperationError(gettext_noop("%(address)s is the host's own address on the network"), address=address)
return address
def modify_dhcp_range(self, index, range_start, range_end):
"""Refused while the network runs: the running network would keep the old range."""
if self.is_active():
- raise util.OperationError(f"Stop network {self.net.name()} to change its DHCP range")
+ raise util.OperationError(gettext_noop("Stop network %(net)s to change its DHCP range"), net=self.net.name())
tree, ip, info = self._ip(index)
dhcp_range = ip.find("dhcp/range")
if dhcp_range is None:
- raise util.OperationError(f"This address of network {self.net.name()} has no DHCP range")
+ raise util.OperationError(gettext_noop("This address of network %(net)s has no DHCP range"), net=self.net.name())
start = self._checked_address(info, range_start, "range start")
end = self._checked_address(info, range_end, "range end")
if start > end:
- raise util.OperationError(f"The range starts at {start}, after its end {end}")
+ raise util.OperationError(gettext_noop("The range starts at %(start)s, after its end %(end)s"), start=start, end=end)
if start <= ipaddress.ip_address(info["address"]) <= end:
- raise util.OperationError(f"The range takes in {info['address']}, the host's own address")
+ raise util.OperationError(gettext_noop("The range takes in %(address)s, the host's own address"), address=info['address'])
dhcp_range.set("start", str(start))
dhcp_range.set("end", str(end))
self._define(tree)
@@ -376,7 +385,7 @@ class wvmNetwork(wvmConnect):
_tree, ip, _info = self._ip(index)
host = next((h for h in ip.findall("dhcp/host") if h.get("ip") == address), None)
if host is None:
- raise util.OperationError(f"Network {self.net.name()} has no fixed address {address}")
+ raise util.OperationError(gettext_noop("Network %(net)s has no fixed address %(address)s"), net=self.net.name(), address=address)
# the host as it is: a host may have only a name and an address
self.update(VIR_NETWORK_UPDATE_COMMAND_DELETE, VIR_NETWORK_SECTION_IP_DHCP_HOST,
etree.tostring(host, with_tail=False).decode(), index)
@@ -398,16 +407,16 @@ class wvmNetwork(wvmConnect):
if original:
old = next((h for h in hosts if h.get("ip") == original), None)
if old is None:
- raise util.OperationError(f"The fixed address {original} is gone meanwhile; reload the page")
+ raise util.OperationError(gettext_noop("The fixed address %(original)s is gone meanwhile; reload the page"), original=original)
for other in hosts:
if other is old:
continue
if other.get("ip") == str(address):
- raise util.OperationError(f"{address} is another entry's fixed address")
+ raise util.OperationError(gettext_noop("%(address)s is another entry's fixed address"), address=address)
if mac_duid and other.get(key, "").lower() == mac_duid.lower():
- raise util.OperationError(f"{mac_duid} has another fixed address, {other.get('ip')}")
+ raise util.OperationError(gettext_noop("%(mac_duid)s has another fixed address, %(other)s"), mac_duid=mac_duid, other=other.get('ip'))
if name and other.get("name") == name:
- raise util.OperationError(f"The name {name} has another fixed address, {other.get('ip')}")
+ raise util.OperationError(gettext_noop("The name %(name)s has another fixed address, %(other)s"), name=name, other=other.get('ip'))
new = copy.deepcopy(old) if old is not None else etree.Element("host")
new.tail = None
for attribute, value in ((key, mac_duid), ("name", name), ("ip", str(address))):
@@ -443,11 +452,11 @@ class wvmNetwork(wvmConnect):
def set_qos(self, direction, average, peak, burst):
if direction not in ("inbound", "outbound"):
- raise ValueError("Direction must be inbound or outbound")
+ raise util.OperationError(gettext_noop("Direction must be inbound or outbound"))
try:
average, peak, burst = int(average), int(peak), int(burst)
except ValueError:
- raise ValueError("QoS rates must be whole numbers")
+ raise util.OperationError(gettext_noop("QoS rates must be whole numbers")) from None
tree = self.config()
band = tree.find("bandwidth")
if band is None:
@@ -470,8 +479,8 @@ class wvmNetwork(wvmConnect):
names one, the same UUID, not another network."""
tree = util.parse_xml(new_xml)
if tree.tag != "network" or tree.findtext("name") != self.net.name():
- raise util.OperationError(f"The XML must define network {self.net.name()}")
+ raise util.OperationError(gettext_noop("The XML must define network %(net)s"), net=self.net.name())
if tree.findtext("uuid") not in (None, self.net.UUIDString()):
- raise util.OperationError("The XML must keep the network's UUID")
+ raise util.OperationError(gettext_noop("The XML must keep the network's UUID"))
self.wvm.networkDefineXML(new_xml)
self._forget()
diff --git a/vrtManager/nodedevs.py b/vrtManager/nodedevs.py
index 2a510e12..b92d0465 100644
--- a/vrtManager/nodedevs.py
+++ b/vrtManager/nodedevs.py
@@ -1,4 +1,5 @@
from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext_noop
from libvirt import (VIR_CONNECT_LIST_NODE_DEVICES_CAP_PCI_DEV, VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV,
VIR_ERR_NO_NODE_DEVICE, libvirtError)
from lxml import etree
@@ -104,14 +105,26 @@ def usb_named(selector, model, address):
return "model" if selected is None else None
+# Problems are messages (util.message), shown in the reader's language; a
+# device's label is one too, put in each problem of it
+def pci_label(address):
+ return [util.message(gettext_noop("PCI device %(address)s"), address=address)]
+
+
+def usb_label(device):
+ return [util.message(gettext_noop("USB device %(bus)s/%(device)s (%(product)s)"),
+ bus=device["bus"], device=device["device"], product=device["product"])]
+
+
def device_problems(label, vms, resources, vm):
"""Why a host device cannot be given to the VM named vm: the other VMs
that name it, and what the host has on it."""
problems = []
if others := sorted(set(vms) - {vm}):
- problems.append(f"{label} is given to {', '.join(others)}")
+ problems.append(util.message(gettext_noop("%(device)s is given to %(vms)s"), device=label, vms=", ".join(others)))
if resources:
- problems.append(f"{label} holds the host's {', '.join(resources)}")
+ problems.append(util.message(gettext_noop("%(device)s holds the host's %(resources)s"), device=label,
+ resources=", ".join(resources)))
return problems
@@ -130,29 +143,37 @@ def pci_problems(vm, device, by_address, managed):
be one of managed, or have no driver or vfio-pci; no other VM may have
any of them; bridges stay with the pcieport driver or none. Nothing in
the group is taken implicitly."""
- label = f"PCI device {device['address']}"
+ label = pci_label(device["address"])
# a bridge of any kind (a host bridge, a PCIe port) is the host's
if device["bridge"] or device["bridge_class"]:
- return [f"{label} is a bridge"]
+ return [util.message(gettext_noop("%(device)s is a bridge"), device=label)]
problems = device_problems(label, device["vms"], device["resources"], vm)
if device["resources"]:
- problems.append(f"bind {device['address']} to vfio-pci on the host first if it is not needed there")
+ problems.append(util.message(gettext_noop(
+ "bind %(address)s to vfio-pci on the host first if it is not needed there"), address=device["address"]))
if device["group"] is None:
- return problems + [f"{label} is in no IOMMU group"]
- shares = f"{label} shares IOMMU group {device['group']} with"
+ return problems + [util.message(gettext_noop("%(device)s is in no IOMMU group"), device=label)]
+ group = {"device": label, "group": device["group"]}
for member in device["group_members"]:
if member == device["address"]:
continue
other = by_address.get(member)
if other is None:
- problems.append(f"{shares} {member}, which cannot be read")
+ problems.append(util.message(gettext_noop(
+ "%(device)s shares IOMMU group %(group)s with %(member)s, which cannot be read"), **group, member=member))
elif others := sorted(set(other["vms"]) - {vm}):
- problems.append(f"{shares} {member} ({other['product']}), which {', '.join(others)} uses")
+ problems.append(util.message(gettext_noop(
+ "%(device)s shares IOMMU group %(group)s with %(member)s (%(product)s), which %(vms)s uses"),
+ **group, member=member, product=other["product"], vms=", ".join(others)))
elif other["bridge"]:
if other["driver"] not in ("", "pcieport"):
- problems.append(f"{shares} the bridge {member} (driver {other['driver']})")
+ problems.append(util.message(gettext_noop(
+ "%(device)s shares IOMMU group %(group)s with the bridge %(member)s (driver %(driver)s)"),
+ **group, member=member, driver=other["driver"]))
elif member not in managed and other["driver"] not in ("", "vfio-pci"):
- problems.append(f"{shares} {member} ({other['product']}), which the host (driver {other['driver']}) uses")
+ problems.append(util.message(gettext_noop(
+ "%(device)s shares IOMMU group %(group)s with %(member)s (%(product)s), which the host "
+ "(driver %(driver)s) uses"), **group, member=member, product=other["product"], driver=other["driver"]))
return problems
@@ -163,20 +184,23 @@ def hostdev_problems(vm, pci, usb, wanted_pci, wanted_usb, managed=frozenset()):
another VM names them or their PCI controller too, the host has its own
network interfaces or disks on them, or they are hubs."""
by_address = {device["address"]: device for device in pci}
- problems = [f"PCI device {address} is not on the host" for address in sorted(wanted_pci - by_address.keys())]
+ problems = [util.message(gettext_noop("%(device)s is not on the host"), device=pci_label(address))
+ for address in sorted(wanted_pci - by_address.keys())]
for device in pci:
if device["address"] in wanted_pci:
problems += pci_problems(vm, device, by_address, managed)
for device in usb:
address = (device["bus"], device["device"])
if any(usb_named(selector, device["model"], address) for selector in wanted_usb):
- label = f"USB device {device['bus']}/{device['device']} ({device['product']})"
+ label = usb_label(device)
problems += device_problems(label, device["vms"] + device["by_model"], device["resources"], vm)
if device["hub"]:
- problems.append(f"{label} is a hub")
+ problems.append(util.message(gettext_noop("%(device)s is a hub"), device=label))
controller = by_address.get(device["controller"])
if controller and (holders := sorted(set(controller["vms"]) - {vm})):
- problems.append(f"{label} is on the PCI controller {controller['address']}, given to {', '.join(holders)}")
+ problems.append(util.message(gettext_noop(
+ "%(device)s is on the PCI controller %(controller)s, given to %(vms)s"),
+ device=label, controller=controller["address"], vms=", ".join(holders)))
return problems
@@ -304,4 +328,5 @@ def check_hostdevs(conn, vm, trees):
for tree, (wanted_pci, wanted_usb) in wanted:
problems += hostdev_problems(vm, pci, usb, wanted_pci, wanted_usb, managed_pci(tree))
if problems:
- raise util.OperationError("; ".join(dict.fromkeys(problems)))
+ unique = list({util.render(m["text"], m["values"], str): m for m in problems}.values())
+ raise util.OperationError(gettext_noop("%(problems)s"), problems=unique)
diff --git a/vrtManager/nwfilters.py b/vrtManager/nwfilters.py
index c963f3e4..c1786146 100644
--- a/vrtManager/nwfilters.py
+++ b/vrtManager/nwfilters.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import copy
import re
from functools import cached_property
@@ -58,23 +59,23 @@ class wvmNWFilters(wvmConnect):
host has is refused, as it would replace that filter."""
tree = util.parse_xml(xml)
if tree.tag != "filter":
- raise util.OperationError("Enter one element")
+ raise util.OperationError(gettext_noop("Enter one element"))
if not NAME.fullmatch(tree.get("name") or ""):
- raise util.OperationError("The filter's name may have letters, digits, dots, dashes and underscores")
+ raise util.OperationError(gettext_noop("The filter's name may have letters, digits, dots, dashes and underscores"))
for nwfilter in self.wvm.listAllNWFilters(0):
if nwfilter.name() == tree.get("name"):
- raise util.OperationError("A network filter with this name already exists")
+ raise util.OperationError(gettext_noop("A network filter with this name already exists"))
if nwfilter.UUIDString() == tree.findtext("uuid"):
- raise util.OperationError("A network filter with this UUID already exists")
+ raise util.OperationError(gettext_noop("A network filter with this UUID already exists"))
self.wvm.nwfilterDefineXML(xml)
def clone_nwfilter(self, name, clone_name):
"""Define a copy of filter name as clone_name, with its rules,
references and their parameters, and a UUID of its own."""
if not NAME.fullmatch(clone_name):
- raise util.OperationError("The filter's name may have letters, digits, dots, dashes and underscores")
+ raise util.OperationError(gettext_noop("The filter's name may have letters, digits, dots, dashes and underscores"))
if clone_name in self.get_nwfilters():
- raise util.OperationError(f"A network filter named {clone_name} already exists")
+ raise util.OperationError(gettext_noop("A network filter named %(clone_name)s already exists"), clone_name=clone_name)
tree = etree.fromstring(self.get_nwfilter(name).XMLDesc(0))
tree.set("name", clone_name)
tree.remove(tree.find("uuid"))
@@ -86,10 +87,10 @@ class wvmNWFilters(wvmConnect):
or a VM using a filter that references it, would not start."""
vms = self.get_users().get(name, [])
if vms:
- raise util.OperationError(f"Filter {name} is used by VM {', '.join(vms)}")
+ raise util.OperationError(gettext_noop("Filter %(name)s is used by VM %(vms)s"), name=name, vms=', '.join(vms))
referencing = [nwfilter["name"] for nwfilter in self.get_nwfilters_info() if name in nwfilter["refs"]]
if referencing:
- raise util.OperationError(f"Filter {name} is referenced by filter {', '.join(referencing)}")
+ raise util.OperationError(gettext_noop("Filter %(name)s is referenced by filter %(referencing)s"), name=name, referencing=', '.join(referencing))
self.get_nwfilter(name).undefine()
@@ -155,7 +156,7 @@ class wvmNWFilter(wvmNWFilters):
except ValueError:
index = -1
if not 0 <= index < len(elements) or element_text(elements[index]) != same_text(text):
- raise util.OperationError("The filter has changed; reload the page and try again")
+ raise util.OperationError(gettext_noop("The filter has changed; reload the page and try again"))
tree.remove(elements[index])
self._define(tree)
return elements[index]
@@ -165,9 +166,9 @@ class wvmNWFilter(wvmNWFilters):
its name and UUID."""
tree = util.parse_xml(xml)
if tree.tag != "filter" or tree.get("name") != self.get_name():
- raise util.OperationError(f"The XML must define filter {self.get_name()}")
+ raise util.OperationError(gettext_noop("The XML must define filter %(name)s"), name=self.get_name())
if tree.findtext("uuid") != self.get_uuid():
- raise util.OperationError("The XML must keep the filter's UUID")
+ raise util.OperationError(gettext_noop("The XML must keep the filter's UUID"))
self.wvm.nwfilterDefineXML(xml)
def delete_rule(self, index, text):
@@ -179,7 +180,7 @@ class wvmNWFilter(wvmNWFilters):
def add_ref(self, name):
if name not in self.get_ref_choices():
- raise util.OperationError(f"Filter {name} cannot be referenced: it is this one, referenced already or not on the host")
+ raise util.OperationError(gettext_noop("Filter %(name)s cannot be referenced: it is this one, referenced already or not on the host"), name=name)
tree = copy.deepcopy(self.tree)
etree.SubElement(tree, "filterref", filter=name)
self._define(tree)
@@ -189,7 +190,7 @@ class wvmNWFilter(wvmNWFilters):
protocol per rule, so it is never merged into another rule."""
rule = util.parse_xml(xml)
if rule.tag != "rule":
- raise util.OperationError("Enter one element")
+ raise util.OperationError(gettext_noop("Enter one element"))
tree = copy.deepcopy(self.tree)
tree.append(rule)
self._define(tree)
diff --git a/vrtManager/storage.py b/vrtManager/storage.py
index b56dab21..81d76faf 100644
--- a/vrtManager/storage.py
+++ b/vrtManager/storage.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import contextlib
import libvirt
@@ -188,8 +189,7 @@ class wvmStorage(wvmConnect):
running_only=True,
)
if vms:
- raise util.OperationError(f"Pool {name} holds disks of the running VMs {', '.join(vms)}; "
- "shut them down first")
+ raise util.OperationError(gettext_noop("Pool %(name)s holds disks of the running VMs %(vms)s; shut them down first"), name=name, vms=', '.join(vms))
return self.pool.destroy()
def delete(self):
@@ -197,8 +197,7 @@ class wvmStorage(wvmConnect):
no longer start. A disk given by its file path does not need it."""
name = self.pool.name()
if vms := self.domains_using(lambda domain: domain.xpath("devices/disk/source[@pool=$name]", name=name)):
- raise util.OperationError(f"VMs {', '.join(vms)} have disks in pool {name} by its name and would no "
- "longer start; detach those disks first")
+ raise util.OperationError(gettext_noop("VMs %(vms)s have disks in pool %(name)s by its name and would no longer start; detach those disks first"), vms=', '.join(vms), name=name)
self.pool.undefine()
def get_autostart(self):
@@ -277,7 +276,7 @@ class wvmStorage(wvmConnect):
"""libvirt deletes any volume; one a VM still needs is refused."""
vol = self.pool.storageVolLookupByName(name)
if vol.path() in self.paths_in_use():
- raise util.OperationError(f"Volume {name} is in use: a VM's disk, a backing file or a snapshot's file")
+ raise util.OperationError(gettext_noop("Volume %(name)s is in use: a VM's disk, a backing file or a snapshot's file"), name=name)
vol.delete(0)
def get_volume_format_type(self, name):
diff --git a/vrtManager/tests/test_util.py b/vrtManager/tests/test_util.py
index 08089c7b..f53973f8 100644
--- a/vrtManager/tests/test_util.py
+++ b/vrtManager/tests/test_util.py
@@ -161,6 +161,32 @@ class OperationErrorTestCase(unittest.TestCase):
# a text without values is not formatted: libvirt's may hold a %
self.assertEqual(str(OperationError("100% used")), "100% used")
+ def test_reads_in_the_active_language_and_keeps_english_in_its_args(self):
+ from django.utils import translation
+ from vrtManager.util import OperationError, message
+
+ error = OperationError("The disks cannot be migrated: %(problems)s", problems=[
+ message("%(target)s: the VM has no disk file to copy", target="vda"),
+ message("%(target)s (%(path)s): %(refusal)s", target="sda", path="/i/os.iso",
+ refusal=[message("an image is not copied; eject it first")])])
+ english = ("The disks cannot be migrated: vda: the VM has no disk file to copy; "
+ "sda (/i/os.iso): an image is not copied; eject it first")
+ self.assertEqual(error.args[0], english)
+ with translation.override("tr"):
+ self.assertNotEqual(str(error), english)
+ self.assertEqual(error.args[0], english) # logs and the audit log stay English
+ self.assertIn("vda", str(error))
+
+ def test_a_job_keeps_number_values_for_their_formats(self):
+ from operations.utils import said, words
+ from vrtManager.util import OperationError
+
+ error = OperationError("Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f GiB "
+ "(the copied disks, plus %(percent)g%% of them, at least %(minimum)g GiB)",
+ pool="default", free=1.25, size=3.0, percent=10, minimum=2)
+ self.assertEqual(words(said(error)), "Storage pool default has 1.2 GiB free; the clone needs 3.0 GiB "
+ "(the copied disks, plus 10% of them, at least 2 GiB)")
+
if __name__ == "__main__":
unittest.main()
diff --git a/vrtManager/tests/test_xml_safety.py b/vrtManager/tests/test_xml_safety.py
index b41b4d06..2a78932c 100644
--- a/vrtManager/tests/test_xml_safety.py
+++ b/vrtManager/tests/test_xml_safety.py
@@ -48,11 +48,11 @@ class TestXmlSafety(unittest.TestCase):
wn.wvm = MagicMock()
# Invalid direction
- with self.assertRaises(ValueError):
+ with self.assertRaises(util.OperationError):
wn.set_qos("lateral", 100, 200, 300)
# Invalid numeric strings
- with self.assertRaises(ValueError):
+ with self.assertRaises(util.OperationError):
wn.set_qos("inbound", "not_a_number", 200, 300)
def test_snapshot_xml_payload_is_escaped(self):
diff --git a/vrtManager/util.py b/vrtManager/util.py
index f23c4548..eda7ae6a 100644
--- a/vrtManager/util.py
+++ b/vrtManager/util.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import ipaddress
import random
import secrets
@@ -8,23 +9,47 @@ import libvirt
import lxml.etree as etree
from django.conf import settings
+from django.utils.translation import gettext
+def message(text, **values):
+ """Our text, marked with gettext_noop, with %(name)s for each of values.
+ A value may be a list of messages: they are put in its place one after
+ another."""
+ return {"text": str(text), "values": values}
+
+
+def render(text, values, translate):
+ """text with its values, through translate (gettext, or str for English)."""
+ def shown(value):
+ if isinstance(value, list):
+ return "; ".join(render(m["text"], m["values"], translate) for m in value)
+ if isinstance(value, OperationError):
+ return render(value.text, value.values, translate)
+ return value
+ text = translate(text)
+ return text % {key: shown(value) for key, value in values.items()} if values else text
+
+
class OperationError(libvirt.libvirtError):
- """An error with WebVirtCloud's own message: text, with %(name)s for each
- of values. str() is the English message; a page or a job shows text
- translated, with the values (operations.utils.said).
+ """An error with WebVirtCloud's own message (see message()). Its args are
+ the English message, for logs and the audit log; str() is the message in
+ the active language, English outside a request. A job keeps the text and
+ values (operations.utils.said) and shows them in its reader's language.
libvirtError(msg) reports the thread's last libvirt error instead of msg
when there is one, so an earlier, unrelated failure would be shown.
"""
def __init__(self, text, **values):
- Exception.__init__(self, text % values if values else text)
self.text, self.values = str(text), values
+ Exception.__init__(self, render(self.text, values, str))
self.err = None
+ def __str__(self):
+ return render(self.text, self.values, gettext)
+
class ConnectionFailed(OperationError):
"""The host could not be connected to; the message may name it and how it
@@ -112,9 +137,9 @@ def parse_xml(xml):
try:
tree = etree.fromstring(xml.encode(), parser) # bytes: the XML may declare its encoding
except etree.XMLSyntaxError as err:
- raise OperationError(f"The XML is not valid: {err}") from None
+ raise OperationError(gettext_noop("The XML is not valid: %(err)s"), err=err) from None
if tree.getroottree().docinfo.internalDTD is not None:
- raise OperationError("The XML may not declare a DTD")
+ raise OperationError(gettext_noop("The XML may not declare a DTD"))
return tree
diff --git a/vrtManager/virtsecrets.py b/vrtManager/virtsecrets.py
index 8dba8070..ede90744 100644
--- a/vrtManager/virtsecrets.py
+++ b/vrtManager/virtsecrets.py
@@ -1,3 +1,4 @@
+from django.utils.translation import gettext_noop
import base64
from libvirt import VIR_ERR_NO_STORAGE_POOL, VIR_STORAGE_XML_INACTIVE, libvirtError
@@ -22,7 +23,7 @@ def value_bytes(usage_type, value):
try:
return base64.b64decode(value, validate=True)
except ValueError: # not base64, or not ASCII; the message never holds the value
- raise util.OperationError("A Ceph key is base64 text, as `ceph auth get-key` prints it") from None
+ raise util.OperationError(gettext_noop("A Ceph key is base64 text, as `ceph auth get-key` prints it")) from None
def secrets_named(tree, path, by_usage):
@@ -111,8 +112,8 @@ class wvmSecrets(wvmConnect):
if users := self.get_users(self.get_secrets_info()).get(uuid):
named = []
if users["vms"]:
- named.append(f"VMs {', '.join(users['vms'])}")
+ named.append(util.message(gettext_noop("VMs %(names)s"), names=", ".join(users["vms"])))
if users["pools"]:
- named.append(f"pools {', '.join(users['pools'])}")
- raise util.OperationError(f"Secret {uuid} is used by {'; '.join(named)}; remove it from them first")
+ named.append(util.message(gettext_noop("pools %(names)s"), names=", ".join(users["pools"])))
+ raise util.OperationError(gettext_noop("Secret %(uuid)s is used by %(named)s; remove it from them first"), uuid=uuid, named=named)
secret.undefine()