Merge pull request #509 from Vaso73/i18n/slovak-zram-pbs-cli

i18n: complete Slovak ZRAM and PBS CLI messages
This commit is contained in:
MacRimi
2026-10-21 22:09:24 +01:00
committed by GitHub
11 changed files with 553 additions and 37 deletions
@@ -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())
@@ -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__':
@@ -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})
+35 -5
View File
@@ -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é)",
+1 -1
View File
@@ -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"
}
}
+49 -11
View File
@@ -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
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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"
+2 -2
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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";;