diff --git a/.github/scripts/tests/test_pbs_cli_localization.py b/.github/scripts/tests/test_pbs_cli_localization.py new file mode 100644 index 00000000..bda1064b --- /dev/null +++ b/.github/scripts/tests/test_pbs_cli_localization.py @@ -0,0 +1,250 @@ +"""PBS CLI language and no-effect boundaries using real functions, isolated files.""" +import contextlib +import copy +import importlib.util +import io +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[3] + + +def function(path, name): + text = (ROOT/path).read_text() + start = text.index(name+'() {') + return text[start:text.index('\n}\n', start)+3] + + +def repository(components, suite='trixie'): + return dict(Types=['deb'], URIs=['https://credential.invalid/private'], Suites=[suite], + Components=components.split(), Enabled=True) + + +class RepositoryCliTests(unittest.TestCase): + def run_policy(self, case, language='sk', mode='plan', malformed=None, product='pbs'): + spec = importlib.util.spec_from_file_location('policy_fixture', ROOT/'scripts/global/repository_policy.py') + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + state = dict(files=[dict(path='/etc/apt/sources.list.d/pbs.sources', repositories=[repository('pbs-enterprise')]), + dict(path='/etc/apt/sources.list.d/debian.sources', repositories=[repository('main')])], + errors=[], digest='fixture', **{'standard-repos':[{'handle':'no-subscription'}]}) + status='notfound'; calls=[] + if case=='inventory': state['errors']=['PRIVATE RAW DIAGNOSTIC'] + if case=='status': status='unknown' + if case=='suite': state['files'][0]['repositories'][0]['Suites']=['bookworm'] + if case=='active': status='active'; state['files']=state['files'][1:] + if case=='debian': state['files']=state['files'][:1] + if case=='mixed': state['files'][0]['repositories'][0]['Components'].append('main') + if case=='handle': state['standard-repos']=[] + if case=='preserve': state['files'][0]['repositories'][0]['Components']=['pbs-no-subscription'] + original=copy.deepcopy(state) + def run(args): + calls.append(args) + if args[0]=='proxmox-backup-manager': return json.dumps(dict(status=status, serverid='PRIVATE SERVER ID')) + if 'create' in args: + state['files'][0]['repositories'][0]['Enabled']=False + if case=='partial': state['errors']=['PRIVATE RAW DIAGNOSTIC'] + return '' + if 'set' in args: + state['files'].append(dict(path='/etc/apt/sources.list.d/public.sources',repositories=[repository('pbs-no-subscription')])) + return '' + return json.dumps(state) + with tempfile.TemporaryDirectory(prefix='pbs-policy-i18n-') as folder: + module.BASE=Path(folder) + (module.BASE/'lang').mkdir() + (module.BASE/'config.json').write_text(json.dumps({'language':language})) + catalog=(ROOT/'lang/sk.json').read_text() if malformed is None else malformed + (module.BASE/'lang/sk.json').write_text(catalog) + stdout,stderr=io.StringIO(),io.StringIO() + suite='sid' if case=='unsupported' else 'trixie' + with patch.object(module,'run',side_effect=run), patch.object(module.sys,'argv',['repository_policy.py',mode,suite,product]), contextlib.redirect_stdout(stdout),contextlib.redirect_stderr(stderr): + result=module.main() + return result,stdout.getvalue(),stderr.getvalue(),calls,state,original + + def test_pbs_errors_are_localized_without_changing_decisions_or_exposing_raw_data(self): + expectations={'inventory':'Zoznam repozitárov APT', 'status':'Stav predplatného', + 'suite':'iné vydanie', 'active':'aktívne predplatné', 'debian':'základný repozitár Debianu', + 'mixed':'spoločný záznam APT', 'handle':'no-subscription', 'unsupported':'Nepodporované vydanie', + 'partial':'stav repozitárov je neznámy'} + for case,expected in expectations.items(): + with self.subTest(case=case): + result,out,error,calls,state,original=self.run_policy(case,mode='apply' if case=='partial' else 'plan') + self.assertEqual(result,1); self.assertEqual(out,''); self.assertIn(expected,error) + self.assertNotIn('PRIVATE',error); self.assertNotIn('credential.invalid',error) + writes=[c for c in calls if 'create' in c or 'set' in c] + if case=='partial': + self.assertEqual(len(writes),1) + self.assertFalse(state['files'][0]['repositories'][0]['Enabled']) + self.assertIn('Automatický návrat',error) + else: + self.assertEqual(writes,[]); self.assertEqual(state,original) + if case=='unsupported': self.assertEqual(calls,[]) + + def test_english_error_and_stdout_tokens_remain_compatible(self): + en=self.run_policy('unsupported',language='en') + self.assertEqual(en[2],'Unsupported Proxmox suite; no repository changed.\n') + pve=self.run_policy('unsupported',product='pve') + self.assertEqual(pve[2],en[2]) + for mode,expected in [('plan','offer\n'),('apply','changed\n')]: + result,out,error,*_=self.run_policy('offer',mode=mode) + self.assertEqual((result,out,error),(0,expected,'')) + result,out,error,calls,*_=self.run_policy('preserve',mode='apply') + self.assertEqual((result,out,error),(0,'preserve\n','')) + self.assertFalse(any(c[0]=='proxmox-backup-manager' or 'set' in c or 'create' in c for c in calls)) + + def test_bad_or_missing_translation_falls_back_to_original_english(self): + for catalog in ('{}','not json','[]',json.dumps({'Unsupported Proxmox suite; no repository changed.':42}), + json.dumps({'Unsupported Proxmox suite; no repository changed.':'{unknown}'})): + with self.subTest(catalog=catalog): + result,out,error,calls,*_=self.run_policy('unsupported',malformed=catalog) + self.assertEqual(result,1);self.assertEqual(out,'');self.assertEqual(calls,[]) + self.assertEqual(error,'Unsupported Proxmox suite; no repository changed.\n') + + def test_translation_cannot_drop_actual_repository_details(self): + spec=importlib.util.spec_from_file_location('policy_format_fixture',ROOT/'scripts/global/repository_policy.py') + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) + with tempfile.TemporaryDirectory(prefix='pbs-policy-format-') as folder: + module.BASE=Path(folder);(module.BASE/'lang').mkdir() + (module.BASE/'config.json').write_text('{"language":"sk"}') + error=module.PolicyError('Inspect {ui}; no repository changed.',ui='Administration > Repositories') + for invalid in ('Skontrolujte repozitáre.','{ui.__class__}','{other}','{ui:.0}','{ui!r}'): + (module.BASE/'lang/sk.json').write_text(json.dumps({error.template:invalid})) + self.assertEqual(module.cli_error(error,'pbs'),str(error)) + for invalid in ('not json','[]','{"language":"../../private"}'): + (module.BASE/'config.json').write_text(invalid) + self.assertEqual(module.cli_error(error,'pbs'),str(error)) + + +UPDATE_HARNESS=r''' +BASE_DIR="$FIXTURE" +LANG_DIR="$REPO/lang" +SCRIPT_TITLE='fixture' +TAB='' BGN='' CL='' GN='' BL='' +mkdir -p "$FIXTURE/log" "$FIXTURE/tmp" "$FIXTURE/boot" "$FIXTURE/run" +: > "$FIXTURE/calls" +record() { printf '%s\n' "$*" >> "$FIXTURE/calls"; } +pmx_journal_context() { :; } +pmx_is_pbs() { [[ "$CASE" != pve ]]; } +pmx_platform_field() { [[ "$1" != supported ]] && printf '%s' "$SUITE" || printf true; } +ensure_pbs_repositories() { record repository-plan; return 0; } +dpkg-query() { [[ "$SUITE" == bookworm ]] && printf 3.4.0 || printf 4.0.1; } +apt-cache() { record apt-cache "$@"; printf ' Candidate: 4.0.2\n'; } +df() { printf 'Filesystem 1K-blocks Used Available Use%% Mounted\nfixture 10000 1000 %s 10%% /\n' "$([[ "$CASE" == disk ]] && echo 512000 || echo 4096000)"; } +apt-get() { record apt-get "$@"; [[ "$CASE" != refresh ]]; } +apt() { + record apt "$@" + if [[ "$*" == 'list --upgradable' ]]; then + printf 'Listing...\n' + [[ "$CASE" == current ]] || printf 'fixture/trixie 1.2 amd64 [upgradable from: 1.1]\n' + return 0 + fi + [[ "$CASE" != failure ]] +} +fixture_env() { "$@"; } +APT_ENV='fixture_env' +pmx_format_upgradable() { cat; } +pmx_record_execution() { record journal "$1"; } +pmx_dpkg_changes_since() { :; } +msg_error() { printf '%s\n' "$*"; } +msg_ok() { printf '%s\n' "$*"; } +msg_info2() { printf '%s\n' "$*"; } +msg_warn() { printf '%s\n' "$*"; } +msg_title() { :; } +clear() { :; } +show_proxmenux_logo() { :; } +whiptail() { printf '%s\n' "$*" >> "$FIXTURE/dialog"; [[ "$CASE" != cancel ]]; } +uname() { printf old-kernel; } +touch "$FIXTURE/boot/vmlinuz-new-kernel" +''' + + +class UpdateCliTests(unittest.TestCase): + def run_update(self,case,language='sk',suite='trixie'): + with tempfile.TemporaryDirectory(prefix='pbs-update-i18n-') as folder: + body=function('scripts/global/update-pbs.sh','update_pbs') + for old,new in [('/var/log/',folder+'/log/'),('/tmp/proxmenux_screen_capture_',folder+'/tmp/proxmenux_screen_capture_'),('/var/run/',folder+'/run/'),('/boot/',folder+'/boot/')]: body=body.replace(old,new) + script=UPDATE_HARNESS+function('scripts/utils.sh','translate')+body+'\nupdate_pbs\n' + result=subprocess.run(['bash','-c',script],capture_output=True,text=True,timeout=15, + env={**os.environ,'REPO':str(ROOT),'FIXTURE':folder,'CASE':case,'LANGUAGE':language,'SUITE':suite}) + files={str(p.relative_to(folder)):p.read_text() for p in Path(folder).rglob('*') if p.is_file()} + return result,files + + def test_pve_guard_cannot_reach_repository_or_apt_calls(self): + result,files=self.run_update('pve') + self.assertEqual(result.returncode,1,result.stderr) + self.assertEqual(files['calls'],'') + self.assertFalse(any(name.startswith('log/') for name in files)) + + def test_disk_and_upgrade_errors_keep_their_actual_parameters(self): + result,files=self.run_update('disk') + self.assertEqual(result.returncode,1,result.stderr) + self.assertIn('Nedostatok miesta na disku. Dostupné:',result.stdout) + self.assertIn('500MB',result.stdout) + self.assertNotIn('apt',files['calls']) + result,files=self.run_update('failure') + self.assertEqual(result.returncode,1,result.stderr) + self.assertIn('Aktualizácia systému zlyhala. Skontrolujte denník:',result.stdout) + self.assertIn('/log/proxmox-update-',result.stdout) + self.assertNotIn('autoremove',files['calls']) + + def test_update_status_cancel_refresh_error_and_reboot_notice(self): + for suite in ('bookworm','trixie'): + for case in ('current','cancel','refresh','success'): + with self.subTest(suite=suite,case=case): + result,files=self.run_update(case,suite=suite) + self.assertEqual(result.returncode,1 if case=='refresh' else 0,result.stderr) + if case!='success': self.assertNotIn('full-upgrade',files['calls']) + if case=='success': self.assertIn('reštartujte server',result.stdout) + if case in ('current','cancel'): self.assertIn('Aktuálna verzia PBS',files['dialog']) + + +class DialogTests(unittest.TestCase): + def test_automated_confirmation_and_banner_cancel_do_not_run_actions(self): + cases=[('scripts/menus/menu_post_install.sh','confirm_automated_script', + ['repozitár no-subscription','upozornenie na predplatné','pamäť','jadro','sieť']), + ('scripts/post_install/auto_post_install.sh','remove_subscription_banner', + ['Proxmox Backup Server – odstránenie upozornenia na predplatné'])] + for path,name,expected in cases: + with tempfile.TemporaryDirectory(prefix='pbs-dialog-') as folder: + script=function('scripts/utils.sh','translate')+function(path,name)+r''' +PMX_ON_PBS=true +LOCAL_SCRIPTS='/fixture/scripts' +clear() { :; }; sleep() { :; }; stop_spinner() { :; } +msg_warn() { printf '%s\n' "$*"; } +bash() { printf forbidden >> "$CAPTURE/actions"; } +register_tool() { printf forbidden >> "$CAPTURE/actions"; } +dialog() { printf '%s\n' "$*" >> "$CAPTURE/dialog"; return 1; } +whiptail() { dialog "$@"; } +'''+name+'\n' + result=subprocess.run(['bash','-c',script],capture_output=True,text=True,timeout=15, + env={**os.environ,'LANGUAGE':'sk','LANG_DIR':str(ROOT/'lang'),'CAPTURE':folder}) + self.assertEqual(result.returncode,1 if name=='remove_subscription_banner' else 0,result.stderr) + dialog=(Path(folder)/'dialog').read_text() + for text in expected:self.assertIn(text,dialog) + self.assertFalse((Path(folder)/'actions').exists()) + if name=='remove_subscription_banner':self.assertIn('bolo zrušené používateľom',result.stdout) + + def test_community_labels_preserve_dispatch_names_and_translate_display(self): + script=function('scripts/utils.sh','translate')+function('scripts/menus/menu_post_install.sh','post_install_label')+'\npost_install_label "$1"\n' + for text,expected in [('Proxmox Backup Server Post Install','nastavenie po inštalácii'), + ('Proxmox Backup Server Microcode','mikrokód procesora')]: + result=subprocess.run(['bash','-c',script,'bash',text],capture_output=True,text=True,timeout=15, + env={**os.environ,'LANGUAGE':'sk','LANG_DIR':str(ROOT/'lang')}) + self.assertEqual(result.returncode,0,result.stderr);self.assertIn(expected,result.stdout) + + def test_failed_apt_hook_check_removes_only_fixture_hook_with_localized_warning(self): + with tempfile.TemporaryDirectory(prefix='pbs-hook-') as folder: + script=function('scripts/utils.sh','translate')+function('scripts/global/remove-banner-pve-v3.sh','create_apt_hook')+r''' +apt-config() { return 1; } +msg_warn() { printf '%s\n' "$*"; } +create_apt_hook +''' + result=subprocess.run(['bash','-c',script],capture_output=True,text=True,timeout=15, + env={**os.environ,'LANGUAGE':'sk','LANG_DIR':str(ROOT/'lang'),'APT_HOOK':folder+'/hook'}) + self.assertEqual(result.returncode,0,result.stderr) + self.assertIn('Nesprávna syntax hooku APT',result.stdout) + self.assertFalse((Path(folder)/'hook').exists()) diff --git a/.github/scripts/tests/test_terminal_catalogs.py b/.github/scripts/tests/test_terminal_catalogs.py index f74966cb..a69aedd8 100644 --- a/.github/scripts/tests/test_terminal_catalogs.py +++ b/.github/scripts/tests/test_terminal_catalogs.py @@ -69,13 +69,43 @@ class TerminalCatalogsTests(unittest.TestCase): def test_shipped_active_catalogs_keep_literal_contracts(self): result = subprocess.run( [sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'), - '--root', str(ROOT), '--require-complete'], + '--root', str(ROOT)], capture_output=True, text=True, timeout=60, ) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) for locale in MODULE.DEFAULT_LANGUAGES: self.assertIn(f'[{locale}] active=', result.stdout) - self.assertNotIn('missing=1', result.stdout) + # New hand-reviewed Slovak keys ship with their source changes; + # other locales may use the runtime English fallback until translated. + result = subprocess.run( + [sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'), + '--root', str(ROOT), '--languages', 'sk', '--require-complete'], + capture_output=True, text=True, timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn('missing=0', result.stdout) + + def test_optional_completeness_still_rejects_invalid_present_translations(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / 'scripts').mkdir() + (root / 'lang').mkdir() + (root / 'scripts/check.sh').write_text('translate "Check /etc/pve/storage.cfg"') + (root / 'lang/sk.json').write_text(json.dumps({'Check /etc/pve/storage.cfg': 'Skontrolujte /etc/pve/storage.cfg'})) + (root / 'lang/es.json').write_text('{}') + + def check(languages, *flags): + return subprocess.run( + [sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'), + '--root', str(root), '--languages', languages, *flags], + capture_output=True, text=True, timeout=60, + ) + + self.assertEqual(check('es,sk').returncode, 0) + self.assertEqual(check('sk', '--require-complete').returncode, 0) + self.assertNotEqual(check('es', '--require-complete').returncode, 0) + (root / 'lang/es.json').write_text(json.dumps({'Check /etc/pve/storage.cfg': 'Compruebe /etc/PVE/storage.cfg'})) + self.assertNotEqual(check('es,sk').returncode, 0) if __name__ == '__main__': diff --git a/.github/scripts/tests/test_zram_localization.py b/.github/scripts/tests/test_zram_localization.py new file mode 100644 index 00000000..bf9bc768 --- /dev/null +++ b/.github/scripts/tests/test_zram_localization.py @@ -0,0 +1,168 @@ +"""Render the real ZRAM CLI paths in isolated files with host effects stubbed.""" +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[3] +DETAILS = { + "version": ("Cannot determine installed systemd-zram-generator version", "Nepodarilo sa zistiť verziu nainštalovaného balíka systemd-zram-generator"), + "mismatch": ("Existing ZRAM profile is active but does not match its configuration", "Existujúci profil ZRAM je aktívny, ale nezodpovedá svojej konfigurácii"), + "stop": ("Could not stop existing dev-zram0.swap", "Nepodarilo sa zastaviť existujúcu jednotku dev-zram0.swap"), + "directory": ("Could not create ZRAM generator directory", "Nepodarilo sa vytvoriť adresár pre konfiguráciu generátora ZRAM"), + "config": ("Could not write ZRAM generator configuration", "Nepodarilo sa zapísať konfiguráciu generátora ZRAM"), + "marker": ("Could not write ZRAM package ownership state", "Nepodarilo sa zapísať údaj o tom, či balík ZRAM nainštaloval ProxMenux"), + "start": ("Could not activate dev-zram0.swap", "Nepodarilo sa aktivovať jednotku dev-zram0.swap"), + "verify": ("Active ZRAM algorithm, capacity or swap sources did not match the profile", "Algoritmus kompresie, veľkosť alebo zdroje swapu aktívnej ZRAM nezodpovedajú profilu"), +} +REFUSAL = "Another ZRAM generator configuration exists; refusing to overwrite it:" +REFUSAL_SK = "Existuje iná konfigurácia generátora ZRAM; neprepíše sa:" + + +def function(path, name): + text = path.read_text() + start = text.index(name + "() {") + return text[start:text.index("\n}\n", start) + 3] + + +HARNESS = r''' +set -u +BASE_DIR="$FIXTURE/state" +LANG_DIR="$REPO/lang" +mkdir -p "$BASE_DIR" "$FIXTURE/etc/systemd/zram-generator.conf.d" "$FIXTURE/proc" +CONFIG="$FIXTURE/etc/systemd/zram-generator.conf.d/90-proxmenux.conf" +MARKER="$BASE_DIR/zram-swap.state" +printf 'Filename\tType\tSize\tUsed\tPriority\n' > "$FIXTURE/proc/swaps" +printf '/swapfile none swap sw 0 0\n' > "$FIXTURE/etc/fstab" +: > "$FIXTURE/calls" +record() { printf '%s\n' "$*" >> "$FIXTURE/calls"; } +pmx_journal_context() { :; } +msg_error() { printf '%s\n' "$*"; } +msg_success() { printf '%s\n' "$*"; } +register_tool() { record register "$@"; } +pmx_install_pkg() { record package-install; return 1; } +apt-get() { record apt "$@"; return 1; } +dpkg-query() { + record dpkg-query + case "$*" in + *'${Status}'*) printf 'install ok installed\n';; + *'${Version}'*) [[ "$CASE" == version ]] || printf '1.2.0-1\n';; + esac +} +dpkg() { return 0; } +install() { record directory; [[ "$CASE" != directory ]]; } +pmx_write_file() { + record write "$1" + if [[ "$CASE" == config && "$1" == "$CONFIG" ]] || + [[ "$CASE" == marker && "$1" == "$MARKER" ]]; then + cat >/dev/null + return 1 + fi + cat > "$1" +} +pmx_remove_file() { record remove "$1"; rm -f -- "$1"; } +pmx_record_uninstall() { record record-uninstall; } +swapoff() { record swapoff; return 0; } +systemctl() { + record systemctl "$@" + [[ "$CASE" != start || "$1" != start ]] && + [[ "$CASE" != stop || "$1" != stop ]] +} +source "$REPO/scripts/post_install/zram_profile_helpers.sh" +zram_profile_verify_active() { [[ "$CASE" != verify && "$CASE" != mismatch ]]; } +if [[ "$CASE" == mismatch || "$CASE" == stop ]]; then + printf '# ProxMenux ZRAM profile\n[zram0]\nzram-size = ram / 10\n' > "$CONFIG" + [[ "$CASE" != mismatch ]] || zram_render_generator_config true > "$CONFIG" + printf 'installed_by_proxmenux=false\n' > "$MARKER" + printf '/dev/zram0\tpartition\t3355443\t8\t32767\n' >> "$FIXTURE/proc/swaps" +fi +if [[ "$CASE" == foreign ]]; then + printf '[zram1]\nzram-size = ram / 5\n' > "$FIXTURE/etc/systemd/zram-generator.conf" +fi +if [[ "$CASE" == unreadable ]]; then + zram_foreign_generator_config() { return 2; } +fi +''' + + +class ZramLocalizationTests(unittest.TestCase): + def test_missing_optional_translation_uses_original_english(self): + with tempfile.TemporaryDirectory(prefix="zram-fallback-") as folder: + (Path(folder) / 'xx.json').write_text('{}') + script = function(ROOT / "scripts/utils.sh", "translate") + '\ntranslate "$1"\n' + result = subprocess.run(["bash", "-c", script, "bash", DETAILS['version'][0]], + text=True, capture_output=True, timeout=15, + env={**os.environ, 'LANGUAGE': 'xx', 'LANG_DIR': folder}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), DETAILS['version'][0]) + + def run_case(self, case, language="sk"): + with tempfile.TemporaryDirectory(prefix="zram-i18n-") as folder: + directory = Path(folder) + installer = function(ROOT / "scripts/post_install/customizable_post_install.sh", "configure_zram_swap") + installer = installer.replace("/etc/systemd/zram-generator.conf.d/90-proxmenux.conf", folder + "/etc/systemd/zram-generator.conf.d/90-proxmenux.conf") + installer = installer.replace("/proc/swaps", folder + "/proc/swaps").replace("/etc/fstab", folder + "/etc/fstab") + installer = installer.replace('zram_foreign_generator_config "$config"', 'zram_foreign_generator_config "$config" "$FIXTURE"') + installer = installer.replace('zram_list_foreign_systemd_zram_units)', 'zram_list_foreign_systemd_zram_units "$FIXTURE")') + script = HARNESS + function(ROOT / "scripts/utils.sh", "translate") + installer + "\nconfigure_zram_swap\n" + result = subprocess.run(["bash", "-c", script], text=True, capture_output=True, + env={**os.environ, "REPO": str(ROOT), "FIXTURE": folder, "CASE": case, "LANGUAGE": language}, timeout=15) + calls = (directory / "calls").read_text().splitlines() + files = {str(p.relative_to(directory)): p.read_text() for p in directory.rglob("*") if p.is_file()} + return result, calls, files + + def test_all_activation_details_are_localized_and_english_is_preserved(self): + for case, (english, slovak) in DETAILS.items(): + for language, expected in (("sk", slovak), ("en", english)): + with self.subTest(case=case, language=language): + result, calls, files = self.run_case(case, language) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn(expected, result.stdout) + self.assertEqual(files["etc/fstab"], "/swapfile none swap sw 0 0\n") + if case in ("version", "mismatch"): + self.assertTrue(all(call == "dpkg-query" for call in calls), calls) + elif case == "stop": + self.assertEqual(files["state/zram-swap.state"], "installed_by_proxmenux=false\n") + self.assertIn("# ProxMenux ZRAM profile", files["etc/systemd/zram-generator.conf.d/90-proxmenux.conf"]) + else: + self.assertNotIn("state/zram-swap.state", files) + self.assertNotIn("etc/systemd/zram-generator.conf.d/90-proxmenux.conf", files) + self.assertFalse(any(call.startswith(("apt ", "package-install", "register ")) for call in calls), calls) + + def test_foreign_generator_is_refused_in_both_languages_without_any_effect(self): + for language, expected in (("sk", REFUSAL_SK), ("en", REFUSAL)): + result, calls, files = self.run_case("foreign", language) + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn(expected, result.stdout) + self.assertIn("/etc/systemd/zram-generator.conf", result.stdout) + self.assertEqual(calls, []) + self.assertEqual(files["etc/systemd/zram-generator.conf"], "[zram1]\nzram-size = ram / 5\n") + self.assertNotIn("state/zram-swap.state", files) + self.assertNotIn("etc/systemd/zram-generator.conf.d/90-proxmenux.conf", files) + self.assertEqual(files["etc/fstab"], "/swapfile none swap sw 0 0\n") + self.assertEqual(files["proc/swaps"], "Filename\tType\tSize\tUsed\tPriority\n") + + def test_failed_inspection_is_not_mistaken_for_no_conflict(self): + result, calls, files = self.run_case("unreadable") + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("Nie je možné skontrolovať existujúcu konfiguráciu generátora ZRAM", result.stdout) + self.assertEqual(calls, []) + self.assertNotIn("state/zram-swap.state", files) + self.assertNotIn("etc/systemd/zram-generator.conf.d/90-proxmenux.conf", files) + + def test_uninstall_label_and_cancel_do_not_call_uninstall(self): + for language, label in (("sk", "Profil swapu ZRAM"), ("en", "ZRAM swap profile")): + with tempfile.TemporaryDirectory(prefix="zram-menu-") as folder: + tools = Path(folder) / "tools.json" + tools.write_text('{"zram_swap":true}') + script = function(ROOT / "scripts/utils.sh", "translate") + script += function(ROOT / "scripts/post_install/uninstall-tools.sh", "show_uninstall_menu") + script += '\nensure_tools_json() { :; }; migrate_installed_tools() { :; }\nuninstall_zram_swap() { printf forbidden; return 1; }\ndialog() { printf "%s\\n" "$@" > "$MENU_CAPTURE"; return 1; }\nshow_uninstall_menu\n' + result = subprocess.run(["bash", "-c", script], text=True, capture_output=True, + env={**os.environ, "LANGUAGE": language, "LANG_DIR": str(ROOT / "lang"), "TOOLS_JSON": str(tools), "MENU_CAPTURE": folder + "/dialog.txt"}, timeout=15) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(label, (Path(folder) / "dialog.txt").read_text()) + self.assertNotIn("forbidden", result.stdout + result.stderr) + self.assertEqual(json.loads(tools.read_text()), {"zram_swap": True}) diff --git a/lang/sk.json b/lang/sk.json index fe1f96ce..5e34a7ec 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -1,4 +1,25 @@ { + "APT hook syntax issue, removing...": "Nesprávna syntax hooku APT; hook sa odstráni...", + "Banner removal cancelled by user.": "Odstránenie upozornenia na predplatné bolo zrušené používateľom.", + "Could not complete or verify the repository switch; changes may be partial and repository state is unknown. Inspect {ui} before retrying; no automatic rollback was attempted.": "Nepodarilo sa dokončiť alebo overiť zmenu repozitárov; zmeny môžu byť čiastočné a stav repozitárov je neznámy. Pred ďalším pokusom skontrolujte {ui}. Automatický návrat do pôvodného stavu sa nevykonal.", + "Enterprise shares an APT stanza with other components. Split it in the Proxmox repository UI; no repository changed.": "Enterprise má spoločný záznam APT s ďalšími komponentmi. Rozdeľte ho v rozhraní repozitárov Proxmoxu; žiadny repozitár sa nezmenil.", + "Host has an active subscription but no active {short} repository. Configure its Enterprise source in {ui}; no repository changed.": "Server má aktívne predplatné, ale nemá aktívny repozitár {short}. Nastavte zdroj Enterprise v {ui}; žiadny repozitár sa nezmenil.", + "Insufficient disk space. Available:": "Nedostatok miesta na disku. Dostupné:", + "No active Debian base repository for this suite; configure it in the Proxmox repository UI before continuing.": "Pre toto vydanie nie je aktívny základný repozitár Debianu; pred pokračovaním ho nastavte v rozhraní repozitárov Proxmoxu.", + "Proxmox APT repository inventory is invalid or reports parse errors. Fix sources in {ui}; no repository changed.": "Zoznam repozitárov APT Proxmoxu je neplatný alebo obsahuje chyby načítania. Opravte zdroje v {ui}; žiadny repozitár sa nezmenil.", + "Proxmox Backup Server Microcode": "Proxmox Backup Server – mikrokód procesora", + "Proxmox Backup Server Post Install": "Proxmox Backup Server – nastavenie po inštalácii", + "Proxmox Backup Server Subscription Banner Removal": "Proxmox Backup Server – odstránenie upozornenia na predplatné", + "Proxmox no-subscription standard repository handle unavailable; no repository changed.": "Štandardný repozitár Proxmoxu no-subscription nie je dostupný; žiadny repozitár sa nezmenil.", + "Subscription status is not unambiguously active, notfound or expired. Check proxmox-backup-manager subscription get locally; no repository changed.": "Stav predplatného nie je jednoznačne active, notfound alebo expired. Overte ho lokálne príkazom proxmox-backup-manager subscription get; žiadny repozitár sa nezmenil.", + "System upgrade failed. Check log:": "Aktualizácia systému zlyhala. Skontrolujte denník:", + "This host runs {name} on {suite}, but these {name} repositories are for another suite: {details}. Disable or remove them in {ui} and run the update again; no repository changed.": "Na serveri beží {name} na vydaní {suite}, ale tieto repozitáre {name} sú pre iné vydanie: {details}. Vypnite alebo odstráňte ich v {ui} a spustite aktualizáciu znova; žiadny repozitár sa nezmenil.", + "Unsupported Proxmox suite; no repository changed.": "Nepodporované vydanie Proxmoxu; žiadny repozitár sa nezmenil.", + "kernel": "jadro", + "memory": "pamäť", + "network": "sieť", + "no-subscription repository": "repozitár no-subscription", + "subscription banner": "upozornenie na predplatné", "# Alternative if you prefer screen": "# Alternatíva, ak preferujete screen", "# Recommended: avoids disconnection during upgrade": "# Odporúčané: zabráni odpojeniu počas aktualizácie", "(Checked entries will be removed. Uncheck to keep in VM.)": "(Označené položky sa odstránia. Zrušte označenie, ak ich chcete ponechať vo VM.)", @@ -88,7 +109,7 @@ "A multi-container application is not recreated: its containers are updated together.": "Aplikácia s viacerými kontajnermi sa nevytvorí znova: jej kontajnery sa aktualizujú spoločne.", "A new ProxMenux version is available:": "Je dostupná nová verzia ProxMenux:", "A new kernel is staged for the next boot:": "Na najbližší štart je pripravený nový kernel:", - "A newer kernel is installed. Reboot before the next backup window to use it.": "Je nainštalované novšie jadro. Aby ste ho mohli použiť, reštartujte ho pred ďalším oknom zálohy.", + "A newer kernel is installed. Reboot before the next backup window to use it.": "Je nainštalované novšie jadro. Pred ďalším plánovaným zálohovaním reštartujte server, aby sa použilo.", "A newer version is available:": "Je dostupná novšia verzia:", "A pending operation exists for": "Čaká na operation existuje", "A pending stack assembly already exists; it is not overwritten": "Nevyriešená zostava stohov už existuje; nie je prepísaná", @@ -210,6 +231,7 @@ "Activate ProxMenux Monitor": "Zapnúť ProxMenux Monitor", "Active": "Aktívne", "Active Connections": "Aktívne pripojenia", + "Active ZRAM algorithm, capacity or swap sources did not match the profile": "Algoritmus kompresie, veľkosť alebo zdroje swapu aktívnej ZRAM nezodpovedajú profilu", "Active exports:": "Aktívne exporty:", "Active session:": "Aktívna relácia:", "Actual device path on the host": "Skutočná dráha zariadenia na hostiteľovi", @@ -412,7 +434,7 @@ "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čí.", "Another ZRAM configuration exists or could not be inspected; systemd-zram-generator was retained": "Existuje iná konfigurácia ZRAM alebo ju nebolo možné skontrolovať; systemd-zram-generator bol zachovaný", - "Another ZRAM generator configuration exists; refusing to overwrite it:": "Existuje iná konfigurácia generátora ZRAM; refusing to overwrite it:", + "Another ZRAM generator configuration exists; refusing to overwrite it:": "Existuje iná konfigurácia generátora ZRAM; neprepíše sa:", "Another ZRAM swap setup exists; refusing to replace it:": "Existuje ďalšie nastavenie výmeny ZRAM; odmietnutie jeho výmeny:", "Another device of the network already answers on this address:": "Na tejto adrese už odpovedá iné zariadenie v sieti:", "Another instance uses": "Iný prípad použitia", @@ -740,6 +762,7 @@ "Cannot find": "Nedá sa nájsť", "Cannot identify the vendor of the device:": "Nie je možné identifikovať predajcu zariadenia:", "Cannot inspect current swap configuration": "Nie je možné skontrolovať aktuálnu konfiguráciu výmeny", + "Cannot determine installed systemd-zram-generator version": "Nepodarilo sa zistiť verziu nainštalovaného balíka systemd-zram-generator", "Cannot inspect existing ZRAM configurations": "Nie je možné skontrolovať existujúce konfigurácie ZRAM", "Cannot inspect existing ZRAM generator configuration": "Nie je možné skontrolovať existujúcu konfiguráciu generátora ZRAM", "Cannot load backup library: lib_host_backup_common.sh": "Nepodarilo sa načítať knižnicu zálohovania: lib_host_backup_common.sh", @@ -1335,6 +1358,8 @@ "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:", "Could not copy the keyfile into place.": "Keyfile sa nepodarilo skopírovať na správne miesto.", "Could not copy the keyfile into place. Check permissions on:": "Keyfile sa nepodarilo skopírovať na správne miesto. Skontrolujte oprávnenia na:", + "Could not activate dev-zram0.swap": "Nepodarilo sa aktivovať jednotku dev-zram0.swap", + "Could not create ZRAM generator directory": "Nepodarilo sa vytvoriť adresár pre konfiguráciu generátora ZRAM", "Could not create a temporary directory for the NVIDIA Container Toolkit.": "Nepodarilo sa vytvoriť dočasný adresár pre NVIDIA Container Toolkit.", "Could not create converter directory:": "Priečinok pre konvertor sa nepodarilo vytvoriť:", "Could not create destination directory:": "Cieľový priečinok sa nepodarilo vytvoriť:", @@ -1423,6 +1448,7 @@ "Could not stage pending restore. Nothing new was scheduled.": "Nepodarilo sa pripraviť čakajúce obnovenie. Nič nové nebolo naplánované.", "Could not stop LXC": "LXC sa nepodarilo zastaviť", "Could not stop ZRAM swap; profile configuration remains": "Nepodarilo sa zastaviť výmenu ZRAM; konfigurácia profilu zostáva zachovaná", + "Could not stop existing dev-zram0.swap": "Nepodarilo sa zastaviť existujúcu jednotku dev-zram0.swap", "Could not translate the Compose command/entrypoint": "Nepodarilo sa preložiť príkaz/bod vstupu", "Could not unload nouveau module (may be in use). The blacklist will take effect after reboot. Installation will continue but a reboot will be required.": "Modul nouveau sa nepodarilo uvoľniť (možno sa práve používa). Blacklist sa prejaví po reštarte. Inštalácia bude pokračovať, ale reštart bude potrebný.", "Could not unmount": "Nepodarilo sa odpojiť", @@ -1431,6 +1457,8 @@ "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 write ZRAM generator configuration": "Nepodarilo sa zapísať konfiguráciu generátora ZRAM", + "Could not write ZRAM package ownership state": "Nepodarilo sa zapísať údaj o tom, či balík ZRAM nainštaloval ProxMenux", "Could not write to:": "Nepodarilo sa zapísať do:", "Crafty Controller default login": "Crafty Controller predvolené prihlásenie", "Create Directory": "Vytvoriť priečinok", @@ -2197,6 +2225,7 @@ "Execution engine for Index-TTS": "Vykonávací motor pre Index-TTS", "Existing Groups": "Existujúce skupiny", "Existing TLS certificate reused:": "Existujúce osvedčenie TLS opätovne použité:", + "Existing ZRAM profile is active but does not match its configuration": "Existujúci profil ZRAM je aktívny, ale nezodpovedá svojej konfigurácii", "Existing file, re-downloading...": "Súbor už existuje, sťahujem ho znova...", "Existing filesystem:": "Existujúci súborový systém:", "Existing hostpci entries detected — they will be reused": "Zistené existujúce hostpci položky - použijú sa znova", @@ -4182,7 +4211,7 @@ "No active Proxmox storage accepts OCI template archives.": "Žiadne aktívne úložisko Proxmox neakceptuje archívy šablón OCI.", "No active exports or showmount not available": "Nie sú aktívne exporty alebo príkaz showmount nie je dostupný", "No active session": "Žiadna aktívna relácia", - "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "Žiadne aktívne predplatné a žiadne použiteľné úložisko PBS.Neinteraktívny režim nemôže zmeniť zdroje APT;nakonfigurujte ich v Administrácia > Repozitáre.", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "Server nemá aktívne predplatné ani použiteľný repozitár PBS. Neinteraktívny režim nemôže meniť zdroje APT; nastavte ich v Administration > Repositories.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "Žiadne aktívne predplatné a žiadne použiteľné úložisko PVE. Neinteraktívny režim nemôže zmeniť zdroje APT; nakonfigurujte ich v časti Uzol > Aktualizácie > Úložiská.", "No additional GPU can be added.": "Nie je možné pridať ďalšiu GPU.", "No additional device needs to be added.": "Netreba pridávať žiadne ďalšie zariadenie.", @@ -5273,7 +5302,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "Pomocník úložiska nie je k dispozícii. Inštalácia NVIDIA sa zastavila.", "Repository policy returned an unexpected result.": "Politika úložiska vrátila neočakávaný výsledok.", "Repository ready.": "Repozitár je pripravený.", - "Repository sources changed, but APT package-list refresh failed. Operation stopped; sources were not rolled back. Inspect Administration > Repositories and retry apt-get update before continuing.": "Zdroje úložiska sa zmenili, ale obnovenie zoznamu balíkov APT zlyhalo. Prevádzka zastavená;zdroje neboli vrátené späť. Pred pokračovaním skontrolujte Administrácia > Repozitáre a zopakujte apt-get update.", + "Repository sources changed, but APT package-list refresh failed. Operation stopped; sources were not rolled back. Inspect Administration > Repositories and retry apt-get update before continuing.": "Zdroje repozitárov sa zmenili, ale obnovenie zoznamu balíkov APT zlyhalo. Operácia sa zastavila; pôvodné zdroje sa neobnovili. Pred pokračovaním skontrolujte Administration > Repositories a zopakujte apt-get update.", "Repository sources changed, but APT package-list refresh failed. Operation stopped; sources were not rolled back. Inspect Node > Updates > Repositories and retry apt-get update before continuing.": "Zdroje úložiska sa zmenili, ale obnovenie zoznamu balíkov APT zlyhalo. Prevádzka zastavená; zdroje neboli vrátené späť. Pred pokračovaním skontrolujte Node > Updates > Repository a zopakujte apt-get update.", "Repository switch declined; no APT source changed.": "Prepnutie úložiska bolo odmietnuté; žiadny zdroj APT sa nezmenil.", "Repository:": "Repozitár:", @@ -7257,7 +7286,7 @@ "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Asistent aktualizácie: Proxmox VE 8 → 9 (Trixie)", "Upgrade canceled by user": "Aktualizácia zrušená používateľom", "Upgrade one node at a time": "Aktualizujte vždy iba jeden uzol naraz", - "Upgrade the system, offering the": "Aktualizujte systém a ponúknite", + "Upgrade the system, offering the": "Aktualizácia systému a prípadný prechod na", "Upgrade to Proxmox VE 9": "Aktualizovať na Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "Aktualizujem NVIDIA nástroje (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "Nahrať šifrovanú kópiu kľúča do PBS, aby ste ju vedeli obnoviť na preinštalovanom hostovi iba pomocou frázy?", @@ -7678,6 +7707,7 @@ "ZRAM profile removed, but package purge failed": "Profil ZRAM bol odstránený, ale vymazanie balíka zlyhalo", "ZRAM still holds pages; configuration was preserved": "ZRAM stále drží stránky; konfigurácia bola zachovaná", "ZRAM still holds pages; profile remains active": "ZRAM stále drží stránky; profil zostáva aktívny", + "ZRAM swap profile": "Profil swapu ZRAM", "ZRAM swap profile enabled": "Swap profil ZRAM povolený", "ZRAM swap profile removed": "Výmenný profil ZRAM bol odstránený", "ZSTD - fast, good ratio (recommended)": "ZSTD - rýchly, dobrý pomer (odporúčané)", diff --git a/scripts/global/remove-banner-pve-v3.sh b/scripts/global/remove-banner-pve-v3.sh index 92df1028..76df07b5 100644 --- a/scripts/global/remove-banner-pve-v3.sh +++ b/scripts/global/remove-banner-pve-v3.sh @@ -312,7 +312,7 @@ EOFAPT # Verify APT hook syntax apt-config dump >/dev/null 2>&1 || { - msg_warn "APT hook syntax issue, removing..." + msg_warn "$(translate "APT hook syntax issue, removing...")" rm -f "$APT_HOOK" } } diff --git a/scripts/global/repository_policy.py b/scripts/global/repository_policy.py index 95f40d68..00829730 100644 --- a/scripts/global/repository_policy.py +++ b/scripts/global/repository_policy.py @@ -7,8 +7,10 @@ repository API responses: they can contain subscription keys or credentials. """ import copy import json +from pathlib import Path import re import subprocess +import string import sys ENDPOINT = '/nodes/localhost/apt/repositories' @@ -26,6 +28,41 @@ PRODUCTS = { 'test': {'pbs-no-subscription', 'pbs-test', 'pbstest'}, 'ui': 'Administration > Repositories', 'ceph': False}, } +BASE = Path(__file__).resolve().parents[2] + + +def N_(text): + """Mark a CLI message without changing the policy's English exception.""" + return text + + +class PolicyError(ValueError): + def __init__(self, template, **params): + self.template = template + self.params = params + super().__init__(template.format(**params)) + + +def cli_error(error, product): + """Localize only PBS CLI diagnostics; policy callers retain English errors.""" + if product != 'pbs' or not isinstance(error, PolicyError): + return str(error) + try: + language = json.loads((BASE / 'config.json').read_text()).get('language', 'en') + if language not in ('sk', 'de', 'es', 'fr', 'it', 'pt', 'sv'): + return str(error) + catalog = json.loads((BASE / 'lang' / f'{language}.json').read_text()) + message = catalog.get(error.template) + if isinstance(message, str) and message.strip(): + fields = lambda text: {name for _, name, _, _ in string.Formatter().parse(text) if name is not None} + if fields(message) != fields(error.template) or any( + spec or conversion for _, _, spec, conversion in string.Formatter().parse(message) + ): + return str(error) + return message.format(**error.params) + except (OSError, ValueError, KeyError, IndexError, TypeError, AttributeError): + pass + return str(error) def run(args): @@ -43,7 +80,7 @@ def subscription_status(product='pve'): except (ValueError, AttributeError): status = None if status not in ('active', 'notfound', 'expired'): - raise ValueError('Subscription status is not unambiguously active, notfound or expired. Check proxmox-backup-manager subscription get locally; no repository changed.') + raise PolicyError(N_('Subscription status is not unambiguously active, notfound or expired. Check proxmox-backup-manager subscription get locally; no repository changed.')) return status # pvesubscription's CLI get printer emits sorted "key: value" lines, # including a secret key and server ID. Only parse the exact status line. @@ -110,12 +147,12 @@ def repository_state(suite, product='pve'): entries.append((file['path'], index, row)) return data, entries except (KeyError, TypeError, ValueError, IndexError) as exc: - raise ValueError(f"Proxmox APT repository inventory is invalid or reports parse errors. Fix sources in {p['ui']}; no repository changed.") from exc + raise PolicyError(N_('Proxmox APT repository inventory is invalid or reports parse errors. Fix sources in {ui}; no repository changed.'), ui=p['ui']) from exc def evaluate(suite, apply=False, product='pve'): if suite not in ('bookworm', 'trixie'): - raise ValueError('Unsupported Proxmox suite; no repository changed.') + raise PolicyError(N_('Unsupported Proxmox suite; no repository changed.')) p = PRODUCTS[product] data, entries = repository_state(suite, product) pve = [] @@ -139,8 +176,8 @@ def evaluate(suite, apply=False, product='pve'): ceph.append((path, index, row)) # Name the stale sources, never their URIs: those can carry credentials. if wrong_pve: - raise ValueError(f"This host runs {p['name']} on {suite}, but these {p['name']} repositories are for another suite: " - f"{'; '.join(wrong_pve)}. Disable or remove them in {p['ui']} and run the update again; no repository changed.") + raise PolicyError(N_('This host runs {name} on {suite}, but these {name} repositories are for another suite: {details}. Disable or remove them in {ui} and run the update again; no repository changed.'), + name=p['name'], suite=suite, details='; '.join(wrong_pve), ui=p['ui']) if wrong_ceph: raise ValueError(f"This host runs Proxmox VE on {suite}, but these Enterprise Ceph repositories are for another suite: " f"{'; '.join(wrong_ceph)}. Correct them in Node > Updates > Repositories before switching; no repository changed.") @@ -152,20 +189,20 @@ def evaluate(suite, apply=False, product='pve'): status = subscription_status(product) # fail closed before any write if status == 'active': if not pve: - raise ValueError(f"Host has an active subscription but no active {p['short']} repository. Configure its Enterprise source in {p['ui']}; no repository changed.") + raise PolicyError(N_('Host has an active subscription but no active {short} repository. Configure its Enterprise source in {ui}; no repository changed.'), short=p['short'], ui=p['ui']) return 'preserve' if not debian: - raise ValueError('No active Debian base repository for this suite; configure it in the Proxmox repository UI before continuing.') + raise PolicyError(N_('No active Debian base repository for this suite; configure it in the Proxmox repository UI before continuing.')) # No active product source or only inaccessible Enterprise. A mixed stanza # cannot be disabled without also disabling an unrelated component. disable = [item for item in pve if p['enterprise'] in item[2]['Components']] + ceph for _, _, row in disable: expected = {p['enterprise']} if p['enterprise'] in row['Components'] else {'enterprise'} if set(row['Components']) != expected: - raise ValueError('Enterprise shares an APT stanza with other components. Split it in the Proxmox repository UI; no repository changed.') + raise PolicyError(N_('Enterprise shares an APT stanza with other components. Split it in the Proxmox repository UI; no repository changed.')) handles = [r.get('handle') for r in data['standard-repos'] if r.get('handle') == 'no-subscription'] if len(handles) != 1: - raise ValueError('Proxmox no-subscription standard repository handle unavailable; no repository changed.') + raise PolicyError(N_('Proxmox no-subscription standard repository handle unavailable; no repository changed.')) if not apply: return 'offer' # Adopt a refreshed digest only after checking the complete expected @@ -192,11 +229,12 @@ def evaluate(suite, apply=False, product='pve'): for path, index, _ in disable for p, i, row in current): raise ValueError('Could not verify the switched repositories; inspect the Proxmox repository UI before retrying.') except ValueError as exc: - raise ValueError(f"Could not complete or verify the repository switch; changes may be partial and repository state is unknown. Inspect {p['ui']} before retrying; no automatic rollback was attempted.") from exc + raise PolicyError(N_('Could not complete or verify the repository switch; changes may be partial and repository state is unknown. Inspect {ui} before retrying; no automatic rollback was attempted.'), ui=p['ui']) from exc return 'changed' def main(): + product = 'pve' try: if len(sys.argv) not in (3, 4) or sys.argv[1] not in ('plan', 'apply') \ or (len(sys.argv) == 4 and sys.argv[3] not in PRODUCTS): @@ -204,7 +242,7 @@ def main(): product = sys.argv[3] if len(sys.argv) == 4 else 'pve' print(evaluate(sys.argv[2], apply=sys.argv[1] == 'apply', product=product)) except ValueError as exc: - print(str(exc), file=sys.stderr) + print(cli_error(exc, product), file=sys.stderr) return 1 return 0 diff --git a/scripts/global/update-pbs.sh b/scripts/global/update-pbs.sh index 54574676..80200ba7 100644 --- a/scripts/global/update-pbs.sh +++ b/scripts/global/update-pbs.sh @@ -52,7 +52,7 @@ update_pbs() { local available_space available_space=$(df /var/cache/apt/archives | awk 'NR==2 {print int($4/1024)}') if [ "$available_space" -lt 1024 ]; then - msg_error "$(translate "Insufficient disk space. Available: ${available_space}MB")" + msg_error "$(translate "Insufficient disk space. Available:") ${available_space}MB" return 1 fi @@ -149,7 +149,7 @@ update_pbs() { rm -f "$screen_capture" if [ $upgrade_exit_code -ne 0 ]; then - msg_error "$(translate "System upgrade failed. Check log: $log_file")" + msg_error "$(translate "System upgrade failed. Check log:") $log_file" echo "Finished: $(date -Iseconds) — upgrade failed (exit $upgrade_exit_code)" >> "$log_file" return 1 fi diff --git a/scripts/menus/menu_post_install.sh b/scripts/menus/menu_post_install.sh index d4e63cda..2f9696e6 100644 --- a/scripts/menus/menu_post_install.sh +++ b/scripts/menus/menu_post_install.sh @@ -63,13 +63,13 @@ confirm_automated_script() { if [[ "$PMX_ON_PBS" == true ]]; then script_info+="$(translate "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server"):\n\n" - script_info+="• $(translate "Upgrade the system, offering the") \Z4no-subscription repository\Z0 $(translate "when the server has no subscription (you will be asked)")\n" + script_info+="• $(translate "Upgrade the system, offering the") \Z4$(translate "no-subscription repository")\Z0 $(translate "when the server has no subscription (you will be asked)")\n" else script_info+="$(translate "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server"):\n\n" script_info+="• $(translate "Configure") \Z4free repositories\Z0 $(translate "and upgrade the system (disables the enterprise repo)")\n" fi - script_info+="• $(translate "Optionally remove") \Z4subscription banner\Z0 $(translate "from Proxmox web interface (you will be asked)")\n" - script_info+="• $(translate "Optimize") \Z4memory\Z0, \Z4kernel\Z0, $(translate "and") \Z4network\Z0 $(translate "for better performance and stability")\n" + script_info+="• $(translate "Optionally remove") \Z4$(translate "subscription banner")\Z0 $(translate "from Proxmox web interface (you will be asked)")\n" + script_info+="• $(translate "Optimize") \Z4$(translate "memory")\Z0, \Z4$(translate "kernel")\Z0, $(translate "and") \Z4$(translate "network")\Z0 $(translate "for better performance and stability")\n" script_info+="• $(translate "Install and configure") \Z4Log2RAM\Z0 $(translate "(only on SSD/NVMe) to protect your disk")\n" script_info+="• $(translate "Enable") \Z4ZFS autotrim\Z0 $(translate "on SSD/NVMe pools that support discard")\n" script_info+="• $(translate "Improve log rotation and limit log size to save space and extend disk life")\n" @@ -289,10 +289,10 @@ post_install_label() { echo "Proxmox VE Microcode" ;; "Proxmox Backup Server Post Install") - echo "Proxmox Backup Server Post Install" + translate "Proxmox Backup Server Post Install" ;; "Proxmox Backup Server Microcode") - echo "Proxmox Backup Server Microcode" + translate "Proxmox Backup Server Microcode" ;; "Community Scripts") translate "Community Scripts" diff --git a/scripts/post_install/auto_post_install.sh b/scripts/post_install/auto_post_install.sh index e11f131f..1602ad39 100644 --- a/scripts/post_install/auto_post_install.sh +++ b/scripts/post_install/auto_post_install.sh @@ -182,9 +182,9 @@ remove_subscription_banner() { # description: Patch the Proxmox web UI to suppress the subscription dialog and register a successful patch. if [[ "$PMX_ON_PBS" == true ]]; then stop_spinner - if ! whiptail --title "Proxmox Backup Server Subscription Banner Removal" \ + if ! whiptail --title "$(translate "Proxmox Backup Server Subscription Banner Removal")" \ --yesno "$(translate "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?")\n\n$(translate "Attention: Removing the subscription banner may cause issues in the web interface after a future update.")\n\n$(translate "If this happens, you can restore the backup from the 'Subscription Banner Removal' option in 'Uninstall optimizations'.")\n\n$(translate "Are you sure you want to continue?")" 14 75; then - msg_warn "Banner removal cancelled by user." + msg_warn "$(translate "Banner removal cancelled by user.")" return 1 fi if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"; then diff --git a/scripts/post_install/customizable_post_install.sh b/scripts/post_install/customizable_post_install.sh index 72e1b86d..2ce24dc3 100644 --- a/scripts/post_install/customizable_post_install.sh +++ b/scripts/post_install/customizable_post_install.sh @@ -2574,7 +2574,7 @@ configure_zram_swap() { fi version="$(dpkg-query -W -f='${Version}' systemd-zram-generator 2>/dev/null)" if [[ -z "$version" ]]; then - failure="Cannot determine installed systemd-zram-generator version" + failure="$(translate "Cannot determine installed systemd-zram-generator version")" else if dpkg --compare-versions "$version" ge '1.2.0'; then supports_params=true @@ -2588,7 +2588,7 @@ configure_zram_swap() { msg_success "$(translate "ZRAM profile is already active")" return 0 fi - failure="Existing ZRAM profile is active but does not match its configuration" + failure="$(translate "Existing ZRAM profile is active but does not match its configuration")" fi if [[ -z "$failure" && "$previous_active" == true ]]; then if ! swapoff /dev/zram0; then @@ -2597,31 +2597,31 @@ configure_zram_swap() { fi state_mutated=true if ! systemctl stop dev-zram0.swap >/dev/null 2>&1; then - failure="Could not stop existing dev-zram0.swap" + failure="$(translate "Could not stop existing dev-zram0.swap")" fi fi if [[ -z "$failure" ]]; then state_mutated=true if ! install -d -m 755 "$(dirname "$config")"; then - failure="Could not create ZRAM generator directory" + failure="$(translate "Could not create ZRAM generator directory")" elif ! printf '%s\n' "$desired" | pmx_write_file "$config"; then - failure="Could not write ZRAM generator configuration" + failure="$(translate "Could not write ZRAM generator configuration")" fi fi if [[ -z "$failure" ]]; then old_umask="$(umask)" umask 077 if ! printf 'installed_by_proxmenux=%s\n' "$package_owned" | pmx_write_file "$marker"; then - failure="Could not write ZRAM package ownership state" + failure="$(translate "Could not write ZRAM package ownership state")" fi umask "$old_umask" [[ -f "$marker" ]] && chmod 600 "$marker" fi if [[ -z "$failure" ]]; then if ! systemctl daemon-reload || ! systemctl start dev-zram0.swap; then - failure="Could not activate dev-zram0.swap" + failure="$(translate "Could not activate dev-zram0.swap")" elif ! zram_profile_verify_active "$algorithm"; then - failure="Active ZRAM algorithm, capacity or swap sources did not match the profile" + failure="$(translate "Active ZRAM algorithm, capacity or swap sources did not match the profile")" fi fi if [[ -z "$failure" ]]; then diff --git a/scripts/post_install/uninstall-tools.sh b/scripts/post_install/uninstall-tools.sh index 91f90867..570a8bf3 100644 --- a/scripts/post_install/uninstall-tools.sh +++ b/scripts/post_install/uninstall-tools.sh @@ -1356,7 +1356,7 @@ show_uninstall_menu() { system_limits) desc="System Limits Increase";; entropy) desc="Entropy Generation (haveged)";; memory_settings) desc="Memory Settings Optimization";; - zram_swap) desc="ZRAM swap profile";; + zram_swap) desc="$(translate "ZRAM swap profile")";; kernel_panic) desc="Kernel Panic Configuration";; apt_ipv4) desc="APT IPv4 Force";; kexec) desc="kexec for quick reboots";;