i18n: clarify restored OCI registration and cancellation

This commit is contained in:
VAIO73
2026-10-21 07:05:14 +00:00
parent 56844d7f04
commit 06fbea8735
7 changed files with 266 additions and 23 deletions
@@ -0,0 +1,222 @@
"""Restored-app registration through real Menu/CLI/Dialog; no host effects."""
from contextlib import nullcontext, redirect_stdout
import copy
import io
import json
from pathlib import Path
import string
import subprocess
import tempfile
import unittest
from unittest.mock import patch
from test_oci_update_cancellation import CancellationFixture, ROOT
from proxmenux_oci import cli, i18n, management as menu, ui as native_ui
import oci_restore_recovery as restored
import oci_ui
class RegistrationFixture(CancellationFixture):
def setUp(self):
super().setUp()
self.found = [{'vmid':110, 'hostname':'fiction-primary'}]
self.original_found = copy.deepcopy(self.found)
self.ui.confirm.return_value = False
item = patch.object(menu, 'restored_applications', side_effect=lambda project:self.found)
self.pending = item.start();self.addCleanup(item.stop)
self.firewall = self.guard(menu, '_restored_firewall_rules')
self.backend = [self.guard(restored, name) for name in ('recover','register','run')]
self.files = [self.guard(Path, name) for name in ('write_text','write_bytes','unlink')]
self.processes = [self.guard(subprocess, name) for name in ('run','Popen','check_output')]
def members(self):
self.found.append({'vmid':111,'hostname':'fiction-dependent'})
self.original_found = copy.deepcopy(self.found)
def no_effects(self):
super().no_effects()
self.firewall.assert_not_called()
self.safe_backend()
def safe_backend(self):
for item in [*self.backend,*self.files,*self.processes]:item.assert_not_called()
self.assertEqual(self.found,self.original_found)
def accepted(self, answers, completed=True):
self.ui.confirm.side_effect=answers
self.firewall.side_effect=None
self.firewall.return_value=[{'vmid':row['vmid'],'port':61208 if row['vmid']==110 else 19999,
'source':'192.0.2.0/24'} for row in self.found]
self.lifecycle.side_effect=None;self.lifecycle.return_value=completed
self.pending.side_effect=lambda project:[] if completed and self.lifecycle.called else self.found
class RegistrationCancellationTests(RegistrationFixture):
def test_offer_no_is_explicit_falsy_cancellation(self):
self.assertIs(menu.offer_recovery(ROOT/'oci',self.ui),menu.CANCELLED)
self.assertFalse(menu.CANCELLED);self.no_effects()
def test_direct_single_no_finishes_before_a_third_pending_read(self):
self.assertEqual(menu.direct_recovery(ROOT/'oci'),6)
self.assertEqual(self.pending.call_count,2)
self.no_effects()
def test_real_cli_single_no_returns_six_without_output(self):
with patch.object(cli,'Catalog'),redirect_stdout(io.StringIO()) as output:
self.assertEqual(cli.main(['recover']),6)
self.assertEqual(output.getvalue(),'');self.no_effects()
def test_real_cli_multi_container_no_is_not_a_guest_action(self):
self.members()
with patch.object(cli,'Catalog'):
self.assertEqual(cli.main(['recover']),6)
text=self.ui.confirm.call_args.args[0]
self.assertIn('CT 110',text);self.assertIn('CT 111',text)
self.no_effects()
def test_actual_dialog_initial_no_and_escape_for_single_and_multi(self):
dialog=native_ui.DialogUI()
for multi in (False,True):
if multi:self.members()
for code in (1,255):
with self.subTest(multi=multi,code=code),patch.object(native_ui,'interactive_ui',return_value=dialog), \
patch.object(dialog,'_run',return_value=subprocess.CompletedProcess('dialog',code,stderr='')):
self.assertEqual(menu.direct_recovery(ROOT/'oci'),6)
self.no_effects()
def test_native_menu_continues_to_list_after_registration_no(self):
self.ui.choose.return_value=None
with patch.object(menu,'_clean_orphans'),patch.object(menu,'_records_unread',return_value=[]):
menu.interactive_management(ROOT/'oci',self.ui)
self.ui.choose.assert_called_once();self.no_effects()
def test_optional_firewall_and_start_no_registers_without_flags(self):
self.members();self.accepted([True,False,False,False])
self.assertEqual(menu.direct_recovery(ROOT/'oci'),0)
self.assertEqual(self.lifecycle.call_args.args[0],
[menu.sys.executable,str(ROOT/'oci/remote/oci_restore_recovery.py'),'recover'])
self.assertEqual(self.ui.confirm.call_count,4);self.safe_backend()
def test_single_optional_no_preserves_registration_without_start(self):
self.accepted([True,False,False])
self.assertEqual(menu.direct_recovery(ROOT/'oci'),0)
self.assertEqual(self.lifecycle.call_args.args[0],
[menu.sys.executable,str(ROOT/'oci/remote/oci_restore_recovery.py'),'recover'])
self.assertEqual(self.ui.confirm.call_count,3);self.safe_backend()
def test_only_one_confirmed_firewall_is_forwarded_and_no_start(self):
self.members();self.accepted([True,False,True,False])
self.assertEqual(menu.direct_recovery(ROOT/'oci'),0)
self.assertEqual(self.lifecycle.call_args.args[0][-2:],['--host-firewall','111'])
self.assertNotIn('--start',self.lifecycle.call_args.args[0]);self.safe_backend()
def test_explicit_start_yes_retains_flag_but_never_starts_any_guest_here(self):
self.accepted([True,False,True])
self.assertEqual(menu.direct_recovery(ROOT/'oci'),0)
self.assertEqual(self.lifecycle.call_args.args[0][-1],'--start');self.safe_backend()
def test_failed_registration_stays_failure_with_pending_records(self):
self.accepted([True,False,False],completed=False)
self.assertEqual(menu.direct_recovery(ROOT/'oci'),1)
self.safe_backend()
def test_empty_list_is_success_without_confirmation_or_backend(self):
self.found.clear();self.original_found=[]
self.assertEqual(menu.direct_recovery(ROOT/'oci'),0)
self.ui.confirm.assert_not_called();self.ui.message.assert_called_once();self.no_effects()
def test_root_identity_failure_never_probes_restored_apps(self):
with patch.object(menu.os,'geteuid',return_value=1000):
self.assertEqual(menu.direct_recovery(ROOT/'oci'),1)
self.pending.assert_not_called();self.ui.confirm.assert_not_called();self.no_effects()
def test_empty_offer_does_not_cancel_the_native_menu(self):
self.found.clear();self.original_found=[]
self.assertIsNone(menu.offer_recovery(ROOT/'oci',self.ui))
self.ui.confirm.assert_not_called();self.no_effects()
def test_empty_registration_state_is_rendered_in_slovak_without_recovery(self):
catalog=json.loads((ROOT/'lang/sk.json').read_text())
with patch.object(i18n,'_language','sk'),patch.object(i18n,'_cache',catalog):
self.found.clear();self.original_found=[]
self.assertEqual(menu.direct_recovery(ROOT/'oci'),0)
self.assertEqual(self.ui.message.call_args.args[0],
'Žiadna OCI aplikácia obnovená zo zálohy nečaká na opätovnú registráciu.')
self.no_effects()
def test_real_backend_blocker_stays_error_and_uses_registration_wording(self):
catalog=json.loads((ROOT/'lang/sk.json').read_text())
plan={'title':'Fictional restored application','restored':[{'vmid':110}],
'blockers':['Fictional missing resource'],'notes':[]}
with patch.object(menu.sys,'argv',['fixture','recover','--root','/fiction-only']), \
patch.object(restored.instances,'locked',return_value=nullcontext()), \
patch.object(restored,'prepare',return_value=[plan]), \
patch.object(oci_ui,'_language','sk'),patch.object(oci_ui,'_cache',catalog), \
patch.object(restored,'msg_error') as error,patch.object(restored,'msg_info2'):
self.assertEqual(restored.main(),1)
error.assert_called_once_with('Túto aplikáciu zatiaľ nemožno znova zaregistrovať:')
self.no_effects()
def test_real_host_resource_check_reports_missing_data_without_creating_anything(self):
catalog=json.loads((ROOT/'lang/sk.json').read_text())
plan={'restored':[{'vmid':110,'config':'mp0: /fiction-only/data,mp=/data\n',
'copy':{'record':{'deployment':{},'observed':{}}}}], 'blockers':[],'notes':[]}
with patch.object(oci_ui,'_language','sk'),patch.object(oci_ui,'_cache',catalog), \
patch.object(restored.os.path,'isdir',return_value=False), \
patch.object(restored,'firewall_rule',return_value=None):
restored.check_host_resources(plan)
self.assertEqual(len(plan['blockers']),1)
self.assertIn('/fiction-only/data',plan['blockers'][0])
self.assertIn('registráciu zopakujte',plan['blockers'][0])
self.no_effects()
class RegistrationContractTests(unittest.TestCase):
def test_real_monitor_shell_recover_keeps_global_cli_contract_and_exit_six(self):
with tempfile.TemporaryDirectory() as folder:
base=Path(folder);(base/'scripts/oci').mkdir(parents=True)
(base/'utils.sh').write_text('load_language() { :; }\ninitialize_cache() { :; }\n')
(base/'scripts/oci/oci_manager_apps.sh').write_text('printf "%s\\n" "$@"\nexit 6\n')
script=(ROOT/'scripts/oci/manage_instance.sh').read_text()
script=script.replace('/usr/local/share/proxmenux',folder)
import os
result=subprocess.run(['bash','-c',script],capture_output=True,text=True,
env={**os.environ,'ACTION':'recover','VMID':'not-a-vmid'})
self.assertEqual(result.returncode,6);self.assertEqual(result.stdout,'recover\n')
self.assertEqual(result.stderr,'')
def test_curated_slovak_registration_words_and_format_fields(self):
catalog=json.loads((ROOT/'lang/sk.json').read_text())
expected={
'Recover restored OCI applications':'Registrácia',
'No restored OCI application is waiting to be recovered.':'registráciu',
'Recover them now?':'zaregistrovať',
'This application cannot be recovered yet:':'zaregistrovať',
'The application could not be recovered:':'zaregistrovať',
'Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.':'vyberte Nie',
'it is locked or its disk could not be mounted':'uzamknutý',
'The local storage does not accept snippets':'local',
'it uses an NVIDIA GPU and this host has no working NVIDIA driver and Container Toolkit. Install them and run the recovery again.':'NVIDIA Container Toolkit',
'the NVIDIA hook of the container is not available:':'spúšťací skript',
'its Rclone mount is published again on this host at':'v ceste',
'Choose another bridge for the container in Proxmox (Network) and run the recovery again.':'registráciu',
'Mount or create it with its data and run the recovery again.':'registráciu',
'Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.':'registráciu',
}
fields=lambda text:sorted(f for _,f,_,_ in string.Formatter().parse(text) if f)
for key,word in expected.items():
with self.subTest(key=key):
self.assertIn(word,catalog[key]);self.assertEqual(fields(key),fields(catalog[key]))
def test_registration_web_text_is_distinct_from_previous_installation_recovery(self):
for locale in ('en','sk'):
data=json.loads((ROOT/'AppImage/messages'/locale/'common.json').read_text())['vmLxc']
with self.subTest(locale=locale):
self.assertEqual(data['ociUpdates']['registerRestored'],
'Znova zaregistrovať' if locale=='sk' else 'Register again')
self.assertEqual(data['ociUpdates']['recover'],'Obnoviť' if locale=='sk' else 'Recover')
self.assertIn('zaregistrovať' if locale=='sk' else 'Register again',data['updates']['ociRestoredBody'])
self.assertIn('Zaregistrovať' if locale=='sk' else 'Register',data['ociUpdates']['terminalTitleRecover'])
if __name__=='__main__':unittest.main()
+1 -1
View File
@@ -5747,7 +5747,7 @@ export function VirtualMachines() {
onClick={() => setOciAction({ vmid: selectedVM.vmid, action: "recover" })}
>
<RotateCcw className="h-4 w-4 mr-1.5" />
{t("vmLxc.ociUpdates.recover")}
{t("vmLxc.ociUpdates.registerRestored")}
</Button>
</div>
)}
+3 -2
View File
@@ -1327,10 +1327,11 @@
"update": "Update",
"modify": "Modify",
"recover": "Recover",
"registerRestored": "Register again",
"terminalTitleUpdate": "Update OCI application",
"terminalTitleRecoverPrevious": "Recover the previous OCI installation",
"terminalTitleModify": "Modify OCI application",
"terminalTitleRecover": "Recover restored OCI application",
"terminalTitleRecover": "Register restored OCI applications",
"terminalDescription": "Runs the same process as the ProxMenux menu: OCI manager Apps → Manage installed OCI applications.",
"keepBackup": "Keep the backup taken before updating",
"noKeepBackup": "The backup taken before updating is not kept",
@@ -1561,7 +1562,7 @@
"ociTitle": "OCI image container",
"ociBody": "This container was created from an OCI image outside OCI manager Apps. It is updated by replacing its image, which ProxMenux does not manage for it.",
"ociRestoredTitle": "Restored OCI application",
"ociRestoredBody": "This container was restored from a backup and this host has no record of its application, so it cannot be updated from here yet. Recover registers it again on this host, the same as OCI manager Apps → Manage installed OCI applications.",
"ociRestoredBody": "This container was restored from a backup and this host has no record of its OCI application, so it cannot be updated here yet. Register again opens the registration of restored OCI applications on this node, as in OCI manager Apps → Manage installed OCI applications.",
"dockerImagesTitle": "Docker images",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Images",
+3 -2
View File
@@ -1252,10 +1252,11 @@
"update": "Aktualizovať",
"modify": "Upraviť",
"recover": "Obnoviť",
"registerRestored": "Znova zaregistrovať",
"terminalTitleUpdate": "Aktualizovať OCI aplikáciu",
"terminalTitleRecoverPrevious": "Obnoviť predchádzajúcu OCI inštaláciu",
"terminalTitleModify": "Upraviť OCI aplikáciu",
"terminalTitleRecover": "Obnoviť obnovenú OCI aplikáciu",
"terminalTitleRecover": "Zaregistrovať OCI aplikácie obnovené zo zálohy",
"terminalDescription": "Použije sa rovnaký postup ako v ponuke Správca OCI aplikácií → Spravovať nainštalované OCI aplikácie.",
"keepBackup": "Ponechať zálohu vytvorenú pred aktualizáciou",
"noKeepBackup": "Záloha vytvorená pred aktualizáciou sa neponecháva",
@@ -1486,7 +1487,7 @@
"ociTitle": "Kontajner z OCI obrazu",
"ociBody": "Tento kontajner bol vytvorený z OCI (Docker) obrazu mimo Správcu OCI aplikácií. Aktualizuje sa nahradením obrazu, čo ProxMenux pri tomto kontajneri nespravuje.",
"ociRestoredTitle": "Obnovená OCI aplikácia",
"ociRestoredBody": "Tento kontajner bol obnovený zo zálohy a tento hostiteľ nemá záznam o jeho aplikácii, preto ho zatiaľ nemožno aktualizovať odtiaľto. Obnoviť ho znova zaregistruje na tomto hostiteľovi, rovnako ako OCI manager Apps → Spravovať nainštalované OCI aplikácie.",
"ociRestoredBody": "Tento kontajner bol obnovený zo zálohy, ale tento server nemá záznam o jeho OCI aplikácii, preto ju tu zatiaľ nemožno aktualizovať. Možnosť Znova zaregistrovať otvorí registráciu obnovených OCI aplikácií na tomto uzle, rovnako ako ponuka Správca OCI aplikácií → Spravovať nainštalované OCI aplikácie.",
"dockerImagesTitle": "Docker obrazy",
"dockerAppTitle": "Docker",
"dockerImagesSubheading": "Obrazy",
+14 -14
View File
@@ -903,7 +903,7 @@
"Choose an action:": "Vyberte akciu:",
"Choose an export to mount:": "Vyberte export, ktorý chcete pripojiť:",
"Choose an option:": "Vyberte možnosť:",
"Choose another bridge for the container in Proxmox (Network) and run the recovery again.": "Vyberte iný most pre kontajner v Proxmox (Sieť) a znova spustite obnovu.",
"Choose another bridge for the container in Proxmox (Network) and run the recovery again.": "V Proxmoxe vyberte pre kontajner iný sieťový most (Sieť) a registráciu zopakujte.",
"Choose any host path you want to share with CTs.": "Vyberte ľubovoľnú cestu na serveri, ktorú chcete zdieľať s CT.",
"Choose conflict policy": "Vyberte, ako vyriešiť konflikt",
"Choose conflict policy for the source VM:": "Vyberte pravidlo riešenia konfliktu pre zdrojovú VM:",
@@ -1143,7 +1143,7 @@
"Conflicting path included in backup:": "Konfliktná cesta zahrnutá v zálohe:",
"Conflicting utilities removed": "Konfliktné nástroje odstránené",
"Connect a Coral Accelerator and try again.": "Pripojte Coral Accelerator a skúste to znova.",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Pripojte ho alebo ho odstráňte z kontajnera v Proxmox (zdroje) a znova spustite obnovenie.",
"Connect it, or remove it from the container in Proxmox (Resources), and run the recovery again.": "Pripojte zariadenie alebo ho odstráňte z konfigurácie kontajnera v Proxmoxe (Zdroje) a registráciu zopakujte.",
"Connect your devices and users together in your own secure virtual private network.": "Spojte svoje zariadenia a užívateľov vo vlastnej zabezpečenej virtuálnej súkromnej sieti.",
"Connect your devices into a secure WireGuard®-based overlay network with SSO, MFA and granular access controls.": "Pripojte svoje zariadenia do zabezpečenej prekryvnej siete založenej na WireGuard® s ovládacími prvkami SSO, MFA a granulovaného prístupu.",
"Connected": "Pripojené",
@@ -3789,7 +3789,7 @@
"Mount name": "Názov pripojenia",
"Mount not authorized by the operation": "Pripojiť nie je povolené operation",
"Mount options:": "Možnosti pripojenia:",
"Mount or create it with its data and run the recovery again.": "Pripojte alebo vytvorte ho s jeho údajmi a znova spustite obnovenie.",
"Mount or create it with its data and run the recovery again.": "Pripojte alebo vytvorte adresár s potrebnými údajmi a registráciu zopakujte.",
"Mount path must be an absolute path starting with /": "Cesta pripojenia musí byť absolútna a začínať znakom /",
"Mount path:": "Cesta pripojenia:",
"Mount paths must not overlap": "Pripojiť cesty sa nesmú prekrývať",
@@ -4302,7 +4302,7 @@
"No recent Samba servers found.": "Nenašli sa žiadne nedávne Samba servery.",
"No registered OCI containers are available for selection on this host.": "Na tomto hostiteľovi nie sú dostupné žiadne registrované kontajnery OCI.",
"No registry API was found at {registry}. Check the hostname and port.": "Na {registry} sa nenašlo žiadne API registra. Skontrolujte názov hostiteľa a port.",
"No restored OCI application is waiting to be recovered.": "Žiadna obnovená aplikácia OCI nečaká na obnovenie.",
"No restored OCI application is waiting to be recovered.": "Žiadna OCI aplikácia obnovená zo zálohy nečaká na opätovnú registráciu.",
"No results": "Žiadne výsledky",
"No routing information found.": "Nenašli sa žiadne informácie o smerovaní.",
"No scheduled backup jobs configured.": "Nie sú nastavené žiadne naplánované úlohy záloh.",
@@ -5068,10 +5068,10 @@
"Recover OCI stack": "Obnoviť OCI aplikáciu s viacerými kontajnermi",
"Recover now?": "Obnoviť teraz?",
"Recover or complete the operation?": "Obnoviť alebo dokončiť operáciu?",
"Recover restored OCI applications": "Obnovte obnovené aplikácie OCI",
"Recover restored OCI applications": "Registrácia obnovených OCI aplikácií",
"Recover the keyfile using your recovery passphrase?": "Obnoviť keyfile pomocou obnovovacej frázy?",
"Recover the previous installation": "Obnoviť predchádzajúcu inštaláciu",
"Recover them now?": "Obnoviť ich teraz?",
"Recover them now?": "Znova zaregistrovať uvedené aplikácie?",
"Recoverable:": "Obnoviteľné:",
"Recovering the previous installation": "Obnovovanie predchádzajúcej inštalácie",
"Recovery blob upload failed — main backup is OK, but keyfile recovery from PBS will not be available for this backup.": "Nahratie obnovovacieho balíčka zlyhalo - hlavná záloha je v poriadku, ale obnova keyfile z PBS pre túto zálohu nebude dostupná.",
@@ -6021,7 +6021,7 @@
"Start the LXC when finished to apply the selected configuration?": "Spustiť LXC po dokončení pre použitie zvolenej konfigurácie?",
"Start the VM": "Spustiť VM",
"Start the VM to begin Windows installation from the mounted ISO.": "Spustite VM a začnite inštaláciu Windowsu z pripojeného ISO.",
"Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Spustiť aplikácie po ich zaregistrovaní? Odpoveď Nie, ak pôvodné kontajnery stále bežia na inom hostiteľovi: oba by používali rovnaké adresy.",
"Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Spustiť aplikácie po ich opätovnej registrácii? Ak pôvodné kontajnery stále bežia na inom serveri, vyberte Nie. Obe inštalácie by používali rovnaké adresy.",
"Start the converted container:": "Spustiť prevedený kontajner:",
"Start the interactive Proxmox package upgrade": "Spustiť interaktívnu aktualizáciu balíkov Proxmoxu",
"Start the main system upgrade:": "Spustite hlavnú aktualizáciu systému:",
@@ -6344,7 +6344,7 @@
"The all-in-one AI application.": "Aplikácia UI.",
"The application configuration needs a first start to complete.": "Konfigurácia aplikácie potrebuje prvý štart na dokončenie.",
"The application could not be modified:": "Aplikáciu nebolo možné upraviť:",
"The application could not be recovered:": "Aplikáciu nebolo možné obnoviť:",
"The application could not be recovered:": "Aplikáciu sa nepodarilo znova zaregistrovať:",
"The application could not be recreated:": "Aplikáciu nebolo možné znova vytvoriť:",
"The application did not complete its initial setup:": "Žiadosť nevyplnila svoje pôvodné nastavenie:",
"The application did not get an address on the access network:": "Aplikácia nedostala adresu v prístupovej sieti:",
@@ -6561,7 +6561,7 @@
"The kernel module is not active:": "Modul jadra nie je aktívny:",
"The kernel module of version": "Modul jadra verzie",
"The local envelope is dropped and future backups do not upload anything. Uploaded envelopes already on PBS stay intact and remain recoverable with their original passphrase.": "Lokálna obálka sa zahodí a budúce zálohy už nič nenahrajú. Obálky, ktoré už sú v PBS, zostanú zachované a budú obnoviteľné pôvodnou frázou.",
"The local storage does not accept snippets": "Miestne úložisko neprijíma úryvky",
"The local storage does not accept snippets": "Úložisko local nepodporuje obsah typu snippets",
"The long test runs directly on the disk hardware.": "Dlhý test beží priamo na hardvéri disku.",
"The machine learning container is rebuilt with the image of the new choice. Its model cache, the library and the database are kept. The whole application is stopped and updated, as in an update. If the operation fails after a container was changed, the previous containers and the previous choice are restored. An operation that stops halfway is recovered from the OCI management menu.": "Kontajner strojového učenia sa znova vytvorí z obrazu pre zvolený CPU alebo GPU. Vyrovnávacia pamäť modelov, knižnica a databáza sa zachovajú. Celá aplikácia sa zastaví a aktualizuje rovnakým postupom ako pri bežnej aktualizácii. Ak operácia po zmene kontajnera zlyhá, ProxMenux sa pokúsi obnoviť predchádzajúce kontajnery aj pôvodnú voľbu CPU/GPU. Ak operácia zostane nedokončená, obnovte ju z ponuky správy OCI.",
"The machine learning container is rebuilt with the image of the new choice. Its model cache, the library and the database are kept. The whole application is stopped and updated, as in an update; if anything fails, the previous containers and the previous choice are restored.": "Kontajner strojového učenia je prestavaný s obrazom novej voľby. Uchováva sa jej vyrovnávacia pamäť modelu, knižnica a databáza. Celá aplikácia sa zastaví a aktualizuje ako pri aktualizácii; ak čokoľvek zlyhá, obnovia sa predchádzajúce kontajnery a predchádzajúca voľba.",
@@ -6846,7 +6846,7 @@
"This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Táto VM vyžaduje ďalšie kroky inštalácie. Návod nájdete tu:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144",
"This account is for {registry}. Enter an image from that registry.": "Tento účet patrí registru {registry}. Zadajte obraz z tohto registra.",
"This action will:": "Táto akcia vykoná:",
"This application cannot be recovered yet:": "Túto aplikáciu zatiaľ nie je možné obnoviť:",
"This application cannot be recovered yet:": "Túto aplikáciu zatiaľ nemožno znova zaregistrovať:",
"This application cannot be recovered yet; nothing was changed:": "Túto aplikáciu zatiaľ nie je možné obnoviť; nič sa nezmenilo:",
"This application is not an Immich installed by ProxMenux": "Táto aplikácia nie je Immich nainštalovaný pomocou ProxMenuxu",
"This application is not in the OCI catalog.": "Táto aplikácia nie je v katalógu OCI.",
@@ -7905,12 +7905,12 @@
"is referenced in the following stopped VM(s)/CT(s):": "je uvedený v týchto zastavených VM/CT:",
"it asks for the network of the host; the container gets its own address instead": "žiada o sieť hostiteľa; kontajner dostane namiesto toho svoju vlastnú adresu",
"it could not be started:": "nedalo sa spustiť:",
"it is locked or its disk could not be mounted": "je zamknutá alebo sa jej disk nepodarilo pripojiť",
"it is locked or its disk could not be mounted": "je uzamknutý alebo sa nepodarilo pripojiť jeho disk",
"it publishes no other architecture": "nepublikuje žiadnu inú architektúru",
"it uses an NVIDIA GPU and this host has no working NVIDIA driver and Container Toolkit. Install them and run the recovery again.": "používa NVIDIA GPU a tento hostiteľ nemá funkčný ovládač NVIDIA a súpravu kontajnerových nástrojov. Nainštalujte ich a znova spustite obnovenie.",
"it uses an NVIDIA GPU and this host has no working NVIDIA driver and Container Toolkit. Install them and run the recovery again.": "používa GPU NVIDIA, ale na tomto serveri nie je funkčný ovládač NVIDIA alebo NVIDIA Container Toolkit. Nainštalujte chýbajúce súčasti a registráciu zopakujte.",
"it uses the Compose option": "používa možnosť Compose",
"its NVIDIA runtime could not be rebuilt for the driver of this host; the container will not start until it is installed again:": "jeho runtime NVIDIA nebolo možné prebudovať pre ovládač tohto hostiteľa; kontajner sa nespustí, kým nebude znova nainštalovaný:",
"its Rclone mount is published again on this host at": "jeho pripojenie Rclone je opäť zverejnené na tomto hostiteľovi na adrese",
"its Rclone mount is published again on this host at": "jeho pripojenie Rclone sa na tomto serveri opäť sprístupní v ceste",
"its Rclone mount is published by a host script that a backup does not include. Enable the mount again from the Rclone entry of the catalog before starting it.": "jeho pripojenie Rclone je publikované hostiteľským skriptom, ktorý záloha neobsahuje. Pred spustením znova povoľte pripojenie v položke Rclone v katalógu.",
"its backup was made before ProxMenux kept the record inside the container": "jeho záloha bola vykonaná skôr, ako ProxMenux uchovával záznam v kontajneri",
"its configuration could not be read": "nepodarilo sa prečítať jeho konfiguráciu",
@@ -8123,7 +8123,7 @@
"systemctl restart networking failed:": "systemctl restart networking zlyhal:",
"systemd OnCalendar expression": "systemd OnCalendar výraz",
"the API key was not generated on the first start": "kľúč API nebol vytvorený pri prvom štarte",
"the NVIDIA hook of the container is not available:": "hák NVIDIA kontajnera nie je k dispozícii:",
"the NVIDIA hook of the container is not available:": "spúšťací skript NVIDIA pre tento kontajner nie je dostupný:",
"the bridge does not exist on this host:": "most na tomto hostiteľovi neexistuje:",
"the bridge exists on this host with another network:": "most existuje na tomto hostiteľovi s inou sieťou:",
"the container has its own address, so the port Docker published on the host is not needed": "kontajner má svoju vlastnú adresu, takže port Docker zverejnené na hostiteľa nie je potrebné",
+4 -2
View File
@@ -229,7 +229,7 @@ def offer_recovery(project, ui):
f"{translate('Until they are registered again they cannot be updated or managed from here. The recovery checks first what prevents their registration; an application with such a problem is not registered. A Rclone mount that has to be enabled again does not prevent it, but then the applications are not started automatically.')}"
f"\n\n{translate('Recover them now?')}")
if not ui.confirm(text, default=True):
return
return CANCELLED
command = [sys.executable, str(project / 'remote/oci_restore_recovery.py'), 'recover']
for rule in _restored_firewall_rules(project):
if ui.confirm(translate('CT {vmid} is a host monitor and had a rule in the host firewall. Allow TCP port {port} from {subnet} through the firewall of this host? Existing firewall rules are not changed.').format(
@@ -252,7 +252,9 @@ def direct_recovery(project):
if not restored_applications(project):
ui.message(translate('No restored OCI application is waiting to be recovered.'), translate('Restored OCI applications'))
return EXIT_DONE
offer_recovery(project, ui)
completed = offer_recovery(project, ui)
if completed is CANCELLED:
return EXIT_CANCELLED
return EXIT_DONE if not restored_applications(project) else EXIT_FAILED
+19 -2
View File
@@ -69,7 +69,7 @@ const fs=require('node:fs');
assert.match(fs.readFileSync('oci/src/proxmenux_oci/management.py','utf8'),/^EXIT_CANCELLED = 6 /m);
assert.equal(completion.OCI_CANCELLED_EXIT_CODE,6);
const parent=ts.createSourceFile('vms.tsx',read('components/virtual-machines.tsx'),ts.ScriptTarget.Latest,true,ts.ScriptKind.TSX);
let ociElement,reset,pendingRecoveryClick;
let ociElement,reset,pendingRecoveryClick,registrationClick,registrationLabel;
function find(node) {
if(ts.isJsxSelfClosingElement(node) && node.tagName.getText(parent)==='ScriptTerminalModal' &&
node.attributes.properties.some(p=>p.name?.getText(parent)==='scriptPath' && p.initializer?.text?.endsWith('/oci/manage_instance.sh'))) ociElement=node;
@@ -77,13 +77,30 @@ function find(node) {
if(ts.isJsxElement(node) && node.openingElement.tagName.getText(parent)==='Button') {
const click=node.openingElement.attributes.properties.find(p=>p.name?.getText(parent)==='onClick')?.initializer?.expression;
if(click?.getText(parent).includes('setOciAction(') && click.getText(parent).includes('action: "update"')) pendingRecoveryClick=click;
if(click?.getText(parent).includes('setOciAction(') && click.getText(parent).includes('action: "recover"')) {
registrationClick=click;
registrationLabel=node.children.find(child=>ts.isJsxExpression(child)&&child.expression)?.expression;
}
}
ts.forEachChild(node,find);
}
find(parent);assert.ok(ociElement);assert.ok(reset);assert.ok(pendingRecoveryClick);
find(parent);assert.ok(ociElement);assert.ok(reset);assert.ok(pendingRecoveryClick);assert.ok(registrationClick);assert.ok(registrationLabel);
const attribute=name=>ociElement.attributes.properties.find(p=>p.name?.getText(parent)===name)?.initializer?.expression;
assert.equal(attribute('cancelledExitCode').getText(parent),'OCI_CANCELLED_EXIT_CODE');
(async()=>{
for(const locale of ['en','sk']) {
const scope={t:translator(locale),selectedVM:{vmid:110},applyBackup:false,applyBackupStorage:'',selectedBackupStorage:''};
scope.setOciAction=value=>scope.ociAction=value;
executable(registrationClick,parent,scope)();
assert.equal(scope.ociAction.action,'recover');assert.equal(scope.ociAction.vmid,110);
assert.equal(executable(registrationLabel,parent,scope),locale==='sk'?'Znova zaregistrovať':'Register again');
assert.equal(executable(attribute('title'),parent,scope),locale==='sk'?
'Zaregistrovať OCI aplikácie obnovené zo zálohy':'Register restored OCI applications');
scope.selectedVM={vmid:111};
assert.equal(executable(attribute('params'),parent,scope).VMID,'110','registration preserves the clicked target context');
assert.equal(executable(attribute('params'),parent,scope).ACTION,'recover','registration retains its existing global CLI action');
assert.equal(scope.t('vmLxc.ociUpdates.recover'),locale==='sk'?'Obnoviť':'Recover');
}
for(const locale of ['en','sk']) for(const pending of [false,true]) {
const scope={t:translator(locale), selectedVM:{vmid:110}, ociInstance:{pending},
applyBackup:false,applyBackupStorage:'',selectedBackupStorage:''};