diff --git a/instances/jobs.py b/instances/jobs.py index 1a22ca17..1956a0bf 100644 --- a/instances/jobs.py +++ b/instances/jobs.py @@ -31,7 +31,7 @@ from instances import utils from instances.models import Instance from logs.audit import write from operations import claims -from operations.utils import safe_text, say +from operations.utils import REPORTED, safe_text, say from operations.jobs import Cancelled, Kind, Outcome, Plan, register from operations.models import Claim, Operation @@ -666,7 +666,10 @@ class CloneInstance(Kind): except libvirtError as err: thawed = say(THAW_FAILED, error=err, vm=op.resource_label.rpartition("/")[2]) outcome = self._live_outcome(proxy, op) - outcome.error += thawed + if thawed and outcome.state == Operation.SUCCEEDED: # a warning of the clone, as when the job made it + outcome.result["warning"] = [*outcome.result.get("warning", []), *thawed] + else: + outcome.error += thawed return outcome def _live_outcome(self, proxy, op): @@ -899,6 +902,10 @@ class CreateInstance(Kind): conn.delete_volume(path) except libvirtError: leftovers.append(path) + if leftovers and not isinstance(err, REPORTED): # a fault of the panel is not shown as it is + raise util.OperationError(_("The panel failed unexpectedly; its log has the details. " + "Remove these volumes by hand: %(volumes)s"), + volumes=", ".join(leftovers)) from err if leftovers: raise util.OperationError(_("%(error)s. Remove these volumes by hand: %(volumes)s"), error=err, volumes=", ".join(leftovers)) from err diff --git a/instances/tests/test_clone_job.py b/instances/tests/test_clone_job.py index c3cd21b4..bbcd6eeb 100644 --- a/instances/tests/test_clone_job.py +++ b/instances/tests/test_clone_job.py @@ -621,6 +621,16 @@ class LiveCloneJobTestCase(TransactionTestCase): self.proxy.resize_mem.assert_not_called() self.proxy.start.assert_called_once_with() + def test_a_thaw_that_fails_in_the_recovery_of_a_made_clone_is_a_warning_of_the_clone(self): + # the job panel shows a succeeded job's warning; an error of it was not shown + self.proxy.get_status.return_value = 1 + self.proxy.fs_thaw.side_effect = libvirt_error(libvirt.VIR_ERR_AGENT_UNRESPONSIVE) + self.proxy.backup_result.return_value = None + self.proxy.backup_targets.return_value = {"/other/x.qcow2"} + outcome, _op = self.recovered(defined=True, frozen=True) + self.assertEqual((outcome.state, outcome.error), (Operation.SUCCEEDED, [])) + self.assertIn("virsh domfsthaw", words(outcome.result["warning"])) + def test_recovery_of_a_defined_clone_succeeds(self): self.proxy.backup_result.return_value = None # libvirtd restarted after the clone was defined self.proxy.backup_targets.return_value = {"/other/x.qcow2"} # checked after it diff --git a/instances/tests/test_create_job.py b/instances/tests/test_create_job.py index dbb25923..49ed0926 100644 --- a/instances/tests/test_create_job.py +++ b/instances/tests/test_create_job.py @@ -16,6 +16,8 @@ from instances.models import Instance from instances.tests.create_page import BODY from operations.jobs import Context from operations.models import Operation, acquire +from operations.utils import traceback_of +from vrtManager.util import OperationError class CreateJobTestCase(TransactionTestCase): @@ -100,6 +102,22 @@ class CreateJobTestCase(TransactionTestCase): self.assertEqual(response.status_code, 409) self.assertEqual(Operation.objects.count(), 1) + def test_a_fault_of_the_panel_with_volumes_left_is_not_shown_as_it_is(self): + # its text names nothing users can act on; the volumes to remove are + self.client.post(f"/api/v1/computes/{self.compute.pk}/instances/create/x86_64/q35/", BODY, + content_type="application/json") + op = Operation.objects.get() + acquire("wvc-test:1:a", "wvc-test") + Operation.objects.filter(pk=op.pk).update(state=Operation.RUNNING, worker="wvc-test:1:a") + op.refresh_from_db() + self.conn.create_instance.side_effect = RuntimeError("no such bus") + self.conn.delete_volume.side_effect = libvirt.libvirtError("volume is busy") + with self.assertRaises(OperationError) as raised: + CreateInstance().run(op, Context(op, SimpleNamespace(identity="wvc-test:1:a"))) + self.assertEqual(str(raised.exception), "The panel failed unexpectedly; its log has the details. " + "Remove these volumes by hand: /srv/default/api-vm.qcow2") + self.assertIn("RuntimeError: no such bus", traceback_of(raised.exception)) + @patch("instances.jobs.wvmConnect") def test_a_create_that_left_nothing_failed(self, conn): op = Operation(params={**BODY, "template": ""}, diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index 17c8e066..459b4e79 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 b7029573..4bdf1e10 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-05 15:43+0000\n" +"POT-Creation-Date: 2026-11-06 20:48+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/tr/)\n" @@ -162,13 +162,13 @@ msgid "User Profile" msgstr "Kullanıcı Profili" #: accounts/templates/account.html:17 accounts/templates/profile.html:18 -#: admin/forms.py:73 admin/templates/admin/user_list.html:24 +#: admin/forms.py:74 admin/templates/admin/user_list.html:24 #: admin/templates/admin/user_list.html:30 msgid "Superuser" msgstr "Süperkullanıcı" #: accounts/templates/account.html:19 accounts/templates/profile.html:20 -#: admin/forms.py:73 admin/templates/admin/user_list.html:24 +#: admin/forms.py:74 admin/templates/admin/user_list.html:24 #: admin/templates/admin/user_list.html:32 msgid "Template editor" msgstr "Şablon düzenleyici" @@ -201,7 +201,7 @@ msgid "User sections" msgstr "Kullanıcı bölümleri" #: accounts/templates/account.html:44 accounts/templates/account.html:52 -#: admin/forms.py:185 computes/templates/computes/list.html:30 +#: admin/forms.py:188 computes/templates/computes/list.html:30 #: computes/templates/computes/list.html:57 computes/templates/overview.html:46 msgid "VMs" msgstr "Sanal makineler" @@ -654,11 +654,11 @@ msgstr "OTP QR kodu %(user)s kullanıcısına gönderildi" msgid "User email not set, failed to send QR code" msgstr "Kullanıcı e-postası girilmemiş, QR kod gönderimi başarısız" -#: admin/forms.py:20 +#: admin/forms.py:21 msgid "Change their own password on their profile page" msgstr "Profil sayfasında kendi parolasını değiştirme" -#: admin/forms.py:21 +#: admin/forms.py:22 msgid "" "Clone the VMs they may change and the templates they may view, within their " "quota" @@ -666,21 +666,21 @@ msgstr "" "Kotası dahilinde, değiştirebildiği sanal makineleri ve görebildiği şablonları" " klonlama" -#: admin/forms.py:22 +#: admin/forms.py:23 msgid "Open a console without the VM's console password" msgstr "Sanal makinenin konsol parolası olmadan konsol aç" -#: admin/forms.py:23 +#: admin/forms.py:24 msgid "Take, revert and delete snapshots of the VMs they may change" msgstr "" "Değiştirebildiği sanal makinelerin anlık görüntülerini alma, geri yükleme ve " "silme" -#: admin/forms.py:24 +#: admin/forms.py:25 msgid "See every VM and host in the lists, read-only" msgstr "Listelerde tüm sanal makineleri ve sunucuları salt okunur görme" -#: admin/forms.py:73 admin/templates/admin/logs.html:39 +#: admin/forms.py:74 admin/templates/admin/logs.html:39 #: admin/templates/admin/user_list.html:24 #: admin/templates/admin/user_list.html:34 #: computes/templates/computes/instances.html:46 @@ -692,24 +692,24 @@ msgstr "Listelerde tüm sanal makineleri ve sunucuları salt okunur görme" msgid "User" msgstr "Kullanıcı" -#: admin/forms.py:80 admin/templates/admin/group_form.html:30 +#: admin/forms.py:81 admin/templates/admin/group_form.html:30 msgid "Permissions" msgstr "İzinler" -#: admin/forms.py:87 admin/templates/admin/group_list.html:6 +#: admin/forms.py:88 admin/templates/admin/group_list.html:6 #: admin/templates/admin/group_list.html:8 #: admin/templates/admin/group_list.html:24 #: admin/templates/admin/user_form.html:123 templates/navbar.html:33 msgid "Groups" msgstr "Gruplar" -#: admin/forms.py:139 +#: admin/forms.py:142 #, python-format msgid "NEW_USER_DEFAULT_INSTANCES: %(name)s is not exactly one VM" msgstr "" "NEW_USER_DEFAULT_INSTANCES: %(name)s tam olarak tek bir sanal makine değil" -#: admin/forms.py:186 computes/templates/computes/list.html:58 +#: admin/forms.py:189 computes/templates/computes/list.html:58 #: computes/templates/overview.html:58 instances/forms.py:14 #: instances/templates/allinstances_index_grouped.html:16 #: instances/templates/allinstances_index_nongrouped.html:14 @@ -719,7 +719,7 @@ msgstr "" msgid "vCPUs" msgstr "vCPU" -#: admin/forms.py:187 computes/templates/computes/instances.html:49 +#: admin/forms.py:190 computes/templates/computes/instances.html:49 #: computes/templates/computes/list.html:59 computes/templates/overview.html:66 #: computes/templates/overview.html:150 #: instances/templates/allinstances_index_grouped.html:17 @@ -731,34 +731,34 @@ msgstr "vCPU" msgid "Memory" msgstr "Bellek" -#: admin/forms.py:188 instances/models.py:235 +#: admin/forms.py:191 instances/models.py:235 #: instances/templates/create_instance_w2.html:118 #: instances/templates/instances/settings_tab.html:25 #: instances/templates/instances/stats_tab.html:51 msgid "Disk" msgstr "Disk" -#: admin/forms.py:198 +#: admin/forms.py:201 msgid "No limit" msgstr "Sınır yok" -#: admin/forms.py:214 +#: admin/forms.py:217 msgid "Enter a limit or choose No limit." msgstr "Bir sınır girin ya da Sınır yok'u seçin." -#: admin/forms.py:226 +#: admin/forms.py:229 msgid "The user must change it at their next login" msgstr "Kullanıcı bir sonraki girişinde onu değiştirmek zorundadır" -#: admin/forms.py:232 +#: admin/forms.py:235 msgid "With a password" msgstr "Parolayla" -#: admin/forms.py:232 +#: admin/forms.py:235 msgid "Without a password" msgstr "Parolasız" -#: admin/forms.py:238 +#: admin/forms.py:241 msgid "A user without a password cannot be made to change it" msgstr "Parolası olmayan bir kullanıcıya parola değiştirme zorunluluğu konamaz" @@ -2590,23 +2590,23 @@ msgstr "%%(name)s tarafından devralındı" msgid "Connected. If the VM shows nothing, press Enter." msgstr "Bağlandı. Sanal makine hiçbir şey göstermiyorsa Enter'a basın." -#: console/views.py:43 +#: console/views.py:48 msgid "" "User does not have permission to access console or host/instance not exist" msgstr "" "Kullanıcının konsola erişim izni yok ya da sunucu/sanal makine mevcut değil" -#: console/views.py:69 +#: console/views.py:78 msgid "Fail to get console. Please check the console configuration of your VM." msgstr "" "Konsola erişim başarısız. Lütfen sanal makinenizin konsol yapılandırmasını " "kontrol ediniz." -#: console/views.py:71 +#: console/views.py:80 msgid "The VM is not running: start it to open its console." msgstr "Sanal makine çalışmıyor: konsolunu açmak için başlatın." -#: console/views.py:74 +#: console/views.py:83 msgid "" "This console listens only on its host, which the panel reaches over TCP or " "TLS. Let the console listen on all interfaces in the VM's console settings, " @@ -2617,7 +2617,7 @@ msgstr "" "arabirimlerde dinlemesini sağlayın ya da sunucuyu SSH sunucusu olarak " "ekleyin." -#: console/views.py:80 +#: console/views.py:89 msgid "" "Serial console is disabled on this server. Please switch VM graphics to VNC " "or ask an administrator." @@ -2625,7 +2625,7 @@ msgstr "" "Seri konsol bu sunucuda devre dışı. Lütfen sanal makinenin grafiklerini " "VNC'ye çevirin ya da bir yöneticiye başvurun." -#: console/views.py:85 +#: console/views.py:94 #, python-format msgid "" "Console type '%(type)s' is not supported. Please switch the VM graphics to " @@ -2856,7 +2856,7 @@ msgstr "" msgid "Clone VM" msgstr "Sanal makineyi klonla" -#: instances/jobs.py:562 instances/jobs.py:569 instances/jobs.py:844 +#: instances/jobs.py:562 instances/jobs.py:569 instances/jobs.py:847 msgid "Copying disks" msgstr "Diskler kopyalanıyor" @@ -2873,7 +2873,7 @@ 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:635 instances/jobs.py:719 +#: instances/jobs.py:635 instances/jobs.py:722 msgid "The copy failed" msgstr "Kopyalama başarısız oldu" @@ -2886,7 +2886,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:695 +#: instances/jobs.py:698 #, python-format msgid "" "A volume of the clone was being created; check these volumes, then resolve " @@ -2895,15 +2895,15 @@ msgstr "" "Klonun bir birimi oluşturuluyordu; şu birimleri kontrol edin, sonra bu işi " "çözün: %(volumes)s" -#: instances/jobs.py:699 +#: instances/jobs.py:702 msgid "The job stopped before the copy started" msgstr "İş, kopyalama başlamadan durdu" -#: instances/jobs.py:706 +#: instances/jobs.py:709 msgid "libvirt is still copying the disks" msgstr "libvirt diskleri hâlâ kopyalıyor" -#: instances/jobs.py:709 +#: instances/jobs.py:712 #, python-format msgid "" "Another backup runs on the VM; check these volumes, then resolve this job: " @@ -2912,7 +2912,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:714 +#: instances/jobs.py:717 #, python-format msgid "" "libvirt has no result of the copy; check these volumes, then resolve this " @@ -2921,7 +2921,7 @@ msgstr "" "libvirt'te kopyanın sonucu yok; şu birimleri kontrol edin, sonra bu işi " "çözün: %(volumes)s" -#: instances/jobs.py:726 +#: instances/jobs.py:729 #, python-format msgid "" "The disks were copied, but the clone could not be defined: %(error)s; check " @@ -2930,181 +2930,191 @@ msgstr "" "Diskler kopyalandı, ancak klon tanımlanamadı: %(error)s; şu birimleri kontrol" " edin, sonra bu işi çözün: %(volumes)s" -#: instances/jobs.py:749 instances/templates/create_instance_w1.html:60 +#: instances/jobs.py:752 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:773 instances/jobs.py:818 instances/views.py:2463 +#: instances/jobs.py:776 instances/jobs.py:821 instances/views.py:2463 msgid "A virtual machine with this name already exists" msgstr "Bu isimde bir sanal makine mevcut" -#: instances/jobs.py:775 instances/jobs.py:815 +#: instances/jobs.py:778 instances/jobs.py:818 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:778 +#: instances/jobs.py:781 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:780 +#: instances/jobs.py:783 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:782 +#: instances/jobs.py:785 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:784 +#: instances/jobs.py:787 msgid "Invalid cache mode" msgstr "Geçersiz önbellek modu" -#: instances/jobs.py:786 +#: instances/jobs.py:789 #, 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:788 +#: instances/jobs.py:791 #, python-format msgid "A CD-ROM cannot use the %(bus)s bus" msgstr "CD-ROM %(bus)s veri yolunu kullanamaz" -#: instances/jobs.py:791 +#: instances/jobs.py:794 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:794 +#: instances/jobs.py:797 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:800 +#: instances/jobs.py:803 #, python-format msgid "This host offers no firmware %(firmware)s" msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor" -#: instances/jobs.py:802 vrtManager/instance.py:3011 +#: instances/jobs.py:805 vrtManager/instance.py:3015 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:863 +#: instances/jobs.py:866 msgid "Writing the cloud-init seed" msgstr "cloud-init tohumu yazılıyor" -#: instances/jobs.py:870 +#: instances/jobs.py:873 msgid "Defining the VM" msgstr "Sanal makine tanımlanıyor" -#: instances/jobs.py:903 +#: instances/jobs.py:906 +#, python-format +#| msgid "The panel failed unexpectedly; its log has the details." +msgid "" +"The panel failed unexpectedly; its log has the details. Remove these volumes " +"by hand: %(volumes)s" +msgstr "" +"Panel beklenmedik bir hatayla karşılaştı; ayrıntılar panelin günlüğünde. Bu " +"birimleri elle kaldırın: %(volumes)s" + +#: instances/jobs.py:910 #, python-format msgid "%(error)s. Remove these volumes by hand: %(volumes)s" msgstr "%(error)s. Şu birimleri elle silin: %(volumes)s" -#: instances/jobs.py:928 +#: instances/jobs.py:935 #, python-format msgid "The instance is already on %(compute)s" msgstr "Sanal makine zaten %(compute)s üzerinde" -#: instances/jobs.py:930 +#: instances/jobs.py:937 #, python-format msgid "%(compute)s is not reachable" msgstr "%(compute)s sunucusuna ulaşılamıyor" -#: instances/jobs.py:932 +#: instances/jobs.py:939 #, 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:966 +#: instances/jobs.py:973 #, python-format msgid "" "The VM migrated, but autostart could not be set on the destination: %(error)s" msgstr "" "Sanal makine taşındı, ancak hedefte otomatik başlatma ayarlanamadı: %(error)s" -#: instances/jobs.py:972 +#: instances/jobs.py:979 #, python-format msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s" msgstr "Sanal makine taşındı, ancak NVRAM dosyası kaynakta kaldı: %(error)s" -#: instances/jobs.py:979 +#: instances/jobs.py:986 msgid "Migrate VM" msgstr "Sanal makineyi taşı" -#: instances/jobs.py:1026 +#: instances/jobs.py:1033 msgid "Migrating the VM" msgstr "Sanal makine taşınıyor" -#: instances/jobs.py:1073 +#: instances/jobs.py:1080 msgid "libvirt is still migrating the VM" msgstr "libvirt sanal makineyi hâlâ taşıyor" -#: instances/jobs.py:1084 +#: instances/jobs.py:1091 msgid "The VM stayed on the source host" msgstr "Sanal makine kaynak sunucuda kaldı" -#: instances/jobs.py:1086 +#: instances/jobs.py:1093 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:1134 +#: instances/jobs.py:1141 msgid "Move disk" msgstr "Diski taşı" -#: instances/jobs.py:1141 +#: instances/jobs.py:1148 msgid "Choose a disk of the VM" msgstr "Sanal makinenin bir diskini seçin" -#: instances/jobs.py:1165 +#: instances/jobs.py:1172 msgid "Copying the disk" msgstr "Disk kopyalanıyor" -#: instances/jobs.py:1208 +#: instances/jobs.py:1215 #, 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:1214 +#: instances/jobs.py:1221 #, 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:1216 +#: instances/jobs.py:1223 #, python-format msgid "The definitions of %(dev)s name %(paths)s; check the VM" msgstr "%(dev)s tanımları %(paths)s gösteriyor; sanal makineyi kontrol edin" -#: instances/jobs.py:1223 +#: instances/jobs.py:1230 #, python-format msgid "Disk %(path)s was kept: a disk file that stays needs it" msgstr "%(path)s diski korundu: kalan bir disk dosyası ona ihtiyaç duyuyor" -#: instances/jobs.py:1224 +#: instances/jobs.py:1231 #, python-format msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s" msgstr "Sanal makine kaldırıldı, ancak %(path)s diski silinmedi: %(err)s" -#: instances/jobs.py:1225 +#: instances/jobs.py:1232 #, python-format msgid "Disk %(path)s is used by another VM and was kept" msgstr "" "%(path)s diski başka bir sanal makine tarafından kullanılıyor ve korundu" -#: instances/jobs.py:1279 +#: instances/jobs.py:1286 msgid "Delete VM" msgstr "Sanal makineyi sil" -#: instances/jobs.py:1309 +#: instances/jobs.py:1316 msgid "Deleting the VM" msgstr "Sanal makine siliniyor" -#: instances/jobs.py:1321 +#: instances/jobs.py:1328 msgid "The VM was not deleted; it may have been shut off" msgstr "Sanal makine silinmedi; kapatılmış olabilir" @@ -6258,7 +6268,7 @@ msgstr "Bir ağ arabirimi ve bir QoS yönü seçin" msgid "QoS rates must be whole numbers" msgstr "QoS hızları tam sayı olmalıdır" -#: instances/views.py:2197 networks/views.py:229 +#: instances/views.py:2197 networks/views.py:230 #, python-format msgid "%(qos_dir)s QoS is set" msgstr "%(qos_dir)s QoS ayarlandı" @@ -6271,7 +6281,7 @@ msgstr "" "%(qos_dir)s QoS ayarlandı. Çalışan bir sanal makine, kapatılıp açıldıktan " "sonra bunu alır." -#: instances/views.py:2224 networks/views.py:255 +#: instances/views.py:2224 networks/views.py:256 #, python-format msgid "%(qos_dir)s QoS is deleted" msgstr "%(qos_dir)s QoS silindi" @@ -6460,7 +6470,7 @@ msgstr "" "katılabilir." #: interfaces/templates/create_iface_block.html:30 -#: interfaces/templates/create_iface_block.html:75 interfaces/views.py:31 +#: interfaces/templates/create_iface_block.html:75 interfaces/views.py:32 msgid "Ethernet" msgstr "Ethernet" @@ -6694,19 +6704,19 @@ msgstr "Bağlantı %(link)s" msgid "Link unknown" msgstr "Bağlantı durumu bilinmiyor" -#: interfaces/views.py:30 +#: interfaces/views.py:31 msgid "Bridges" msgstr "Köprüler" -#: interfaces/views.py:30 +#: interfaces/views.py:31 msgid "Bonds" msgstr "Bond'lar" -#: interfaces/views.py:31 +#: interfaces/views.py:32 msgid "VLANs" msgstr "VLAN'lar" -#: interfaces/views.py:45 +#: interfaces/views.py:46 #, python-format msgid "The network devices of this host could not be read: %(error)s" msgstr "Bu sunucunun ağ aygıtları okunamadı: %(error)s" @@ -7260,35 +7270,35 @@ msgstr "SR-IOV" msgid "VMs get a virtual function of the host's network card." msgstr "Sanal makineler sunucunun ağ kartının bir sanal işlevini alır." -#: networks/views.py:166 +#: networks/views.py:167 msgid "The fixed address is unchanged" msgstr "Sabit adres değişmedi" -#: networks/views.py:168 +#: networks/views.py:169 #, python-format msgid "The fixed address %(address)s is set" msgstr "%(address)s sabit adresi ayarlandı" -#: networks/views.py:177 +#: networks/views.py:178 #, python-format msgid "The fixed address %(address)s is deleted" msgstr "%(address)s sabit adresi silindi" -#: networks/views.py:185 +#: networks/views.py:186 msgid "The DHCP range is changed" msgstr "DHCP aralığı değiştirildi" -#: networks/views.py:202 +#: networks/views.py:203 msgid "Network XML is changed. Stop and start network to activate new config." msgstr "" "Ağ XML'i değiştirildi. Yeni yapılandırmayı etkinleştirmek için ağı durdurup " "başlatın." -#: networks/views.py:207 +#: networks/views.py:208 msgid "Network XML is changed." msgstr "Ağ XML i değiştirildi." -#: networks/views.py:222 +#: networks/views.py:223 #, python-format msgid "" "%(qos_dir)s QoS is updated. Network XML is changed. Stop and start network to" @@ -7297,7 +7307,7 @@ msgstr "" "%(qos_dir)s QoS is updated.Ağ XML i değiştirildi. \\ Aktifleştirmek için " "durdurun ve yeniden başlatın." -#: networks/views.py:247 +#: networks/views.py:248 #, python-format msgid "" "%(qos_dir)s QoS is deleted. Network XML is changed. Stop and start network to" @@ -7824,11 +7834,11 @@ msgstr "İptal edilemedi (HTTP %(status)s)." msgid "Cancel failed: the server could not be reached." msgstr "İptal edilemedi: sunucuya ulaşılamadı." -#: operations/utils.py:54 +#: operations/utils.py:55 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:55 +#: operations/utils.py:56 msgid "" "The host of this virtual machine is busy with other requests right now: try " "again in a moment." @@ -7836,20 +7846,25 @@ msgstr "" "Bu sanal makinenin sunucusu şu anda başka isteklerle meşgul: biraz sonra " "yeniden deneyin." -#: operations/utils.py:57 +#: operations/utils.py:58 msgid "You may no longer do this." msgstr "Bunu artık yapamazsınız." -#: operations/utils.py:58 +#: operations/utils.py:59 msgid "The virtual machine is no longer in the panel." msgstr "Sanal makine artık panelde yok." -#: operations/utils.py:169 +#: operations/utils.py:127 +msgid "The panel failed unexpectedly; its log has the details." +msgstr "" +"Panel beklenmedik bir hatayla karşılaştı; ayrıntılar panelin günlüğünde." + +#: operations/utils.py:191 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:172 +#: operations/utils.py:194 #, python-format msgid "" "%(label)s is %(state)s on this resource (started by %(user)s, %(since)s ago);" @@ -7858,12 +7873,12 @@ msgstr "" "%(label)s bu kaynakta %(state)s (%(user)s başlattı, %(since)s önce); İşler " "sayfasına bakın." -#: operations/utils.py:174 +#: operations/utils.py:196 #, python-format msgid "%(label)s is %(state)s on this resource (since %(since)s)." msgstr "%(label)s bu kaynakta %(state)s (%(since)s beri)." -#: operations/worker.py:42 +#: operations/worker.py:43 msgid "" "libvirt's merge finished copying but never completed (for example after a " "libvirtd restart); check the VM before resolving" @@ -7872,7 +7887,7 @@ msgstr "" "libvirtd yeniden başlatıldıktan sonra); çözmeden önce sanal makineyi kontrol " "edin" -#: operations/worker.py:44 +#: operations/worker.py:45 msgid "" "The worker that ran this job stopped responding; an administrator must make " "sure it is no longer running, then resolve this job" @@ -7880,11 +7895,11 @@ msgstr "" "Bu işi çalıştıran worker yanıt vermeyi bıraktı; bir yönetici artık " "çalışmadığından emin olmalı, sonra bu işi çözmeli" -#: operations/worker.py:46 +#: operations/worker.py:47 msgid "Waiting for recovery" msgstr "Kurtarma bekleniyor" -#: operations/worker.py:48 +#: operations/worker.py:49 msgid "" "Recovery could not read the job's state for an hour; check the VM, then " "resolve this job" @@ -7892,7 +7907,7 @@ msgstr "" "Kurtarma bir saat boyunca işin durumunu okuyamadı; sanal makineyi kontrol " "edin, sonra bu işi çözün" -#: operations/worker.py:473 +#: operations/worker.py:474 msgid "The worker stopped before the job changed anything" msgstr "Worker, iş herhangi bir şeyi değiştirmeden önce durdu" @@ -8446,12 +8461,12 @@ msgstr "Yükleme oturumu meşgul. Lütfen yeniden deneyin." msgid "ISO image already exists" msgstr "ISO imajı zaten mevcut" -#: storages/views.py:178 +#: storages/views.py:179 #, python-format msgid "Volume: %(vol)s is deleted." msgstr "Birim: %(vol)s silindi." -#: storages/views.py:196 +#: storages/views.py:197 #, python-format msgid "Image file %(name)s is created successfully" msgstr "İmaj dosyası %(name)s başarıyla oluşturuldu" @@ -9240,64 +9255,64 @@ msgstr "" 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:2403 +#: vrtManager/instance.py:2407 msgid "Cannot clone a VM whose NVRAM is not a file" msgstr "NVRAM'i dosya olmayan bir sanal makine klonlanamaz" -#: vrtManager/instance.py:2471 +#: vrtManager/instance.py:2475 #, 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:2481 +#: vrtManager/instance.py:2485 msgid "Shut the VM down before cloning it" msgstr "Klonlamadan önce sanal makineyi kapatın" -#: vrtManager/instance.py:2535 +#: vrtManager/instance.py:2539 #, 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:2546 +#: vrtManager/instance.py:2550 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:2549 +#: vrtManager/instance.py:2553 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:2552 +#: vrtManager/instance.py:2556 #, 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:2554 +#: vrtManager/instance.py:2558 #, 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:2559 +#: vrtManager/instance.py:2563 #, 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:2561 +#: vrtManager/instance.py:2565 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:2574 +#: vrtManager/instance.py:2578 #, 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:2593 +#: vrtManager/instance.py:2597 #, python-format msgid "" "Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f GiB" @@ -9307,20 +9322,20 @@ msgstr "" "gerektiriyor (kopyalanan diskler, artı bunların %%%(percent)g kadarı, en az " "%(minimum)g GiB)" -#: vrtManager/instance.py:2732 +#: vrtManager/instance.py:2736 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:2748 +#: vrtManager/instance.py:2752 #, python-format msgid "No network interface with MAC %(old_mac)s" msgstr "MAC adresi %(old_mac)s olan ağ arabirimi yok" -#: vrtManager/instance.py:2751 +#: vrtManager/instance.py:2755 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:2842 +#: vrtManager/instance.py:2846 msgid "" "A VM changes its PCI devices while it runs, or is shut off without a saved " "state" @@ -9328,14 +9343,14 @@ msgstr "" "Sanal makine PCI aygıtlarını çalışırken veya kaydedilmiş durumu olmadan " "kapalıyken değiştirir" -#: vrtManager/instance.py:2854 +#: vrtManager/instance.py:2858 #, 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:2856 +#: vrtManager/instance.py:2860 #, python-format msgid "" "%(err)s; PCI device %(address)s: the VM is not running, its next start has it" @@ -9343,7 +9358,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: sanal makine çalışmıyor, sonraki başlatmada " "var" -#: vrtManager/instance.py:2858 +#: vrtManager/instance.py:2862 #, python-format msgid "" "%(err)s; PCI device %(address)s: the VM is not running, its next start does " @@ -9352,7 +9367,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: sanal makine çalışmıyor, sonraki başlatmada " "yok" -#: vrtManager/instance.py:2859 +#: vrtManager/instance.py:2863 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM has it, its next start has it" @@ -9360,7 +9375,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede var, sonraki " "başlatmada var" -#: vrtManager/instance.py:2861 +#: vrtManager/instance.py:2865 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM has it, its next start does " @@ -9369,7 +9384,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede var, sonraki " "başlatmada yok" -#: vrtManager/instance.py:2863 +#: vrtManager/instance.py:2867 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM does not have it, its next " @@ -9378,7 +9393,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede yok, sonraki " "başlatmada var" -#: vrtManager/instance.py:2865 +#: vrtManager/instance.py:2869 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM does not have it, its next " @@ -9387,29 +9402,29 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede yok, sonraki " "başlatmada yok" -#: vrtManager/instance.py:2899 +#: vrtManager/instance.py:2903 #, python-format msgid "The VM has no PCI device %(address)s" msgstr "Sanal makinede %(address)s PCI aygıtı yok" -#: vrtManager/instance.py:2917 +#: vrtManager/instance.py:2921 msgid "The VM has no USB controller" msgstr "Sanal makinede USB denetleyicisi yok" -#: vrtManager/instance.py:2936 +#: vrtManager/instance.py:2940 msgid "The VM has no such USB device" msgstr "Sanal makinede böyle bir USB aygıtı yok" -#: vrtManager/instance.py:2977 +#: vrtManager/instance.py:2981 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:3004 +#: vrtManager/instance.py:3008 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:3023 +#: vrtManager/instance.py:3027 #, python-format msgid "Unknown crash action %(action)s" msgstr "Bilinmeyen çökme eylemi %(action)s" diff --git a/locale/zh/LC_MESSAGES/django.mo b/locale/zh/LC_MESSAGES/django.mo index 710c2baa..17aeed85 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 ab1a8108..07d063bc 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-05 15:43+0000\n" +"POT-Creation-Date: 2026-11-06 20:48+0000\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -159,13 +159,13 @@ msgid "User Profile" msgstr "用户资料" #: accounts/templates/account.html:17 accounts/templates/profile.html:18 -#: admin/forms.py:73 admin/templates/admin/user_list.html:24 +#: admin/forms.py:74 admin/templates/admin/user_list.html:24 #: admin/templates/admin/user_list.html:30 msgid "Superuser" msgstr "超级用户" #: accounts/templates/account.html:19 accounts/templates/profile.html:20 -#: admin/forms.py:73 admin/templates/admin/user_list.html:24 +#: admin/forms.py:74 admin/templates/admin/user_list.html:24 #: admin/templates/admin/user_list.html:32 msgid "Template editor" msgstr "模板编辑者" @@ -198,7 +198,7 @@ msgid "User sections" msgstr "用户页面导航" #: accounts/templates/account.html:44 accounts/templates/account.html:52 -#: admin/forms.py:185 computes/templates/computes/list.html:30 +#: admin/forms.py:188 computes/templates/computes/list.html:30 #: computes/templates/computes/list.html:57 computes/templates/overview.html:46 msgid "VMs" msgstr "虚拟机" @@ -641,29 +641,29 @@ msgstr "OTP 二维码已通过邮件发送给用户 %(user)s" msgid "User email not set, failed to send QR code" msgstr "未设置用户邮箱,无法发送二维码" -#: admin/forms.py:20 +#: admin/forms.py:21 msgid "Change their own password on their profile page" msgstr "在个人资料页修改自己的密码" -#: admin/forms.py:21 +#: admin/forms.py:22 msgid "" "Clone the VMs they may change and the templates they may view, within their " "quota" msgstr "在配额范围内克隆其可修改的虚拟机和可查看的模板" -#: admin/forms.py:22 +#: admin/forms.py:23 msgid "Open a console without the VM's console password" msgstr "无需虚拟机的控制台密码即可打开控制台" -#: admin/forms.py:23 +#: admin/forms.py:24 msgid "Take, revert and delete snapshots of the VMs they may change" msgstr "为其可修改的虚拟机创建、恢复和删除快照" -#: admin/forms.py:24 +#: admin/forms.py:25 msgid "See every VM and host in the lists, read-only" msgstr "在列表中查看所有虚拟机和主机(只读)" -#: admin/forms.py:73 admin/templates/admin/logs.html:39 +#: admin/forms.py:74 admin/templates/admin/logs.html:39 #: admin/templates/admin/user_list.html:24 #: admin/templates/admin/user_list.html:34 #: computes/templates/computes/instances.html:46 @@ -675,23 +675,23 @@ msgstr "在列表中查看所有虚拟机和主机(只读)" msgid "User" msgstr "用户" -#: admin/forms.py:80 admin/templates/admin/group_form.html:30 +#: admin/forms.py:81 admin/templates/admin/group_form.html:30 msgid "Permissions" msgstr "权限" -#: admin/forms.py:87 admin/templates/admin/group_list.html:6 +#: admin/forms.py:88 admin/templates/admin/group_list.html:6 #: admin/templates/admin/group_list.html:8 #: admin/templates/admin/group_list.html:24 #: admin/templates/admin/user_form.html:123 templates/navbar.html:33 msgid "Groups" msgstr "用户组" -#: admin/forms.py:139 +#: admin/forms.py:142 #, python-format msgid "NEW_USER_DEFAULT_INSTANCES: %(name)s is not exactly one VM" msgstr "NEW_USER_DEFAULT_INSTANCES:%(name)s 不是唯一的一台虚拟机" -#: admin/forms.py:186 computes/templates/computes/list.html:58 +#: admin/forms.py:189 computes/templates/computes/list.html:58 #: computes/templates/overview.html:58 instances/forms.py:14 #: instances/templates/allinstances_index_grouped.html:16 #: instances/templates/allinstances_index_nongrouped.html:14 @@ -701,7 +701,7 @@ msgstr "NEW_USER_DEFAULT_INSTANCES:%(name)s 不是唯一的一台虚拟机" msgid "vCPUs" msgstr "vCPU" -#: admin/forms.py:187 computes/templates/computes/instances.html:49 +#: admin/forms.py:190 computes/templates/computes/instances.html:49 #: computes/templates/computes/list.html:59 computes/templates/overview.html:66 #: computes/templates/overview.html:150 #: instances/templates/allinstances_index_grouped.html:17 @@ -713,34 +713,34 @@ msgstr "vCPU" msgid "Memory" msgstr "内存" -#: admin/forms.py:188 instances/models.py:235 +#: admin/forms.py:191 instances/models.py:235 #: instances/templates/create_instance_w2.html:118 #: instances/templates/instances/settings_tab.html:25 #: instances/templates/instances/stats_tab.html:51 msgid "Disk" msgstr "磁盘" -#: admin/forms.py:198 +#: admin/forms.py:201 msgid "No limit" msgstr "不限" -#: admin/forms.py:214 +#: admin/forms.py:217 msgid "Enter a limit or choose No limit." msgstr "请输入限制值,或选择“不限”。" -#: admin/forms.py:226 +#: admin/forms.py:229 msgid "The user must change it at their next login" msgstr "用户必须在下次登录时修改密码" -#: admin/forms.py:232 +#: admin/forms.py:235 msgid "With a password" msgstr "使用密码" -#: admin/forms.py:232 +#: admin/forms.py:235 msgid "Without a password" msgstr "不使用密码" -#: admin/forms.py:238 +#: admin/forms.py:241 msgid "A user without a password cannot be made to change it" msgstr "没有密码的用户无法被要求修改密码" @@ -2507,20 +2507,20 @@ msgstr "已被 %%(name)s 接管" msgid "Connected. If the VM shows nothing, press Enter." msgstr "已连接。如果虚拟机没有显示任何内容,请按 Enter。" -#: console/views.py:43 +#: console/views.py:48 msgid "" "User does not have permission to access console or host/instance not exist" msgstr "用户无权访问控制台,或主机/虚拟机不存在" -#: console/views.py:69 +#: console/views.py:78 msgid "Fail to get console. Please check the console configuration of your VM." msgstr "无法获取控制台。请检查虚拟机的控制台配置。" -#: console/views.py:71 +#: console/views.py:80 msgid "The VM is not running: start it to open its console." msgstr "虚拟机未运行:请启动它以打开控制台。" -#: console/views.py:74 +#: console/views.py:83 msgid "" "This console listens only on its host, which the panel reaches over TCP or " "TLS. Let the console listen on all interfaces in the VM's console settings, " @@ -2528,13 +2528,13 @@ msgid "" msgstr "" "此控制台只在其主机上监听,而面板通过 TCP 或 TLS 连接该主机。请在虚拟机的控制台设置中让控制台监听所有接口,或将该主机添加为 SSH 主机。" -#: console/views.py:80 +#: console/views.py:89 msgid "" "Serial console is disabled on this server. Please switch VM graphics to VNC " "or ask an administrator." msgstr "此服务器已禁用串行控制台。请将虚拟机图形切换为 VNC,或联系管理员。" -#: console/views.py:85 +#: console/views.py:94 #, python-format msgid "" "Console type '%(type)s' is not supported. Please switch the VM graphics to " @@ -2751,7 +2751,7 @@ msgstr "无法解冻客户机的文件系统(%(error)s);请用以下命令 msgid "Clone VM" msgstr "克隆虚拟机" -#: instances/jobs.py:562 instances/jobs.py:569 instances/jobs.py:844 +#: instances/jobs.py:562 instances/jobs.py:569 instances/jobs.py:847 msgid "Copying disks" msgstr "正在复制磁盘" @@ -2766,7 +2766,7 @@ msgid "" "(%(thaw)s); thaw them with: virsh domfsthaw %(vm)s" msgstr "复制失败:%(error)s。无法解冻客户机的文件系统(%(thaw)s);请用以下命令解冻:virsh domfsthaw %(vm)s" -#: instances/jobs.py:635 instances/jobs.py:719 +#: instances/jobs.py:635 instances/jobs.py:722 msgid "The copy failed" msgstr "复制失败" @@ -2777,209 +2777,217 @@ msgid "" "resolve this job: %(volumes)s" msgstr "虚拟机已不在运行该任务的计算节点上;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:695 +#: instances/jobs.py:698 #, 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:699 +#: instances/jobs.py:702 msgid "The job stopped before the copy started" msgstr "任务在复制开始前停止" -#: instances/jobs.py:706 +#: instances/jobs.py:709 msgid "libvirt is still copying the disks" msgstr "libvirt 仍在复制磁盘" -#: instances/jobs.py:709 +#: instances/jobs.py:712 #, python-format msgid "" "Another backup runs on the VM; check these volumes, then resolve this job: " "%(volumes)s" msgstr "虚拟机上正在运行另一个备份;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:714 +#: instances/jobs.py:717 #, 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:726 +#: instances/jobs.py:729 #, python-format msgid "" "The disks were copied, but the clone could not be defined: %(error)s; check " "these volumes, then resolve this job: %(volumes)s" msgstr "磁盘已复制,但无法定义克隆:%(error)s;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:749 instances/templates/create_instance_w1.html:60 +#: instances/jobs.py:752 instances/templates/create_instance_w1.html:60 #: instances/templates/create_instance_w2.html:356 msgid "Create VM" msgstr "创建虚拟机" -#: instances/jobs.py:773 instances/jobs.py:818 instances/views.py:2463 +#: instances/jobs.py:776 instances/jobs.py:821 instances/views.py:2463 msgid "A virtual machine with this name already exists" msgstr "同名虚拟机已存在" -#: instances/jobs.py:775 instances/jobs.py:815 +#: instances/jobs.py:778 instances/jobs.py:818 msgid "There is an instance with same name. Remove it and try again!" msgstr "已存在同名虚拟机。请删除后重试!" -#: instances/jobs.py:778 +#: instances/jobs.py:781 msgid "First you need to create or select an image" msgstr "请先创建或选择一个镜像" -#: instances/jobs.py:780 +#: instances/jobs.py:783 msgid "Choose one disk source: a new disk, existing volumes or a template" msgstr "请选择一种磁盘来源:新磁盘、现有卷或模板" -#: instances/jobs.py:782 +#: instances/jobs.py:785 msgid "Image has already exist. Please check volumes or change instance name" msgstr "镜像已存在。请检查卷或更改虚拟机名称" -#: instances/jobs.py:784 +#: instances/jobs.py:787 msgid "Invalid cache mode" msgstr "无效的缓存模式" -#: instances/jobs.py:786 +#: instances/jobs.py:789 #, python-format msgid "This host offers no CPU mode or model %(mode)s" msgstr "此主机不提供 CPU 模式或型号 %(mode)s" -#: instances/jobs.py:788 +#: instances/jobs.py:791 #, python-format msgid "A CD-ROM cannot use the %(bus)s bus" msgstr "CD-ROM 不能使用 %(bus)s 总线" -#: instances/jobs.py:791 +#: instances/jobs.py:794 msgid "The cloud-init seed is read from a CD-ROM: choose its bus" msgstr "cloud-init 种子从 CD-ROM 读取:请选择其总线" -#: instances/jobs.py:794 +#: instances/jobs.py:797 msgid "The cloud-init seed needs a directory storage pool for the disk" msgstr "cloud-init 种子需要为磁盘使用目录类型的存储池" -#: instances/jobs.py:800 +#: instances/jobs.py:803 #, python-format msgid "This host offers no firmware %(firmware)s" msgstr "此主机不提供固件 %(firmware)s" -#: instances/jobs.py:802 vrtManager/instance.py:3011 +#: instances/jobs.py:805 vrtManager/instance.py:3015 msgid "This host offers no TPM 2.0: install swtpm on it" msgstr "此主机不提供 TPM 2.0:请在主机上安装 swtpm" -#: instances/jobs.py:863 +#: instances/jobs.py:866 msgid "Writing the cloud-init seed" msgstr "正在写入 cloud-init 种子" -#: instances/jobs.py:870 +#: instances/jobs.py:873 msgid "Defining the VM" msgstr "正在定义虚拟机" -#: instances/jobs.py:903 +#: instances/jobs.py:906 +#, python-format +#| msgid "The panel failed unexpectedly; its log has the details." +msgid "" +"The panel failed unexpectedly; its log has the details. Remove these volumes " +"by hand: %(volumes)s" +msgstr "面板发生意外错误;详细信息见面板日志。请手动删除这些卷:%(volumes)s" + +#: instances/jobs.py:910 #, python-format msgid "%(error)s. Remove these volumes by hand: %(volumes)s" msgstr "%(error)s。请手动删除这些卷:%(volumes)s" -#: instances/jobs.py:928 +#: instances/jobs.py:935 #, python-format msgid "The instance is already on %(compute)s" msgstr "虚拟机已在 %(compute)s 上" -#: instances/jobs.py:930 +#: instances/jobs.py:937 #, python-format msgid "%(compute)s is not reachable" msgstr "无法连接 %(compute)s" -#: instances/jobs.py:932 +#: instances/jobs.py:939 #, python-format msgid "%(compute)s is the VM's own host under another name" msgstr "%(compute)s 是该虚拟机所在主机的另一个名称" -#: instances/jobs.py:966 +#: instances/jobs.py:973 #, python-format msgid "" "The VM migrated, but autostart could not be set on the destination: %(error)s" msgstr "虚拟机已迁移,但无法在目标主机上设置自动启动:%(error)s" -#: instances/jobs.py:972 +#: instances/jobs.py:979 #, python-format msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s" msgstr "虚拟机已迁移,但其 NVRAM 文件仍留在源主机上:%(error)s" -#: instances/jobs.py:979 +#: instances/jobs.py:986 msgid "Migrate VM" msgstr "迁移虚拟机" -#: instances/jobs.py:1026 +#: instances/jobs.py:1033 msgid "Migrating the VM" msgstr "正在迁移虚拟机" -#: instances/jobs.py:1073 +#: instances/jobs.py:1080 msgid "libvirt is still migrating the VM" msgstr "libvirt 仍在迁移虚拟机" -#: instances/jobs.py:1084 +#: instances/jobs.py:1091 msgid "The VM stayed on the source host" msgstr "虚拟机留在了源主机上" -#: instances/jobs.py:1086 +#: instances/jobs.py:1093 msgid "" "The VM is on both computes or on neither; check them, then resolve this job." msgstr "虚拟机同时位于两个计算节点上,或都不在;请检查后处理此任务。" -#: instances/jobs.py:1134 +#: instances/jobs.py:1141 msgid "Move disk" msgstr "移动磁盘" -#: instances/jobs.py:1141 +#: instances/jobs.py:1148 msgid "Choose a disk of the VM" msgstr "请选择虚拟机的一个磁盘" -#: instances/jobs.py:1165 +#: instances/jobs.py:1172 msgid "Copying the disk" msgstr "正在复制磁盘" -#: instances/jobs.py:1208 +#: instances/jobs.py:1215 #, python-format msgid "Another block job runs on %(dev)s; check the VM" msgstr "%(dev)s 上正在运行另一个块任务;请检查虚拟机" -#: instances/jobs.py:1214 +#: instances/jobs.py:1221 #, python-format msgid "The disk stayed in %(path)s; remove %(dest)s if it is there" msgstr "磁盘仍在 %(path)s;如果 %(dest)s 存在,请将其删除" -#: instances/jobs.py:1216 +#: instances/jobs.py:1223 #, python-format msgid "The definitions of %(dev)s name %(paths)s; check the VM" msgstr "%(dev)s 的定义指向 %(paths)s;请检查虚拟机" -#: instances/jobs.py:1223 +#: instances/jobs.py:1230 #, python-format msgid "Disk %(path)s was kept: a disk file that stays needs it" msgstr "磁盘 %(path)s 已保留:某个保留的磁盘文件依赖它" -#: instances/jobs.py:1224 +#: instances/jobs.py:1231 #, python-format msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s" msgstr "虚拟机已删除,但磁盘 %(path)s 未被删除:%(err)s" -#: instances/jobs.py:1225 +#: instances/jobs.py:1232 #, python-format msgid "Disk %(path)s is used by another VM and was kept" msgstr "磁盘 %(path)s 正被另一台虚拟机使用,已保留" -#: instances/jobs.py:1279 +#: instances/jobs.py:1286 msgid "Delete VM" msgstr "删除虚拟机" -#: instances/jobs.py:1309 +#: instances/jobs.py:1316 msgid "Deleting the VM" msgstr "正在删除虚拟机" -#: instances/jobs.py:1321 +#: instances/jobs.py:1328 msgid "The VM was not deleted; it may have been shut off" msgstr "虚拟机未被删除;它可能已被关闭" @@ -5931,7 +5939,7 @@ msgstr "请选择网络接口和 QoS 方向" msgid "QoS rates must be whole numbers" msgstr "QoS 速率必须为整数" -#: instances/views.py:2197 networks/views.py:229 +#: instances/views.py:2197 networks/views.py:230 #, python-format msgid "%(qos_dir)s QoS is set" msgstr "%(qos_dir)s QoS 已设置" @@ -5942,7 +5950,7 @@ 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:2224 networks/views.py:255 +#: instances/views.py:2224 networks/views.py:256 #, python-format msgid "%(qos_dir)s QoS is deleted" msgstr "%(qos_dir)s QoS 已删除" @@ -6119,7 +6127,7 @@ msgid "A new bridge over a device of the host; VMs can join it." msgstr "在主机设备上新建网桥;虚拟机可以加入。" #: interfaces/templates/create_iface_block.html:30 -#: interfaces/templates/create_iface_block.html:75 interfaces/views.py:31 +#: interfaces/templates/create_iface_block.html:75 interfaces/views.py:32 msgid "Ethernet" msgstr "以太网" @@ -6343,19 +6351,19 @@ msgstr "链路 %(link)s" msgid "Link unknown" msgstr "链路未知" -#: interfaces/views.py:30 +#: interfaces/views.py:31 msgid "Bridges" msgstr "网桥" -#: interfaces/views.py:30 +#: interfaces/views.py:31 msgid "Bonds" msgstr "绑定" -#: interfaces/views.py:31 +#: interfaces/views.py:32 msgid "VLANs" msgstr "VLAN" -#: interfaces/views.py:45 +#: interfaces/views.py:46 #, python-format msgid "The network devices of this host could not be read: %(error)s" msgstr "无法读取此主机的网络设备:%(error)s" @@ -6880,40 +6888,40 @@ msgstr "SR-IOV" msgid "VMs get a virtual function of the host's network card." msgstr "虚拟机获得主机网卡的一个虚拟功能。" -#: networks/views.py:166 +#: networks/views.py:167 msgid "The fixed address is unchanged" msgstr "固定地址未更改" -#: networks/views.py:168 +#: networks/views.py:169 #, python-format msgid "The fixed address %(address)s is set" msgstr "已设置固定地址 %(address)s" -#: networks/views.py:177 +#: networks/views.py:178 #, python-format msgid "The fixed address %(address)s is deleted" msgstr "已删除固定地址 %(address)s" -#: networks/views.py:185 +#: networks/views.py:186 msgid "The DHCP range is changed" msgstr "DHCP 范围已更改" -#: networks/views.py:202 +#: networks/views.py:203 msgid "Network XML is changed. Stop and start network to activate new config." msgstr "网络 XML 已更改。请停止并启动网络以应用新配置。" -#: networks/views.py:207 +#: networks/views.py:208 msgid "Network XML is changed." msgstr "网络 XML 已更改。" -#: networks/views.py:222 +#: networks/views.py:223 #, python-format msgid "" "%(qos_dir)s QoS is updated. Network XML is changed. Stop and start network to" " activate new config" msgstr "%(qos_dir)s QoS 已更新。网络 XML 已更改。请停止并启动网络以应用新配置" -#: networks/views.py:247 +#: networks/views.py:248 #, python-format msgid "" "%(qos_dir)s QoS is deleted. Network XML is changed. Stop and start network to" @@ -7421,64 +7429,68 @@ msgstr "取消失败(HTTP %(status)s)。" msgid "Cancel failed: the server could not be reached." msgstr "取消失败:无法连接到服务器。" -#: operations/utils.py:54 +#: operations/utils.py:55 msgid "The host of this virtual machine cannot be reached right now." msgstr "当前无法连接此虚拟机所在的主机。" -#: operations/utils.py:55 +#: operations/utils.py:56 msgid "" "The host of this virtual machine is busy with other requests right now: try " "again in a moment." msgstr "此虚拟机所在的主机正忙于处理其他请求:请稍后重试。" -#: operations/utils.py:57 +#: operations/utils.py:58 msgid "You may no longer do this." msgstr "您已无权执行此操作。" -#: operations/utils.py:58 +#: operations/utils.py:59 msgid "The virtual machine is no longer in the panel." msgstr "该虚拟机已不在面板中。" -#: operations/utils.py:169 +#: operations/utils.py:127 +msgid "The panel failed unexpectedly; its log has the details." +msgstr "面板发生意外错误;详细信息见面板日志。" + +#: operations/utils.py:191 msgid "" "A short operation is in progress on this resource; try again in a moment." msgstr "此资源上正在进行一项短时操作,请稍后重试。" -#: operations/utils.py:172 +#: operations/utils.py:194 #, python-format msgid "" "%(label)s is %(state)s on this resource (started by %(user)s, %(since)s ago);" " see the Jobs page." msgstr "此资源上的%(label)s%(state)s(由 %(user)s 于 %(since)s前发起),请查看任务页面。" -#: operations/utils.py:174 +#: operations/utils.py:196 #, python-format msgid "%(label)s is %(state)s on this resource (since %(since)s)." msgstr "此资源上的%(label)s%(state)s(自 %(since)s起)。" -#: operations/worker.py:42 +#: operations/worker.py:43 msgid "" "libvirt's merge finished copying but never completed (for example after a " "libvirtd restart); check the VM before resolving" msgstr "libvirt 的合并已完成复制但从未结束(例如 libvirtd 重启后);处理前请检查虚拟机" -#: operations/worker.py:44 +#: operations/worker.py:45 msgid "" "The worker that ran this job stopped responding; an administrator must make " "sure it is no longer running, then resolve this job" msgstr "运行此任务的工作进程已停止响应;管理员必须确认它已不再运行,然后处理此任务" -#: operations/worker.py:46 +#: operations/worker.py:47 msgid "Waiting for recovery" msgstr "等待恢复" -#: operations/worker.py:48 +#: operations/worker.py:49 msgid "" "Recovery could not read the job's state for an hour; check the VM, then " "resolve this job" msgstr "恢复在一小时内无法读取任务状态;请检查虚拟机,然后处理此任务" -#: operations/worker.py:473 +#: operations/worker.py:474 msgid "The worker stopped before the job changed anything" msgstr "工作进程在任务做出任何更改之前停止" @@ -8016,12 +8028,12 @@ msgstr "上传会话繁忙,请重试。" msgid "ISO image already exists" msgstr "ISO 镜像已存在" -#: storages/views.py:178 +#: storages/views.py:179 #, python-format msgid "Volume: %(vol)s is deleted." msgstr "卷 %(vol)s 已删除。" -#: storages/views.py:196 +#: storages/views.py:197 #, python-format msgid "Image file %(name)s is created successfully" msgstr "镜像文件 %(name)s 创建成功" @@ -8736,57 +8748,57 @@ msgstr "此虚拟机上仍有快照任务在运行;请在其结束后重试" msgid "Shut the VM down to revert to an internal snapshot" msgstr "请关闭虚拟机后再还原到内部快照" -#: vrtManager/instance.py:2403 +#: vrtManager/instance.py:2407 msgid "Cannot clone a VM whose NVRAM is not a file" msgstr "NVRAM 不是文件的虚拟机无法克隆" -#: vrtManager/instance.py:2471 +#: vrtManager/instance.py:2475 #, python-format msgid "Clone failed (%(err)s); remove these copies by hand: %(path)s" msgstr "克隆失败(%(err)s);请手动删除这些副本:%(path)s" -#: vrtManager/instance.py:2481 +#: vrtManager/instance.py:2485 msgid "Shut the VM down before cloning it" msgstr "请先关闭虚拟机再克隆" -#: vrtManager/instance.py:2535 +#: vrtManager/instance.py:2539 #, python-format msgid "Clone failed (%(err)s); remove these copies by hand: %(leftovers)s" msgstr "克隆失败(%(err)s);请手动删除这些副本:%(leftovers)s" -#: vrtManager/instance.py:2546 +#: vrtManager/instance.py:2550 msgid "Live clone needs libvirt 11.10 or later; shut the VM down to clone it" msgstr "在线克隆需要 libvirt 11.10 或更高版本;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2549 +#: vrtManager/instance.py:2553 msgid "A VM with a TPM is cloned only shut down" msgstr "带 TPM 的虚拟机只能在关机时克隆" -#: vrtManager/instance.py:2552 +#: vrtManager/instance.py:2556 #, python-format msgid "Disk %(dev)s is not a local file; shut the VM down to clone it" msgstr "磁盘 %(dev)s 不是本地文件;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2554 +#: vrtManager/instance.py:2558 #, python-format msgid "Disk %(dev)s is shareable; shut the VM down to clone it" msgstr "磁盘 %(dev)s 是可共享的;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2559 +#: vrtManager/instance.py:2563 #, python-format msgid "Disk %(dev)s changes at the next start; shut the VM down to clone it" msgstr "磁盘 %(dev)s 在下次启动时会改变;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2561 +#: vrtManager/instance.py:2565 msgid "Another job is running on the VM; clone it when that ends" msgstr "虚拟机上有另一个任务正在运行;请在其结束后再克隆" -#: vrtManager/instance.py:2574 +#: vrtManager/instance.py:2578 #, python-format msgid "Volume %(name)s already exists in storage pool %(pool)s" msgstr "卷 %(name)s 已存在于存储池 %(pool)s 中" -#: vrtManager/instance.py:2593 +#: vrtManager/instance.py:2597 #, python-format msgid "" "Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f GiB" @@ -8795,92 +8807,92 @@ msgstr "" "存储池 %(pool)s 可用 %(free).1f GiB;克隆需要 %(size).1f GiB(复制的磁盘,再加其 %(percent)g%%,至少" " %(minimum)g GiB)" -#: vrtManager/instance.py:2732 +#: vrtManager/instance.py:2736 msgid "A network interface that is a host device is removed in the XML editor" msgstr "作为主机设备的网络接口需在 XML 编辑器中删除" -#: vrtManager/instance.py:2748 +#: vrtManager/instance.py:2752 #, python-format msgid "No network interface with MAC %(old_mac)s" msgstr "没有 MAC 为 %(old_mac)s 的网络接口" -#: vrtManager/instance.py:2751 +#: vrtManager/instance.py:2755 msgid "A network interface that is a host device is changed in the XML editor" msgstr "作为主机设备的网络接口需在 XML 编辑器中修改" -#: vrtManager/instance.py:2842 +#: vrtManager/instance.py:2846 msgid "" "A VM changes its PCI devices while it runs, or is shut off without a saved " "state" msgstr "虚拟机只能在运行时或无保存的状态的关机时更改其 PCI 设备" -#: vrtManager/instance.py:2854 +#: vrtManager/instance.py:2858 #, 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:2856 +#: vrtManager/instance.py:2860 #, 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:2858 +#: vrtManager/instance.py:2862 #, 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:2859 +#: vrtManager/instance.py:2863 #, 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:2861 +#: vrtManager/instance.py:2865 #, 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:2863 +#: vrtManager/instance.py:2867 #, 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:2865 +#: vrtManager/instance.py:2869 #, 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:2899 +#: vrtManager/instance.py:2903 #, python-format msgid "The VM has no PCI device %(address)s" msgstr "虚拟机没有 PCI 设备 %(address)s" -#: vrtManager/instance.py:2917 +#: vrtManager/instance.py:2921 msgid "The VM has no USB controller" msgstr "虚拟机没有 USB 控制器" -#: vrtManager/instance.py:2936 +#: vrtManager/instance.py:2940 msgid "The VM has no such USB device" msgstr "虚拟机没有此 USB 设备" -#: vrtManager/instance.py:2977 +#: vrtManager/instance.py:2981 msgid "The VM has its own RNG device: change it in the XML editor" msgstr "虚拟机有自己的 RNG 设备:请在 XML 编辑器中修改" -#: vrtManager/instance.py:3004 +#: vrtManager/instance.py:3008 msgid "The VM has its own TPM: change it in the XML editor" msgstr "虚拟机有自己的 TPM:请在 XML 编辑器中修改" -#: vrtManager/instance.py:3023 +#: vrtManager/instance.py:3027 #, python-format msgid "Unknown crash action %(action)s" msgstr "未知的崩溃操作 %(action)s" diff --git a/operations/tests/test_worker.py b/operations/tests/test_worker.py index 582ee827..3595e6a9 100644 --- a/operations/tests/test_worker.py +++ b/operations/tests/test_worker.py @@ -21,7 +21,7 @@ from operations.utils import words from operations import jobs from operations.models import Claim, Operation, WorkerLease from operations.tests import fakes -from operations.worker import Worker, data_dir, host_identity, housekeeping +from operations.worker import Worker, data_dir, host_identity, housekeeping, traceback_of from vrtManager.util import ConnectionFailed @@ -250,12 +250,25 @@ class WorkerTestCase(TransactionTestCase): def test_a_job_thread_that_raised_before_its_effects_fails(self): op = self.submit("test.noop", {}, "key-00000001") - with patch("operations.jobs.Context", side_effect=RuntimeError("no context")): + with patch("operations.jobs.Context", side_effect=RuntimeError("no context")), \ + self.assertLogs("operations.worker", "WARNING") as logs: self.start() self.assertTrue(wait_for(lambda: state(op) == Operation.FAILED)) - self.assertIn("no context", words(Operation.objects.get(pk=op.pk).error)) + # a fault of the panel: users read that, the log has the exception with its traceback + self.assertEqual(words(Operation.objects.get(pk=op.pk).error), "The panel failed unexpectedly; its log has the details.") + self.assertTrue(any("no context" in line and "Traceback" in line for line in logs.output)) self.assertFalse(Claim.objects.filter(operation=op).exists()) + def test_a_traceback_in_the_log_has_no_secret(self): + # as the message: the exception's text is written again at its end + try: + raise RuntimeError("password=review-dummy-secret") + except RuntimeError as err: + trace = traceback_of(err) + self.assertIn("Traceback", trace) + self.assertIn("password=***", trace) + self.assertNotIn("review-dummy-secret", trace) + def test_a_database_error_skips_a_tick(self): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) diff --git a/operations/utils.py b/operations/utils.py index f5240e5e..0a3bc845 100644 --- a/operations/utils.py +++ b/operations/utils.py @@ -7,6 +7,7 @@ the Settings page. The worker reads them itself (the settings middleware only runs in gunicorn).""" import re +import traceback import libvirt from django.core.exceptions import PermissionDenied @@ -121,6 +122,27 @@ def public_error_text(exception): return words(public_error(exception)) +# not libvirt's error or a refusal (whose text is for users) but a fault of +# the panel: its text (a SQL error, a KeyError's key) tells users nothing +UNEXPECTED = gettext_noop("The panel failed unexpectedly; its log has the details.") +REPORTED = (libvirtError, PermissionDenied, Http404, ValueError) + + +def traceback_of(err): + """A fault of the panel, also one a reported error wraps, goes to the log + with its traceback, without secrets as the message (exc_info would write + them); a reported error says enough.""" + fault = err.__cause__ if isinstance(err, REPORTED) else err + if fault is None or isinstance(fault, REPORTED): + return "" + return "\n" + safe_text("".join(traceback.format_exception(err))) + + +def job_error(err): + """What users read of a job's exception.""" + return public_error(err) if isinstance(err, REPORTED) else say(UNEXPECTED) + + def check_params(params): if isinstance(params, dict): for key, value in params.items(): diff --git a/operations/worker.py b/operations/worker.py index 9e9efd07..05113064 100644 --- a/operations/worker.py +++ b/operations/worker.py @@ -28,6 +28,7 @@ from logs.models import Logs from . import claims, jobs, utils from .models import Claim, LeaseLost, Operation, acquire, fenced, renew +from .utils import job_error, traceback_of logger = logging.getLogger(__name__) @@ -220,8 +221,8 @@ class Worker: self.progress_asked.pop(op_id, None) if (err := future.exception()) is not None: # execute raised before it could finish the job itself - logger.error("job %s ended unfinished: %s", op_id, utils.safe_text(err)) - self.recovery.inspect_now(op_id, utils.public_error(err)) + logger.error("job %s ended unfinished: %s%s", op_id, utils.safe_text(err), traceback_of(err)) + self.recovery.inspect_now(op_id, job_error(err)) def _concurrency(self): return self.fixed_concurrency or utils.concurrency() @@ -357,13 +358,13 @@ def execute(worker, op_id): except jobs.Cancelled: jobs.finish(op, jobs.Outcome(Operation.CANCELLED), worker=worker) except Exception as err: - logger.warning("job %s %s raised: %s", op.pk, op.kind, utils.safe_text(err)) + logger.warning("job %s %s raised: %s%s", op.pk, op.kind, utils.safe_text(err), traceback_of(err)) try: # jobs have no request: the error users see never names a host if ctx.effects_started: - worker.recovery.inspect_now(op.pk, error=utils.public_error(err)) + worker.recovery.inspect_now(op.pk, error=job_error(err)) else: - jobs.finish(op, jobs.Outcome(Operation.FAILED, error=utils.public_error(err)), worker=worker) + jobs.finish(op, jobs.Outcome(Operation.FAILED, error=job_error(err)), worker=worker) except LeaseLost: pass finally: @@ -478,13 +479,13 @@ class Scheduler: now = time.monotonic() if now - self.failing_since.setdefault(op_id, now) >= RECOVERY_LIMIT: self._finish(op, jobs.Outcome(Operation.UNCERTAIN, error=self._error( - op_id, [*utils.public_error(err), *utils.say(UNREACHABLE)]))) + op_id, [*job_error(err), *utils.say(UNREACHABLE)]))) return n = self.attempts.get(op_id, 0) self.attempts[op_id] = n + 1 self.next_try[op_id] = now + BACKOFF[min(n, len(BACKOFF) - 1)] self._phase(op_id, WAITING) - logger.warning("recovery of job %s waits: %s", op_id, utils.safe_text(err)) + logger.warning("recovery of job %s waits: %s%s", op_id, utils.safe_text(err), traceback_of(err)) return self.failing_since.pop(op_id, None) if outcome.state == Operation.RUNNING: @@ -507,7 +508,7 @@ class Scheduler: except LeaseLost: self._done(op_id) except Exception as err: - logger.error("recovery of job %s failed: %s", op_id, utils.safe_text(err)) + logger.error("recovery of job %s failed: %s%s", op_id, utils.safe_text(err), traceback_of(err)) self.next_try[op_id] = time.monotonic() + BACKOFF[-1] finally: self.inflight.discard(op_id)