diff --git a/.github/scripts/tests/test_oci_removal_sk.py b/.github/scripts/tests/test_oci_removal_sk.py new file mode 100644 index 00000000..5af41c15 --- /dev/null +++ b/.github/scripts/tests/test_oci_removal_sk.py @@ -0,0 +1,235 @@ +"""Actual removal UI/guards in SK and EN; real host effects are forbidden.""" +from contextlib import nullcontext +import copy +import json +from pathlib import Path +import subprocess +import sys +import unittest +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[3] +sys.path[:0] = [str(ROOT / 'oci/src'), str(ROOT / 'oci/remote')] +from proxmenux_oci import i18n, management +from proxmenux_oci.ui import UserCancelled +import oci_instances as instances +import oci_remove as removal +import oci_ui + + +class RemovalSlovakTests(unittest.TestCase): + def setUp(self): + cache = json.loads((ROOT / 'lang/sk.json').read_text()) + for module in (i18n, oci_ui): + for name, value in (('_language', 'sk'), ('_cache', cache)): + p = patch.object(module, name, value) + p.start(); self.addCleanup(p.stop) + self.records = {} + self.make_records(2) + self.read = self.start(instances, 'read', side_effect=lambda root, vmid: self.records[vmid]) + self.start(management, '_selected_configuration', side_effect=self.config) + self.bridge = self.start(removal, 'bridge_in_use', return_value=False) + self.ui = Mock() + self.ui.review.return_value = False + self.forbidden = [] + for module, name in ((subprocess, 'run'), (subprocess, 'Popen'), (subprocess, 'check_output'), + (Path, 'write_text'), (Path, 'write_bytes'), (Path, 'unlink'), + (removal.shutil, 'rmtree'), (instances, 'write'), (instances, 'command'), + (management, '_run_lifecycle'), (removal, 'run'), + (removal, 'remove_host_state'), (removal.oci_console, 'remove_log'), + (removal, 'release_bridge'), (removal, 'remove_owned_host_firewall'), + (removal, 'release_shared_host_files'), (removal.work_backup, 'forget'), + (removal.image_cache, 'prune')): + self.forbidden.append(self.start(module, name, side_effect=AssertionError('Forbidden effect: ' + name))) + + def start(self, module, name, **kwargs): + p = patch.object(module, name, **kwargs) + result = p.start(); self.addCleanup(p.stop) + return result + + def make_records(self, count): + self.records.clear() + for offset in range(count): + vmid = 999990 + offset + self.records[vmid] = {'vmid': vmid, 'installation_id': 'fixture-' + str(vmid), + 'template': {'catalog_ui': {'title': 'Fixture App'}}, + 'deployment': {'mounts': [{'type': 'host-bind', 'source': '/fixture/saved', + 'container_path': '/saved'}]}} + if offset: + self.records[vmid]['stack_member'] = {'primary_vmid': 999990} + if count > 1: + self.records[999990]['stack'] = {'members': [{'vmid': vmid} for vmid in self.records], + 'deployment': {'network': {'private_bridge': 'vmbr999', 'private_subnet': '10.77.99.0/24'}}} + self.records[999990]['deployment']['host_firewall'] = {'port': 8080} + self.original = copy.deepcopy(self.records) + + def config(self, vmid): + return (f'hostname: fixture-{vmid}\nrootfs: fixture:vm-{vmid}-disk-0,size=8G\n' + f'mp0: fixture:vm-{vmid}-disk-1,mp=/data,size=16G\n' + 'mp1: /fixture/shared,mp=/shared\n').encode() + + def tearDown(self): + self.assertEqual(self.records, self.original) + for effect in self.forbidden: + effect.assert_not_called() + + def test_single_summary_separates_deleted_disks_and_retained_host_paths(self): + self.make_records(1) + text = management._removal_summary(ROOT / 'oci', 999990) + self.assertIn('Kontajnery určené na odstránenie:', text) + self.assertIn('Údaje kontajnera určené na vymazanie:', text) + self.assertIn('/data (16G)', text) + kept = text.split('Cesty hostiteľa nájdené')[1] + self.assertIn('/fixture/shared', kept); self.assertIn('/fixture/saved', kept) + self.assertNotIn('/fixture/shared', text.split('Cesty hostiteľa nájdené')[0]) + self.assertNotIn('súkromnej siete:', text) + + def test_primary_summary_counts_are_grammatical_for_two_three_and_five(self): + for count in (2, 3, 5): + with self.subTest(count=count): + self.make_records(count) + text = management._removal_summary(ROOT / 'oci', 999990) + self.assertIn(f'Počet kontajnerov aplikácie Fixture App: {count}.', text) + self.assertIn('Všetky sú určené na odstránenie.', text) + for vmid in self.records: + self.assertIn(f'CT {vmid}', text) + + def test_member_summary_warns_that_entire_application_is_selected(self): + text = management._removal_summary(ROOT / 'oci', 999991) + self.assertIn('CT 999991 patrí do aplikácie Fixture App (počet kontajnerov: 2).', text) + self.assertIn('odstránenie celej aplikácie', text) + self.assertNotIn('nádob', text) + + def test_free_bridge_is_only_targeted_and_firewall_removal_is_conditional(self): + text = management._removal_summary(ROOT / 'oci', 999990) + self.assertIn('Súkromná sieť určená na uvoľnenie, ak ju nepoužíva iná VM alebo iný CT: vmbr999', text) + self.assertIn('Môže sa odstrániť aj zodpovedajúce spravované pravidlo firewallu hostiteľa.', text) + + def test_shared_bridge_is_kept_and_not_reported_as_released(self): + self.bridge.return_value = True + text = management._removal_summary(ROOT / 'oci', 999990) + self.assertIn('Súkromná sieť zachovaná', text) + self.assertNotIn('určená na uvoľnenie', text) + + def test_missing_config_still_discloses_saved_host_paths_without_deleting(self): + with patch.object(management, '_selected_configuration', side_effect=OSError('fixture missing')): + text = management._removal_summary(ROOT / 'oci', 999990) + self.assertIn('/fixture/saved', text) + self.assertIn('rootfs -', text) + + def test_no_from_real_remove_review_never_calls_lifecycle(self): + self.assertFalse(management._remove(ROOT / 'oci', self.ui, 999991)) + review = self.ui.review.call_args + self.assertFalse(review.kwargs['default']) + self.assertEqual(review.kwargs['question'], + 'Odstrániť aplikáciu? Jej kontajnerové disky sa vymažú a obnoviť ich možno iba zo zálohy.') + + def test_cancelled_real_remove_review_never_calls_lifecycle(self): + self.ui.review.side_effect = UserCancelled('fixture cancelled') + with self.assertRaises(UserCancelled): + management._remove(ROOT / 'oci', self.ui, 999990) + + def test_preparation_error_does_not_show_confirmation_or_call_lifecycle(self): + with patch.object(removal, 'members_of', side_effect=ValueError('fixture record invalid')): + self.assertFalse(management._remove(ROOT / 'oci', self.ui, 999990)) + self.ui.review.assert_not_called() + self.assertIn('Odstránenie nebolo možné pripraviť:', self.ui.message.call_args.args[0]) + + def test_pending_operation_error_precedes_configuration_probe_and_all_mutations(self): + self.make_records(1) + self.records[999990]['pending_transaction'] = {'fixture': True} + self.original = copy.deepcopy(self.records) + with patch.object(removal, 'guest_config') as config: + with self.assertRaisesRegex(ValueError, 'Operácia tejto inštalácie sa ešte nedokončila'): + removal.remove(Path('/fixture/oci'), 999990) + config.assert_not_called() + + def test_container_on_other_node_errors_before_host_cleanup(self): + with patch.object(removal, 'guest_config', return_value=None), \ + patch.object(removal, 'guest_node', return_value='fixture-other-node'): + with self.assertRaisesRegex(ValueError, 'Kontajner beží na inom uzle klastra'): + removal.remove(Path('/fixture/oci'), 999990) + + def run_main(self, result=False, busy=False): + with patch.object(sys, 'argv', ['oci_remove.py', '999990']), \ + patch.object(removal.os, 'geteuid', return_value=0), \ + patch.object(instances, 'locked', side_effect=BlockingIOError if busy else None, + return_value=nullcontext()), \ + patch.object(removal, 'remove', return_value=result) as mocked_remove, \ + patch.object(removal, 'msg_ok') as ok, patch.object(removal, 'msg_error') as error: + code = removal.main() + return code, mocked_remove, ok, error + + def test_busy_registry_error_does_not_enter_removal(self): + code, remove, ok, error = self.run_main(busy=True) + self.assertEqual(code, 1); remove.assert_not_called(); ok.assert_not_called() + self.assertEqual(error.call_args.args[0], 'Register inštancií používa iná operácia OCI') + + def test_partial_completion_is_a_warning_to_review_not_unqualified_success(self): + code, remove, ok, error = self.run_main(result=True) + self.assertEqual(code, 0); error.assert_not_called() + self.assertEqual(ok.call_args.args[0], + 'Príkaz na odstránenie sa dokončil; skontrolujte všetky upozornenia vyššie.') + self.assertNotEqual(ok.call_args.args[0], i18n.translate('The application was removed')) + + def test_clean_completion_keeps_existing_success_contract(self): + code, remove, ok, error = self.run_main() + self.assertEqual(code, 0); error.assert_not_called() + self.assertEqual(ok.call_args.args[0], 'Aplikácia bola odstránená') + + def test_english_primary_and_member_summaries_keep_identical_sentences(self): + with patch.object(i18n, '_language', 'en'): + primary = management._removal_summary(ROOT / 'oci', 999990) + member = management._removal_summary(ROOT / 'oci', 999991) + self.assertTrue(primary.startswith('Fixture App runs in 2 containers. All of them are targeted for removal.\n')) + self.assertTrue(member.startswith('CT 999991 is one of the 2 containers of Fixture App. It cannot be removed on its own,')) + + def test_missing_translation_falls_back_to_the_same_english_summary(self): + with patch.object(i18n, '_cache', {}): + text = management._removal_summary(ROOT / 'oci', 999990) + self.assertTrue(text.startswith('Fixture App runs in 2 containers. All of them are targeted for removal.\n')) + + def test_yes_only_builds_the_existing_command_when_lifecycle_is_inert(self): + self.ui.review.return_value = True + with patch.object(management, '_run_lifecycle', return_value=True) as inert: + self.assertTrue(management._remove(ROOT / 'oci', self.ui, 999990)) + self.assertEqual(inert.call_args.args[0], + [sys.executable, str(ROOT / 'oci/remote/oci_remove.py'), '999990']) + + def test_single_and_stack_menu_routes_use_the_translated_label_and_real_no_review(self): + for count in (1, 2): + with self.subTest(count=count): + self.make_records(count) + self.ui.reset_mock() + self.ui.choose.return_value = 'remove' + row = {'vmid': 999990, 'stack': count > 1, 'pending': False, + 'status': 'installed', 'reason': 'matched'} + with patch.object(management, 'carry_records', return_value={}), \ + patch.object(management, 'check_selected', return_value=row): + self.assertFalse(management.manage_instance(ROOT / 'oci', self.ui, row)) + options = self.ui.choose.call_args.args[1] + self.assertIn(('remove', 'Odstrániť: zmazať aplikáciu a jej kontajnery'), options) + self.ui.review.assert_called_once() + self.assertFalse(self.ui.review.call_args.kwargs['default']) + + def test_changed_identity_warning_does_not_claim_the_other_container_was_deleted(self): + self.make_records(1) + # Exercise the real backend branch; record cleanup is inert as well. + with patch.object(removal, 'guest_config', return_value=b'fixture'), \ + patch.object(instances, 'identity', return_value='fixture-other-identity'), \ + patch.object(removal, 'remove_owned_host_firewall', return_value=True), \ + patch.object(removal, 'release_shared_host_files'), \ + patch.object(removal.work_backup, 'forget'), \ + patch.object(removal.image_cache, 'prune', return_value=[]), \ + patch.object(Path, 'exists', return_value=False), \ + patch.object(Path, 'is_dir', return_value=False), \ + patch.object(removal, 'msg_warn') as warning, \ + patch.object(removal, 'msg_ok') as ok, patch.object(removal, 'msg_info'): + self.assertTrue(removal.remove(Path('/fixture/oci'), 999990)) + self.assertEqual(warning.call_args.args[0], + 'VMID teraz patrí inému kontajneru; ten zostane nedotknutý: CT 999990') + self.assertNotIn('Odstránený kontajner:', str(ok.mock_calls)) + + +if __name__ == '__main__': + unittest.main() diff --git a/lang/sk.json b/lang/sk.json index c0bef7bd..ad28ed02 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -73,7 +73,7 @@ "A legacy gasket-dkms package was found on this host, but no Coral M.2 / PCIe hardware is present.": "Na tomto hostiteľovi sa našiel starý balík gasket-dkms, ale nie je prítomný žiadny hardvér Coral M.2 / PCIe.", "A managed rootfs and an unprivileged container are required": "Riadené rootfs a neprivilegovaný kontajner sú required", "A managed volume with backup enabled is required": "Riadený objem so zapnutou zálohou je required", - "A matching managed host firewall rule may also be removed.": "Zhodné pravidlo brány firewall spravovaného hostiteľa môže byť tiež odstránené.", + "A matching managed host firewall rule may also be removed.": "Môže sa odstrániť aj zodpovedajúce spravované pravidlo firewallu hostiteľa.", "A member VMID is in use by another guest or is on another node": "Člen VMID používa iný hosť alebo je na inom uzle", "A member configuration changed after the stack was checked": "Konfigurácia člena sa zmenila po kontrole zásobníka", "A member configuration changed during the preparation": "Konfigurácia člena sa počas prípravy zmenila", @@ -396,7 +396,7 @@ "An include is not part of the journal; recovery blocked": "Zahrnutie nie je súčasťou časopisu; obnova zablokovaná", "An include was modified outside the journal; recovery blocked": "Zahrnutie bolo upravené mimo časopisu; regenerácia zablokovaná", "An open source generative AI development platform for building AI Agents and LLM workflows": "Open source roding AI developing platform for building AI Agents and LLM workflows", - "An operation of this installation has not finished; recover it from the management menu before removing it": "operation tohto zariadenia ešte nie je dokončená; obnoviť ho z menu riadenia pred jeho odstránením", + "An operation of this installation has not finished; recover it from the management menu before removing it": "Operácia tejto inštalácie sa ešte nedokončila; pred odstránením ju obnovte v ponuke správy OCI", "An update does not accept configuration changes": "Aktualizácia neprijíma zmeny konfigurácie", "Analysis Tools": "Analytické nástroje", "Analysis software that shows your internet speed for up to 30 days.": "Analyzačný softvér, ktorý ukazuje rýchlosť internetu až 30 dní.", @@ -406,8 +406,8 @@ "Analyzing Network Configuration - READ ONLY MODE": "Analyzujem nastavenie siete - režim iba na čítanie", "Analyzing selected disks...": "Analyzujem vybrané disky...", "Analyzing system for available PCIe storage devices...": "Analyzujem systém a hľadám dostupné PCIe úložiskové zariadenia...", - "Another OCI operation is using the instance registry": "Ďalšia OCI operation používa inštančný register", - "Another OCI operation is using the instance registry. Wait for it to finish and open this menu again; no container is modified.": "Ďalšia OCI operation používa inštančný register. Počkajte, kým to dokončíte a znovu otvoríte toto menu; žiadna nádoba nie je upravená.", + "Another OCI operation is using the instance registry": "Register inštancií používa iná operácia OCI", + "Another OCI operation is using the instance registry. Wait for it to finish and open this menu again; no container is modified.": "Register inštancií používa iná operácia OCI. Počkajte na jej dokončenie a znova otvorte túto ponuku; žiadny kontajner sa nezmení.", "Another OCI operation is using the instance registry. Wait for it to finish.": "Register OCI aplikácií používa iná operácia. Počkajte na jej dokončenie.", "Another OCI operation is using the registry. This operation was not started.": "Ďalšia OCI operation používa register. Táto operation sa nezačala.", "Another ProxMenux OCI operation is running. Try again when it finishes.": "Prebieha ďalšia operácia ProxMenux OCI. Skúste to znova, keď to skončí.", @@ -1175,7 +1175,7 @@ "Container configured (not started):": "Kontajner nastavený (nezačína sa):", "Container converted to privileged": "Kontajner prevedený na privilegovaný", "Container created:": "Vytvorený kontajner:", - "Container data targeted for deletion:": "Údaje kontajnera zacielené na odstránenie:", + "Container data targeted for deletion:": "Údaje kontajnera určené na vymazanie:", "Container did not become ready in time. Skipping driver installation.": "Kontajner nebol včas pripravený. Inštaláciu ovládača preskakujem.", "Container did not start in time.": "Kontajner sa nestihol spustiť.", "Container distro": "Distribúcia kontajnera", @@ -1329,7 +1329,7 @@ "Could not change VM virtual display to vga: std": "Virtuálne zobrazenie VM sa nepodarilo zmeniť na vga: std", "Could not check the NVIDIA GPU": "Nepodarilo sa skontrolovať GPU NVIDIA", "Could not clone any gasket-driver repository. Check your internet connection and": "Nepodarilo sa naklonovať žiadny repozitár gasket-driver. Skontrolujte internetové pripojenie a", - "Could not complete private network release:": "Nepodarilo sa dokončiť vydanie súkromnej siete:", + "Could not complete private network release:": "Nepodarilo sa dokončiť uvoľnenie súkromnej siete:", "Could not configure IOMMU kernel parameters automatically. Configure manually and reboot.": "Parametre kernelu pre IOMMU sa nepodarilo nastaviť automaticky. Nastavte ich ručne a reštartujte.", "Could not convert the OCI rootfs to privileged": "Nepodarilo sa previesť OCI rootfs na privilegované", "Could not copy the PVE keyfile into place. Check permissions on:": "PVE keyfile sa nepodarilo skopírovať na správne miesto. Skontrolujte oprávnenia na:", @@ -1430,7 +1430,7 @@ "Could not unmount the container filesystem:": "Nepodarilo sa odpojiť súborový systém kontajnera:", "Could not unmount — disk may be busy. Removing fstab entry anyway.": "Nepodarilo sa odpojiť - disk sa možno používa. Záznam z fstab aj tak odstraňujem.", "Could not update config file.": "Konfiguračný súbor sa nepodarilo aktualizovať.", - "Could not verify removal of the managed host firewall rule.": "Nepodarilo sa overiť odstránenie pravidla brány firewall spravovaného hostiteľa.", + "Could not verify removal of the managed host firewall rule.": "Nepodarilo sa overiť odstránenie spravovaného pravidla firewallu hostiteľa.", "Could not write to:": "Nepodarilo sa zapísať do:", "Crafty Controller default login": "Crafty Controller predvolené prihlásenie", "Create Directory": "Vytvoriť priečinok", @@ -4840,8 +4840,8 @@ "Private network kept, because other guests still use it:": "Súkromná sieť zachovaná, pretože ju stále používajú iné VM alebo CT:", "Private network of the application released:": "Súkromná sieť uvoľnenej žiadosti:", "Private network of the application that is released:": "Súkromná sieť uvoľnenej aplikácie:", - "Private network release attempted:": "Pokus o vydanie súkromnej siete:", - "Private network targeted for release if no other guest uses it:": "Súkromná sieť zacielená na vydanie, ak ju nepoužíva žiadny iný hosť:", + "Private network release attempted:": "Pokus o uvoľnenie súkromnej siete:", + "Private network targeted for release if no other guest uses it:": "Súkromná sieť určená na uvoľnenie, ak ju nepoužíva iná VM alebo iný CT:", "Private network:": "Súkromná sieť:", "Private personal knowledge management": "Súkromné riadenie osobných znalostí", "Privileged": "Privilegovaný", @@ -5142,7 +5142,7 @@ "Remote server via SSH (recommended — off-host, dedup across machines)": "Vzdialený server cez SSH (odporúčané - mimo hosta, deduplikácia medzi strojmi)", "Remote verified:": "Diaľkové overenie:", "Remounting CIFS share with open permissions...": "Znovu pripájam CIFS zdieľanie s otvorenými oprávneniami...", - "Removal command finished; review any warnings above.": "Príkaz na odstránenie je dokončený;skontrolujte všetky vyššie uvedené upozornenia.", + "Removal command finished; review any warnings above.": "Príkaz na odstránenie sa dokončil; skontrolujte všetky upozornenia vyššie.", "Remove CIFS Mount": "Odstrániť CIFS mount", "Remove CIFS Mount (pvesm or fstab)": "Odstrániť CIFS mount (pvesm alebo fstab)", "Remove CIFS Storage": "Odstrániť CIFS úložisko", @@ -5189,13 +5189,13 @@ "Remove passthrough device from VM": "Odstrániť passthrough zariadenie z VM", "Remove saved credentials": "Odstrániť uložené prihlasovacie údaje", "Remove subscription banner": "Odstrániť hlásenie o predplatnom", - "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Odstrániť aplikáciu? Jeho kontajnerové disky sa vymažú a iba záloha ich môže vrátiť späť.", + "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Odstrániť aplikáciu? Jej kontajnerové disky sa vymažú a obnoviť ich možno iba zo zálohy.", "Remove the lock left on a VM by an interrupted task": "Odstrániť zámok, ktorý na VM zanechala prerušená úloha", "Remove the lock left on a container by an interrupted task": "Odstrániť zámok, ktorý na kontajneri zanechala prerušená úloha", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Odstrániť uložené prihlasovacie údaje pre {registry}?\nSpustené aplikácie budú naďalej fungovať.", "Remove the unprivileged flag from configuration:": "Odstrániť príznak neprivilegovaného kontajnera z nastavenia:", "Remove unused packages and their config": "Odstrániť nepoužívané balíky aj ich nastavenia", - "Remove: delete the application and its containers": "Odstrániť: odstrániť aplikáciu a jej obaly", + "Remove: delete the application and its containers": "Odstrániť: zmazať aplikáciu a jej kontajnery", "Removed": "Odstránené", "Removed KVM MSR options from configuration": "Možnosti KVM MSR boli odstránené z nastavenia", "Removed Mount:": "Odstránený mount:", @@ -6320,7 +6320,7 @@ "The VM guest will have exclusive access to the GPU.": "Hosťovská VM bude mať výhradný prístup ku GPU.", "The VM is powered on. Turn it off before adding disks.": "VM je zapnutá. Pred pridaním diskov ju vypnite.", "The VM/LXC will lose access to this disk after formatting.": "VM/LXC po formátovaní stratí prístup k tomuto disku.", - "The VMID belongs to another container now and is not touched:": "VMID teraz patrí do inej nádoby a nedotýka sa:", + "The VMID belongs to another container now and is not touched:": "VMID teraz patrí inému kontajneru; ten zostane nedotknutý:", "The VMID or its contract is already in use; it is not adopted": "VMID alebo jeho zmluva sa už používa; nie je prijatá", "The VMID was reused or its identity is unknown; the operation is blocked": "VMID bol opätovne použitý alebo jeho identita nie je známa; operation je zablokovaný", "The VMID was reused or the container is on another node; it is not overwritten": "VMID bol opätovne použitý alebo kontajner je na inom uzle; nie je prepísaný", @@ -6413,7 +6413,7 @@ "The container is currently stopped. Do you want to start it now to install the package?": "Kontajner je momentálne zastavený. Chcete ho teraz spustiť, aby sa balík nainštaloval?", "The container is not modified because a host directory is not available:": "Kontajner nie je modifikovaný, pretože adresár hostiteľa nie je k dispozícii:", "The container is restarted to apply the changes. Its own data, the database and the other containers of the application are not touched.": "Kontajner sa reštartuje, aby sa zmeny uplatnili. Jeho vlastné údaje, databáza a ostatné kontajnery aplikácie nie sú dotknuté.", - "The container no longer exists:": "Obal už neexistuje:", + "The container no longer exists:": "Kontajner už neexistuje:", "The container of a member was replaced; the assembly is not resumed": "Zásobník člena bol nahradený; zhromaždenie sa neobnovuje", "The container runs on another node of the cluster; migrate it back to this node to remove it:": "Kontajner beží na inom uzle klastra; migrujte ho späť do tohto uzla, aby ste ho odstránili:", "The container should now start as privileged": "Kontajner by sa teraz mal spustiť ako privilegovaný", @@ -7191,7 +7191,7 @@ "Until they are registered again they cannot be updated or managed from here. The recovery checks first that everything they need is on this host; an application that lacks something is not registered.": "Kým nie sú znova zaregistrované, nemožno ich aktualizovať ani spravovať odtiaľto. Obnova najprv skontroluje, či všetko, čo potrebujú, je na tomto hostiteľovi; aplikácia, ktorej niečo chýba, nie je zaregistrovaná.", "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.": "Kým nie sú znova zaregistrované, nemožno ich aktualizovať ani spravovať odtiaľto. Obnova najprv skontroluje, čo bráni ich registrácii; aplikácia s takýmto problémom nie je zaregistrovaná. Pripojenie Rclone, ktoré je potrebné znova povoliť, tomu nebráni, ale aplikácie sa potom nespúšťajú automaticky.", "Untrusted or modified NVIDIA hook": "Nespoľahlivý alebo modifikovaný háčik NVIDIA", - "Unused image removed from the cache:": "Nevyužitý obrázok odstránený z vyrovnávacej pamäte:", + "Unused image removed from the cache:": "Nepoužívaný obraz odstránený z vyrovnávacej pamäte:", "Unused images removed from the cache:": "Nepoužité obrázky odstránené z vyrovnávacej pamäte:", "Update": "Aktualizácia", "Update Available": "Dostupná aktualizácia", @@ -8267,5 +8267,7 @@ "No template is available for '{app_id}'": "Pre aplikáciu '{app_id}' nie je dostupná šablóna", "The application '{app_id}' is not in the index": "Aplikácia '{app_id}' nie je v indexe", "The generated template does not match the schema:": "Vygenerovaná šablóna nezodpovedá schéme:", - "If a newer application image is available, the container is stopped and backed up, and the backup integrity is verified. The container is then updated using the new image.\n\nIf the update fails after the container was changed, ProxMenux attempts to restore the previous installation from the backup. If the operation remains incomplete, select the application in OCI management and choose \"Recover the previous installation\".\n\nHost directories mounted in the container are not restored by the container backup.": "Ak je dostupný novší obraz aplikácie, kontajner sa zastaví, vytvorí sa jeho záloha a overí sa jej integrita. Potom sa kontajner aktualizuje pomocou nového obrazu.\n\nAk aktualizácia po zmene kontajnera zlyhá, ProxMenux sa pokúsi obnoviť predchádzajúcu inštaláciu zo zálohy. Ak operácia zostane nedokončená, v správe OCI vyberte danú aplikáciu a možnosť „Obnoviť predchádzajúcu inštaláciu“.\n\nPripojené adresáre hostiteľa sa zo zálohy kontajnera neobnovujú." + "If a newer application image is available, the container is stopped and backed up, and the backup integrity is verified. The container is then updated using the new image.\n\nIf the update fails after the container was changed, ProxMenux attempts to restore the previous installation from the backup. If the operation remains incomplete, select the application in OCI management and choose \"Recover the previous installation\".\n\nHost directories mounted in the container are not restored by the container backup.": "Ak je dostupný novší obraz aplikácie, kontajner sa zastaví, vytvorí sa jeho záloha a overí sa jej integrita. Potom sa kontajner aktualizuje pomocou nového obrazu.\n\nAk aktualizácia po zmene kontajnera zlyhá, ProxMenux sa pokúsi obnoviť predchádzajúcu inštaláciu zo zálohy. Ak operácia zostane nedokončená, v správe OCI vyberte danú aplikáciu a možnosť „Obnoviť predchádzajúcu inštaláciu“.\n\nPripojené adresáre hostiteľa sa zo zálohy kontajnera neobnovujú.", + "{application} runs in {count} containers.": "Počet kontajnerov aplikácie {application}: {count}.", + "CT {vmid} is one of the {count} containers of {application}.": "CT {vmid} patrí do aplikácie {application} (počet kontajnerov: {count})." } diff --git a/oci/src/proxmenux_oci/management.py b/oci/src/proxmenux_oci/management.py index 91223b11..100f60d9 100644 --- a/oci/src/proxmenux_oci/management.py +++ b/oci/src/proxmenux_oci/management.py @@ -564,13 +564,15 @@ def _removal_summary(project, vmid): bridge = oci_remove.private_bridge(primary) text = [] if len(members) > 1 and vmid == primary_id: - text += [f"{application} {translate('runs in')} {len(members)} {translate('containers')}. " - f"{translate('All of them are targeted for removal.')}", ''] + sentence = translate('{application} runs in {count} containers.').format( + application=application, count=len(members)) + text += [f"{sentence} {translate('All of them are targeted for removal.')}", ''] elif len(members) > 1: alone = translate('It cannot be removed on its own, because the application would stop ' 'working: continuing targets the whole application for removal.') - text += [f"CT {vmid} {translate('is one of the')} {len(members)} " - f"{translate('containers of')} {application}. {alone}", ''] + sentence = translate('CT {vmid} is one of the {count} containers of {application}.').format( + vmid=vmid, count=len(members), application=application) + text += [f"{sentence} {alone}", ''] text += [translate('Containers targeted for removal:'), *lines, '', translate('Container data targeted for deletion:'), *volumes] if bridge and not oci_remove.bridge_in_use(bridge, set(members)):