i18n: complete OCI runtime and maintenance Slovak diagnostics

This commit is contained in:
VAIO73
2026-10-21 07:06:04 +00:00
parent 37a6c02111
commit 6c05688196
15 changed files with 746 additions and 337 deletions
+2 -2
View File
@@ -93,8 +93,8 @@ def main() -> None:
rootfs = Path(sys.argv[1])
configuration = json.loads(sys.argv[2])
path, changed = update_encoding(rootfs, configuration)
state = "updated" if changed else "already applied"
print(f"Jellyfin configuration {state}: /{path.relative_to(rootfs.resolve())}")
state = translate("Jellyfin configuration updated:") if changed else translate("Jellyfin configuration already applied:")
print(f"{state} /{path.relative_to(rootfs.resolve())}")
if __name__ == "__main__":
+4 -2
View File
@@ -20,6 +20,7 @@ import re
import subprocess
import sys
import time
from oci_ui import translate
CONFIGS = Path('/etc/pve/lxc')
HOOKS = Path('/var/lib/lxc')
@@ -121,12 +122,13 @@ def check(vmid, settle=0):
except BlockingIOError:
return
for name, version in repair(vmid, settle):
print(f'CT {vmid}: DHCP client of {name} (IPv{version}) started again', flush=True)
print(translate('CT {vmid}: DHCP client of {name} (IPv{version}) started again').format(
vmid=vmid, name=name, version=version), flush=True)
def main():
if len(sys.argv) != 2 or not (sys.argv[1].isdigit() or sys.argv[1] == '--all'):
print('usage: oci_dhcp_lease.py <vmid> | --all', file=sys.stderr)
print(translate('Usage:') + ' oci_dhcp_lease.py <vmid> | --all', file=sys.stderr)
return 2
LOCKS.mkdir(parents=True, exist_ok=True)
if sys.argv[1] == '--all':
+4 -3
View File
@@ -10,6 +10,7 @@ import sys
import time
import oci_instances as instances
from oci_ui import translate
# Names given by the OCI installers and by updates; other archives are never touched.
NAME = re.compile(r'(?:proxmenux-update-[a-z0-9]+-[0-9a-f]{64}'
@@ -82,10 +83,10 @@ def main(arguments):
command = arguments[0] if arguments else 'prune'
if command == 'prune':
for path, size in prune():
print(f'removed unused image archive: {path} ({size} bytes)')
print(translate('removed unused image archive: {path} ({size} bytes)').format(path=path, size=size))
return 0
if command not in ('list', 'remove') or not all(a.isdigit() for a in arguments[1:]):
print('usage: oci_image_cache.py [prune | list VMID... | remove VMID...]', file=sys.stderr)
print(translate('Usage:') + ' oci_image_cache.py [prune | list VMID... | remove VMID...]', file=sys.stderr)
return 2
with instances.locked(instances.ROOT):
archives = archives_of([int(a) for a in arguments[1:]])
@@ -103,5 +104,5 @@ if __name__ == '__main__':
try:
sys.exit(main(sys.argv[1:]))
except (OSError, ValueError, BlockingIOError) as error:
print(f'image cache: {error}', file=sys.stderr)
print(f"{translate('Image cache')}: {error}", file=sys.stderr)
sys.exit(1)
+1 -1
View File
@@ -284,7 +284,7 @@ def change(root, primary_id, acceleration, render_device=None, gfx_override=None
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('vmid', type=int, help='main container of the application')
parser.add_argument('vmid', type=int, help=translate('main container of the application'))
parser.add_argument('--acceleration', required=True, choices=PROFILES)
parser.add_argument('--render-device')
parser.add_argument('--gfx-override')
+1 -1
View File
@@ -301,7 +301,7 @@ def main(argv=None):
diagnose = sub.add_parser('diagnose')
diagnose.add_argument('vmid', type=int)
diagnose.add_argument('--check-registry', action='store_true')
record = sub.add_parser('record', help='Internal installer operation; not legacy adoption')
record = sub.add_parser('record', help=translate('Internal installer operation; not legacy adoption'))
record.add_argument('vmid', type=int)
for flag in ('template', 'deployment', 'archive', 'digest'):
record.add_argument('--' + flag, required=True)
+2 -2
View File
@@ -1029,9 +1029,9 @@ def main():
parser.add_argument('--root', type=Path, default=instances.ROOT)
parser.add_argument('--start', action='store_true')
parser.add_argument('--host-firewall', type=int, action='append', default=[], metavar='VMID',
help='Add the host firewall rule of this container; one option for each confirmed rule')
help=translate('Add the host firewall rule of this container; one option for each confirmed rule'))
parser.add_argument('--automatic', action='store_true',
help='Only the applications that need no question: the ones the cluster keeps a record of')
help=translate('Only the applications that need no question: the ones the cluster keeps a record of'))
args = parser.parse_args()
if os.geteuid() != 0:
parser.error(translate('Root privileges are required'))
+3 -1
View File
@@ -12,6 +12,7 @@ import argparse
import re
import subprocess
import sys
from oci_ui import translate
# A model of five operators (convolution, pooling and a dense layer), enough
# to make ROCm compile and run on the GPU without downloading anything.
@@ -72,7 +73,8 @@ def main():
try:
check(args.vmid)
except RuntimeError as error:
print(error, file=sys.stderr)
message = translate('the GPU did not answer in time') if str(error) == 'the GPU did not answer in time' else str(error)
print(message, file=sys.stderr)
return 1
return 0
+2 -1
View File
@@ -18,6 +18,7 @@ import time
import oci_instances as instances
from oci_installation_state import parse_config
from oci_ui import translate
APPS = Path('/etc/proxmenux/apps')
SOCKET = Path('/run/proxmenux-monitor/oci-check.sock')
@@ -206,7 +207,7 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', type=Path, default=instances.ROOT)
parser.add_argument('--configs', type=Path, default=Path('/etc/pve/lxc'))
parser.add_argument('--progress', action='store_true', help='Emit numeric progress for the OCI menu')
parser.add_argument('--progress', action='store_true', help=translate('Emit numeric progress for the OCI menu'))
args = parser.parse_args()
try:
if args.progress:
+6 -3
View File
@@ -19,6 +19,7 @@ import time
import oci_instances as instances
import oci_operation_notice
from oci_ui import translate
UNIT = 'proxmenux-oci-watchdog.service'
UNIT_FILE = Path('/etc/systemd/system') / UNIT
@@ -191,7 +192,8 @@ def look(states, now):
lambda seen: stop_requested(read_tasks(), vmid, seen))
data = {'app_name': name, 'vmid': vmid, 'containers': f'CT {vmid}'}
if action == 'start':
print(f'CT {vmid} ({name}) stopped on its own; starting it again (attempt {state["attempts"]})', flush=True)
print(translate('CT {vmid} ({name}) stopped on its own; starting it again (attempt {attempt})').format(
vmid=vmid, name=name, attempt=state['attempts']), flush=True)
try:
started = start(vmid)
except (OSError, subprocess.SubprocessError):
@@ -199,7 +201,8 @@ def look(states, now):
if started and notice_due(state, now):
oci_operation_notice.notify('oci_watchdog_restarted', data)
elif action == 'give-up':
print(f'CT {vmid} ({name}) could not be kept running; it is left stopped', flush=True)
print(translate('CT {vmid} ({name}) could not be kept running; it is left stopped').format(
vmid=vmid, name=name), flush=True)
oci_operation_notice.notify('oci_watchdog_failed', data)
@@ -290,7 +293,7 @@ def main():
try:
vmids = set_watchdog(instances.ROOT, args.vmid, args.state == 'on')
except BlockingIOError:
print('Another OCI operation is using the instance registry.', file=sys.stderr)
print(translate('Another OCI operation is using the instance registry.'), file=sys.stderr)
return 3
except (OSError, ValueError, KeyError) as error:
print(str(error) or type(error).__name__, file=sys.stderr)
+32 -7
View File
@@ -4,9 +4,32 @@
from __future__ import annotations
import ctypes
import json
import os
import platform
import sys
from pathlib import Path
from functools import lru_cache
@lru_cache(maxsize=1)
def _messages() -> dict:
# This helper is also installed alone in libexec: importing oci_ui would
# break that existing caller. Read the same catalog using stdlib only.
base = Path(os.environ.get('PMX_BASE_DIR', '/usr/local/share/proxmenux'))
try:
language = json.loads((base / 'config.json').read_text(encoding='utf-8')).get('language', 'en')
if language == 'en' or not isinstance(language, str) or not language:
return {}
data = json.loads((base / 'lang' / (language + '.json')).read_text(encoding='utf-8'))
return data if isinstance(data, dict) else {}
except (OSError, ValueError, AttributeError):
return {}
def translate(text: str) -> str:
value = _messages().get(text)
return value if isinstance(value, str) and value else text
AT_FDCWD = -100
@@ -41,14 +64,16 @@ def fail(step: str) -> None:
def main() -> int:
if len(sys.argv) != 5 or sys.argv[4] not in {"rw", "ro"}:
print(f"usage: {sys.argv[0]} PID SOURCE TARGET rw|ro", file=sys.stderr)
print(f"{translate('Usage:')} {sys.argv[0]} PID SOURCE TARGET rw|ro", file=sys.stderr)
return 2
machine = platform.machine().lower()
if machine not in SYSCALLS:
print(f"unsupported host architecture: {machine}", file=sys.stderr)
print(f"{translate('Unsupported host architecture:')} {machine}", file=sys.stderr)
return 2
open_tree_nr, move_mount_nr, mount_setattr_nr = SYSCALLS[machine]
pid, source, target, mode = sys.argv[1:]
# The host catalog is no longer reachable after chroot into the container.
_messages()
libc = ctypes.CDLL(None, use_errno=True)
libc.syscall.restype = ctypes.c_long
libc.setns.argtypes = (ctypes.c_int, ctypes.c_int)
@@ -60,7 +85,7 @@ def main() -> int:
ct_root = os.open(f"/proc/{pid}/root", os.O_PATH | os.O_DIRECTORY | os.O_CLOEXEC)
try:
if libc.setns(ct_ns, CLONE_NEWNS) != 0:
fail("enter container namespace")
fail(translate("enter container namespace"))
os.fchdir(ct_root)
os.chroot(".")
os.chdir("/")
@@ -71,7 +96,7 @@ def main() -> int:
OPEN_TREE_CLONE | os.O_CLOEXEC,
)
if tree < 0:
fail("clone source mount tree")
fail(translate("clone source mount tree"))
try:
if mode == "ro":
attributes = MountAttr(attr_set=MOUNT_ATTR_RDONLY)
@@ -84,9 +109,9 @@ def main() -> int:
ctypes.sizeof(attributes),
)
if result != 0:
fail("make cloned mount tree read-only")
fail(translate("make cloned mount tree read-only"))
if libc.setns(host_ns, CLONE_NEWNS) != 0:
fail("return to host namespace")
fail(translate("return to host namespace"))
os.fchdir(host_root)
os.chroot(".")
os.chdir("/")
@@ -99,7 +124,7 @@ def main() -> int:
MOVE_MOUNT_F_EMPTY_PATH,
)
if result != 0:
fail("publish mount tree")
fail(translate("publish mount tree"))
finally:
os.close(tree)
finally:
+1 -1
View File
@@ -164,7 +164,7 @@ def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("archive", type=Path)
parser.add_argument("--extracted-mib", action="store_true",
help="print the estimated unpacked size in MiB (0 when unknown)")
help=translate("print the estimated unpacked size in MiB (0 when unknown)"))
args = parser.parse_args()
if args.extracted_mib:
try:
+32 -33
View File
@@ -472,7 +472,7 @@ def _rclone_mount(catalog: Catalog, ui) -> None:
if result:
from .management import carry_records
carry_records(PROJECT_ROOT, shown=True)
console.msg_ok(f"Remote: {result['remote']}:")
console.msg_ok(f"{translate('Rclone remote')}: {result['remote']}:")
console.msg_ok(f"{translate('Read/write')}: {result['read_write_path']}")
console.msg_ok(f"{translate('Read-only')}: {result['read_only_path']}")
console.wait_for_enter(translate("Press Enter to return to the menu..."))
@@ -623,52 +623,52 @@ def _template_summary_text(template: dict[str, Any]) -> str:
contract = template["container_contract"]
lines = [
source_text(template["catalog_ui"]["title"]),
f"Image: {contract['image']['reference']}",
f"Status: {template['status']}",
"Ports: " + (", ".join(f"{p['container_port']}/{p['protocol']}" for p in contract["ports"]) or "none"),
"Volumes: " + (", ".join(v["container_path"] for v in contract["volumes"]) or "none"),
f"{translate('Image')}: {contract['image']['reference']}",
f"{translate('Status')}: {template['status']}",
translate("Ports") + ": " + (", ".join(f"{p['container_port']}/{p['protocol']}" for p in contract["ports"]) or translate("none")),
translate("Volumes") + ": " + (", ".join(v["container_path"] for v in contract["volumes"]) or translate("none")),
]
blockers = template["compatibility"]["untranslated_blockers"]
if blockers:
lines.append(f"Blockers: {', '.join(blockers)}")
lines.append(f"{translate('Blockers')}: {', '.join(blockers)}")
return "\n".join(lines)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="oci_manager_apps.sh",
description="ProxMenux OCI manager Apps (Technology Preview)")
description=translate("ProxMenux OCI manager Apps (Technology Preview)"))
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("sync", help="Refresh the index from the image sources")
list_parser = subparsers.add_parser("list", help="List applications")
subparsers.add_parser("sync", help=translate("Refresh the index from the image sources"))
list_parser = subparsers.add_parser("list", help=translate("List applications"))
list_parser.add_argument("--filter", default="")
generate_parser = subparsers.add_parser("generate", help="Regenerate the template of one application")
generate_parser = subparsers.add_parser("generate", help=translate("Regenerate the template of one application"))
generate_parser.add_argument("app")
generate_all_parser = subparsers.add_parser("generate-all", help="Regenerate the catalog templates")
generate_all_parser = subparsers.add_parser("generate-all", help=translate("Regenerate the catalog templates"))
generate_all_parser.add_argument("--provider", choices=["all", "linuxserver.io", "imported", "curated"],
default="all")
subparsers.add_parser("apply-icons", help="Resolve catalog icons against the jsdelivr icon sets")
show_parser = subparsers.add_parser("show", help="Show the summary of a template")
subparsers.add_parser("apply-icons", help=translate("Resolve catalog icons against the jsdelivr icon sets"))
show_parser = subparsers.add_parser("show", help=translate("Show the summary of a template"))
show_parser.add_argument("app")
open_parser = subparsers.add_parser("open", help="Open the page of an application in the menu")
open_parser = subparsers.add_parser("open", help=translate("Open the page of an application in the menu"))
open_parser.add_argument("app")
install_parser = subparsers.add_parser("install", help="Configure and install an application")
install_parser = subparsers.add_parser("install", help=translate("Configure and install an application"))
install_parser.add_argument("app")
install_parser.add_argument("--host", default="auto", help="'auto'/'local', or root@IP for development")
install_parser.add_argument("--host", default="auto", help=translate("'auto'/'local', or root@IP for development"))
install_parser.add_argument("--advanced", action="store_true")
install_parser.add_argument("--dry-run", action="store_true")
rclone_parser = subparsers.add_parser("rclone-mount", help="Enable a mount on an installed Rclone OCI")
rclone_parser = subparsers.add_parser("rclone-mount", help=translate("Enable a mount on an installed Rclone OCI"))
rclone_parser.add_argument("--host", default="auto")
rclone_parser.add_argument("--dry-run", action="store_true")
subparsers.add_parser("recover", help="Register again the OCI applications restored from a backup")
manage_parser = subparsers.add_parser("manage", help="Update, modify or recreate one installed OCI instance")
subparsers.add_parser("recover", help=translate("Register again the OCI applications restored from a backup"))
manage_parser = subparsers.add_parser("manage", help=translate("Update, modify or recreate one installed OCI instance"))
manage_parser.add_argument("vmid", type=int)
manage_parser.add_argument("--action", choices=("update", "modify", "recreate"), required=True)
manage_parser.add_argument("--keep-backup", metavar="STORAGE",
help="Keep the backup taken before the update in this Proxmox storage")
help=translate("Keep the backup taken before the update in this Proxmox storage"))
manage_parser.add_argument("--unattended", action="store_true",
help="Scheduled run: no questions; stops where a person has to decide")
help=translate("Scheduled run: no questions; stops where a person has to decide"))
manage_parser.add_argument("--acknowledge-external-data", action="store_true",
help="Host directories are not reverted by the backup (confirmed beforehand)")
help=translate("Host directories are not reverted by the backup (confirmed beforehand)"))
manage_parser.add_argument("--min-image-age-days", type=int, default=0)
return parser
@@ -681,15 +681,15 @@ def main(argv: list[str] | None = None) -> int:
return interactive(catalog)
if args.command == "sync":
payload = catalog.sync_index()
print(f"Index updated: {len(payload['applications'])} candidate applications")
print(translate('Index updated: {count} candidate applications').format(count=len(payload['applications'])))
return 0
if args.command == "list":
query = args.filter.casefold()
for item in catalog.load_index()["applications"]:
if not item.get("hidden", False) and query in item["id"].casefold():
candidate = item.get("automatic_install_candidate")
status = ("installable" if candidate else "pending adaptation" if candidate is False
else item.get("template_status") or "not generated")
status = (translate("installable") if candidate else translate("pending adaptation") if candidate is False
else item.get("template_status") or translate("not generated"))
print(f"{item['id']:<30} {_display_architectures(item):<12} {publisher(item):<14} {status}")
return 0
if args.command == "generate":
@@ -702,13 +702,12 @@ def main(argv: list[str] | None = None) -> int:
print(f"\r[{current:3}/{total}] {marker:<5} {app_id:<32}", end="", flush=True)
report = catalog.generate_all(progress=progress, provider=args.provider)
print(f"\nGenerated: {report['generated_count']}; failed: {report['failed_count']}; "
f"family: {args.provider}")
print('\n' + translate('Generated: {generated}; failed: {failed}; family: {provider}').format(
generated=report['generated_count'], failed=report['failed_count'], provider=args.provider))
return 0 if not report["failed"] else 2
if args.command == "apply-icons":
report = catalog.apply_icons()
print(f"Icons resolved: {report['resolved']}; cleared: {report['cleared']}; "
f"unchanged: {report['unchanged']}; with a theme variant: {report['themed']}")
print(translate('Icons resolved: {resolved}; cleared: {cleared}; unchanged: {unchanged}; with a theme variant: {themed}').format(**report))
return 0
if args.command == "show":
print(_template_summary_text(catalog.compose(args.app)))
@@ -728,14 +727,14 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "install":
template = catalog.compose(args.app)
if not template["compatibility"]["automatic_install_candidate"]:
raise InstallError("Installation blocked: " + ", ".join(template["compatibility"]["untranslated_blockers"]))
raise InstallError(translate("Installation blocked:") + " " + ", ".join(template["compatibility"]["untranslated_blockers"]))
notice = external_services_notice(template)
if notice:
print(notice + "\n")
deployment = build_deployment(template, None, ADVANCED_MODE if args.advanced else DEFAULT_MODE)
print(_deployment_summary_text(template, deployment))
if not args.dry_run and input("\nInstall? [y/N]: ").strip().casefold() not in {"y", "yes"}:
print("Installation cancelled.")
if not args.dry_run and input('\n' + translate('Install? [y/N]:') + ' ').strip().casefold() not in {"y", "yes"}:
print(translate("Installation cancelled."))
return 0
result = run_remote_install(PROJECT_ROOT, template, deployment, args.host, args.dry_run)
if result:
@@ -764,7 +763,7 @@ def main(argv: list[str] | None = None) -> int:
print(json.dumps(deployment, ensure_ascii=False, indent=2))
result = run_remote_rclone_mount(PROJECT_ROOT, template, deployment, args.host, args.dry_run)
if result:
print(f"Read/write: {result['read_write_path']}\nRead-only: {result['read_only_path']}")
print(f"{translate('Read/write')}: {result['read_write_path']}\n{translate('Read-only')}: {result['read_only_path']}")
return 0
return 1
except (KeyboardInterrupt, UserCancelled):
+45
View File
@@ -57,10 +57,55 @@ def diagnostic(error: Any) -> str:
for pattern, source in (
(r"No template is available for '(.*)'", N_("No template is available for '{app_id}'")),
(r"The application '(.*)' is not in the index", N_("The application '{app_id}' is not in the index")),
(r"The application '(.*)' does not come from LinuxServer", N_("The application '{app_id}' does not come from LinuxServer")),
):
match = re.fullmatch(pattern, text)
if match:
return translate(source).format(app_id=match[1])
exact = {
'The CasaOS Compose file has no services': N_('The Compose file has no services.'),
'The Compose file has no services': N_('The Compose file has no services.'),
'x-casaos.main no identifica un servicio valido': N_('x-casaos.main must name an existing service.'),
'The main CasaOS service declares no image': N_('The main Compose service has no image.'),
'El servicio principal no declara una imagen': N_('The main Compose service has no image.'),
'El servicio no declara una imagen': N_('The service declares no image:'),
'The CasaOS document is not a Compose mapping': N_('The Compose document must be a mapping.'),
'The Compose file has no x-casaos metadata': N_('The Compose file has no x-casaos metadata.'),
'environment must be a list or a mapping': N_('Compose environment must be a list or a mapping.'),
'volumes must be a list': N_('Compose volumes must be a list.'),
'ports must be a list': N_('Compose ports must be a list.'),
'The README has no recognisable docker-compose section': N_('The README has no recognisable docker-compose section'),
'La seccion docker-compose no contiene un bloque de codigo': N_('The docker-compose section contains no code block'),
'The docker-compose block is not closed': N_('The docker-compose block is not closed'),
'The block found does not look like a Docker Compose file': N_('The block found does not look like a Docker Compose file'),
'No se puede identificar la imagen principal del Compose': N_('The main Compose service cannot be identified unambiguously'),
'No se puede identificar de forma univoca el servicio principal del Compose': N_('The main Compose service cannot be identified unambiguously'),
'python3-jsonschema is required to generate templates': N_('python3-jsonschema is required to generate templates'),
'Bulk generation requires GITHUB_TOKEN to obtain an immutable revision per application': N_('Bulk generation requires GITHUB_TOKEN to obtain an immutable revision per application'),
'The ProxMenux application catalogue is not a list': N_('The ProxMenux application catalogue is not a list'),
'GitHub returned a truncated tree for the CasaOS catalogue': N_('GitHub returned a truncated tree for the CasaOS catalogue'),
'No Compose files were found in the CasaOS catalogue': N_('No Compose files were found in the CasaOS catalogue'),
}
if text in exact:
return translate(exact[text])
for prefix, source in (
('Cannot normalise the CasaOS identifier:', N_('Invalid application identifier:')),
('Cannot normalise the identifier:', N_('Invalid application identifier:')),
('Docker Compose CasaOS no valido:', N_('The Compose file is not valid YAML:')),
('Docker Compose no valido:', N_('The Compose file is not valid YAML:')),
('La plantilla generada no cumple el esquema:', N_('The generated template does not match the schema:')),
):
if text.startswith(prefix):
return translate(source) + text[len(prefix):]
match = re.fullmatch(r'GitHub returned HTTP 403 for (.+)\. Set GITHUB_TOKEN to raise the API rate limit\.', text)
if match:
return translate('GitHub returned HTTP 403 for {url}. Set GITHUB_TOKEN to raise the API rate limit.').format(url=match[1])
match = re.fullmatch(r'GitHub returned HTTP ([0-9]{3}) (?:for|para) (.+)\.', text)
if match:
return translate('GitHub returned HTTP {status} for {url}.').format(status=match[1], url=match[2])
match = re.fullmatch(r'No se pudo acceder a (.+) despues de 3 intentos: (.*)', text, re.S)
if match:
return translate('Could not reach {url} after 3 attempts: {reason}').format(url=match[1], reason=match[2])
return text