diff --git a/.github/scripts/build_translation_cache.py b/.github/scripts/build_translation_cache.py index 95825a29..b5797010 100644 --- a/.github/scripts/build_translation_cache.py +++ b/.github/scripts/build_translation_cache.py @@ -38,6 +38,18 @@ DEFAULT_CONTEXT = "Context: Technical message for Proxmox and IT. Translate:" # provider, then restore the exact source spelling afterwards. Keep longer # terms first so ``gasket`` cannot consume part of ``gasket-dkms``. PROTECTED_TECHNICAL_TERMS = ( + # Product names, kept whole. Left to a translator, "Proxmox Backup Server" + # became "Proxmox Sicherungsserver" and "Serveur de sauvegarde Proxmox", + # and when a provider rightly handed it back unchanged the string was + # retried and then dropped as untranslated. + "Proxmox Backup Server", + "Proxmox Datacenter Manager", + "Proxmox Mail Gateway", + "Proxmox VE", + "ProxMenux Monitor", + "ProxMenux Optimizations", + "Secure Gateway", + "Tailscale", "google/gasket-driver", "feranick/gasket-driver", "libedgetpu1-std", diff --git a/.github/scripts/build_web_docs_i18n.py b/.github/scripts/build_web_docs_i18n.py index e0567c15..cf77fd5f 100644 --- a/.github/scripts/build_web_docs_i18n.py +++ b/.github/scripts/build_web_docs_i18n.py @@ -73,6 +73,24 @@ TECHNICAL_TERMS = ( "DKMS", "SSH", "API", + "Flask", + "SQLite", + "Prometheus", + "Grafana", + "Uptime Kuma", + "PWA", + "Fail2Ban", + "Tailscale", + "JavaScript", + "TypeScript", + "Next.js", + "Coral", + "rclone", + "journald", + "gasket", + "apex", + "Alpine", + "OpenVINO", ) PROTECTED_PATTERNS = ( @@ -88,9 +106,13 @@ PROTECTED_PATTERNS = ( re.compile(r"(?. + re.compile(r"(?]*>.*?", + r"<(code|codeNw|codeXs|kbd|pre)\b[^>]*>.*?", re.IGNORECASE | re.DOTALL, ) @@ -109,16 +131,23 @@ ANY_TAG_RE = re.compile(r"]*>") BARE_SENTINELS = False PLACEHOLDER_RE = re.compile(r"\{[A-Za-z_][A-Za-z0-9_.-]*\}") NON_TRANSLATABLE_KEYS = { + "accent", + "categoryslug", "command", "code", "href", "icon", + "iconbg", "id", + "menucmd", "path", "route", "slug", "src", "url", + "color", + "tone", + "variant", } SOURCE_STATE_VERSION = 1 @@ -186,12 +215,34 @@ def should_copy(source: str, path: tuple[str | int, ...]) -> bool: if not source.strip() or not re.search(r"[A-Za-z]", source): return True last = str(path[-1]).lower() if path else "" + # These fields describe the surrounding snippet; they are prose, even + # though their names end in "Code". + if last in {"aftercode", "beforecode"}: + return False + # This UI breadcrumb is prose, rather than a filesystem path. + if last == "uipath" and "" in source: + return False + if last == "flow" and source in {"loader", "dedicated"}: + return True + # These exact values drive rendering/navigation; similarly named visible + # labels remain translatable. + if last == "branch" and source in {"main", "develop", "feature/*"}: + return True + if last == "kind" and source in {"internal", "external"}: + return True + if last == "key" and source in {"linux", "nas", "windows"}: + return True if last in NON_TRANSLATABLE_KEYS or any( last.endswith(suffix) for suffix in ("url", "href", "path", "command", "code", "icon") ): return True - if re.fullmatch(r"https?://\S+", source) or re.fullmatch(r"/[A-Za-z0-9_./:@%+=-]+", source): + if re.fullmatch(r"https?://\S+", source) or re.fullmatch(r"/\S+", source): + return True + # Standalone literal blocks contain examples, not prose. Separators between + # several blocks do not make their command/configuration contents translatable. + without_literals = LITERAL_TAG_RE.sub("", source) + if without_literals != source and not re.search(r"[A-Za-z]", ANY_TAG_RE.sub("", without_literals)): return True if re.fullmatch(r"[A-Z0-9_.:/+-]{2,}", source): return True @@ -202,6 +253,15 @@ def leaf_token(path: tuple[str | int, ...]) -> str: return json.dumps(path, ensure_ascii=False, separators=(",", ":")) +def literal_values_match(source: Any, target: Any) -> bool: + """Literal commands and rendering metadata must follow the source too.""" + return all( + not should_copy(leaf.source, leaf.path) + or get_at_path(target, leaf.path) == leaf.source + for leaf in iter_leaves(source) + ) + + def source_fingerprints(source: Any) -> dict[str, str]: return { leaf_token(leaf.path): hashlib.sha256(leaf.source.encode("utf-8")).hexdigest()[:20] @@ -240,7 +300,7 @@ def protect_rich_tags(text: str) -> tuple[str, dict[str, str]]: def replace(match: re.Match[str]) -> str: original = match.group(1) - if original.lower() in {"code", "kbd", "pre"}: + if original.lower() in {"code", "codenw", "codexs", "kbd", "pre"}: return match.group(0) key = original.lower() internal = names.get(key) @@ -329,11 +389,14 @@ def protect_text(text: str) -> tuple[str, dict[str, str]]: def restore_text(text: str, mapping: dict[str, str]) -> str: - missing = [token for token in mapping if token not in text] + # A local model may append a digit to a bare sentinel. A substring match + # would silently restore the prefix and leak the extra digit into prose. + patterns = {token: re.compile(re.escape(token) + r"(?![0-9])") for token in mapping} + missing = [token for token, pattern in patterns.items() if not pattern.search(text)] if missing: raise ValueError(f"translation provider changed protected token {missing[0]}") for token, original in mapping.items(): - text = text.replace(token, original) + text = patterns[token].sub(lambda match: original, text) return text @@ -347,6 +410,11 @@ def validate_contract(source: str, target: str) -> None: raise ValueError("literal code changed") if sorted(PLACEHOLDER_RE.findall(source)) != sorted(PLACEHOLDER_RE.findall(target)): raise ValueError("placeholder contract changed") + # Bare numeric protection markers once leaked a trailing zero just + # outside an otherwise intact code span. Literal/ICU checks cannot see it. + residue = re.compile(r'(?:|)(0+)(?=\s|[;:/<().,-]|$)') + if len(residue.findall(target)) > len(residue.findall(source)): + raise ValueError("unexpected numeric residue next to literal markup") def provider_function(args: argparse.Namespace) -> Callable[[str, str], str]: @@ -822,7 +890,10 @@ def main() -> int: leaves = pending_leaves(source, target, queued_tokens) total_strings += len(iter_leaves(source)) missing_strings += len(leaves) - schema_changed = not schema_matches(source, target) + schema_changed = ( + not schema_matches(source, target) + or not literal_values_match(source, target) + ) if leaves or schema_changed: pending.append( ( diff --git a/.github/scripts/check_terminal_catalogs.py b/.github/scripts/check_terminal_catalogs.py new file mode 100644 index 00000000..ba5795da --- /dev/null +++ b/.github/scripts/check_terminal_catalogs.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Check active terminal translations offline, without generating any text. + +Validates formatting fields and literal commands/identifiers. Missing values +can retain English fallback; --require-complete makes them errors. This does +not establish the accuracy or fluency of the translated prose. +""" +from __future__ import annotations + +import argparse +from pathlib import Path +import re + +from build_translation_cache import ( + DEFAULT_LANGUAGES, STRUCTURAL_LITERAL_RE, extract_catalog_texts, + extract_python_texts, extract_translate_texts, +) +from build_web_docs_i18n import read_json, validate_contract + + +def active_sources(root: Path) -> list[str]: + # Match build-translation-cache.yml, including launchers outside scripts/. + extras = [root / name for name in ('menu', 'install_proxmenux.sh', 'install_proxmenux_beta.sh')] + texts = extract_translate_texts(root / 'scripts', extras) + texts += extract_translate_texts(root / 'oci/remote') + texts += extract_python_texts([root / 'scripts', root / 'oci/src', root / 'oci/remote']) + texts += extract_catalog_texts([root / 'oci/catalog']) + return sorted({text for text in texts if '$' not in text and '`' not in text}) + + +def validate_message(source: str, target: str, *, title_separator: bool = False) -> None: + validate_contract(source, target) + for match in STRUCTURAL_LITERAL_RE.finditer(source): + literal = match.group() + # A page title may replace its decorative separator with a dash. + # In command examples, a pipe remains a shell operator. + if title_separator and literal == '|': + continue + # DNS hostnames are case-insensitive. Paths, field names and command + # names are not, so do not case-fold those to hide a broken example. + is_hostname = re.fullmatch(r'[A-Za-z0-9.-]+\.(?:com|org|net|io)', literal) + if literal not in target and not (is_hostname and literal.casefold() in target.casefold()): + raise ValueError(f'literal command or identifier changed: {literal}') + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root', type=Path, default=Path('.')) + parser.add_argument('--languages', default=','.join(DEFAULT_LANGUAGES)) + parser.add_argument('--require-complete', action='store_true') + args = parser.parse_args() + root = args.root.resolve() + texts = active_sources(root) + errors = 0 + for language in args.languages.split(','): + if language not in DEFAULT_LANGUAGES: + parser.error(f'Unsupported locale: {language}') + catalog = read_json(root / 'lang' / f'{language}.json') or {} + missing = 0 + for source in texts: + target = catalog.get(source) + if not isinstance(target, str) or not target.strip(): + missing += 1 + continue + try: + validate_message(source, target) + except ValueError as error: + print(f'[{language}] {source!r}: {error}') + errors += 1 + print(f'[{language}] active={len(texts)} missing={missing}') + if args.require_complete and missing: + errors += 1 + return 1 if errors else 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/.github/scripts/check_web_docs_catalogs.py b/.github/scripts/check_web_docs_catalogs.py new file mode 100644 index 00000000..422b39cb --- /dev/null +++ b/.github/scripts/check_web_docs_catalogs.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Check documentation translation structure and literal contracts offline. + +Missing locales/leaves are allowed by default because the website falls back +to English. Use --require-complete when validating a catalog completion PR. +This checks data integrity, not translation fluency or linguistic accuracy. +""" +from __future__ import annotations + +import argparse +from pathlib import Path + +from build_web_docs_i18n import ( + DEFAULT_LANGUAGES, + get_at_path, + iter_leaves, + literal_values_match, + read_json, + schema_matches, + source_files, +) +from check_terminal_catalogs import validate_message + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--messages-dir", type=Path, default=Path("web/messages")) + parser.add_argument("--languages", default=",".join(DEFAULT_LANGUAGES)) + parser.add_argument("--require-complete", action="store_true") + args = parser.parse_args() + root = args.messages_dir.resolve() + files = source_files(root / "en", ".") + errors = 0 + for language in args.languages.split(","): + language = language.strip() + if language not in DEFAULT_LANGUAGES: + parser.error(f"Unsupported locale: {language}") + missing = checked = present_files = 0 + for file in files: + relative = file.relative_to(root / "en") + source = read_json(file) + target = read_json(root / language / relative) + if target is not None: + present_files += 1 + if args.require_complete and not schema_matches(source, target): + print(f"[{language}] {relative}: source/target schema differs") + errors += 1 + for leaf in iter_leaves(source): + localized = get_at_path(target, leaf.path) + if not isinstance(localized, str) or (leaf.source.strip() and not localized.strip()): + missing += 1 + continue + try: + validate_message( + leaf.source, localized, + title_separator=bool( + leaf.path and leaf.path[0] == "meta" + and leaf.path[-1] in ("title", "ogTitle", "twitterTitle") + ), + ) + except ValueError as exc: + print(f"[{language}] {relative}:{'.'.join(map(str, leaf.path))}: {exc}") + errors += 1 + checked += 1 + if args.require_complete and not literal_values_match(source, target): + print(f"[{language}] {relative}: literal commands or rendering metadata differ") + errors += 1 + print(f"[{language}] files={present_files}/{len(files)} strings={checked} missing={missing}") + if args.require_complete and missing: + errors += 1 + return 1 if errors else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/run_offline_qualified.py b/.github/scripts/run_offline_qualified.py index 9bd13abc..fe6c26d4 100644 --- a/.github/scripts/run_offline_qualified.py +++ b/.github/scripts/run_offline_qualified.py @@ -13,6 +13,7 @@ import sys import tempfile PYTHON_FILES = ( + "AppImage/scripts/tests/test_native_app_discovery.py", "tests/test_audit_presentation.py", "tests/test_audit_safety_wording.py", "tests/test_audit_policy.py", @@ -24,6 +25,7 @@ PYTHON_FILES = ( "AppImage/scripts/tests/test_health_persistence_recurrence.py", ) NODE_FILES = ( + "tests/lxc_updates/test_native_app_discovery_ui.cjs", "tests/lxc_updates/test_docker_delegated_ui.cjs", "tests/test_audit_diagnostic_document.cjs", "tests/test_audit_policy.cjs", diff --git a/.github/scripts/tests/test_build_web_docs_i18n.py b/.github/scripts/tests/test_build_web_docs_i18n.py index ea2693ca..691a6639 100644 --- a/.github/scripts/tests/test_build_web_docs_i18n.py +++ b/.github/scripts/tests/test_build_web_docs_i18n.py @@ -60,6 +60,124 @@ class WebDocsI18nTests(unittest.TestCase): target = {"title": "Actualizaciones", "url": "https://example.com"} self.assertEqual(MODULE.pending_leaves(source, target), []) + def test_rendering_metadata_is_copied_but_visible_labels_are_translated(self): + for key, value in (("tone", "amber"), ("variant", "source"), + ("accent", "border-blue-300 bg-blue-50"), + ("iconBg", "bg-blue-100 text-blue-700"), + ("color", "green")): + with self.subTest(key=key): + self.assertTrue(MODULE.should_copy(value, ("steps", 0, key))) + self.assertFalse(MODULE.should_copy(value, ("label",))) + self.assertEqual(MODULE.merge_tree({key: value}, {key: "translated"}, {}), {key: value}) + self.assertFalse(MODULE.should_copy("Translation", ("type",))) + self.assertFalse(MODULE.should_copy("English", ("lang",))) + for value in ("loader", "dedicated"): + self.assertTrue(MODULE.should_copy(value, ("cards", 0, "flow"))) + self.assertFalse(MODULE.should_copy(value, ("label",))) + + def test_literal_only_examples_and_api_routes_are_not_translation_gaps(self): + for source in ("apt update", + "net0 / net1.", + "/api/task-log/<upid>", "/api/vms//config"): + with self.subTest(source=source): + self.assertTrue(MODULE.should_copy(source, ("cell",))) + self.assertFalse(MODULE.should_copy("Run apt update first.", ("body",))) + self.assertFalse(MODULE.should_copy("After running the command, check its output.", ("afterCode",))) + self.assertFalse(MODULE.should_copy("Before running the command, read this.", ("beforeCode",))) + self.assertTrue(MODULE.should_copy("curl http://localhost:8008/api/health", ("curlCode",))) + + def test_inline_identifiers_and_encoded_delimiters_round_trip(self): + source = "Set RS_SKIP_PATHS, call _rs_apply_hot and keep localStorage at <host>." + protected, mapping = MODULE.protect_text(source) + for literal in ("RS_SKIP_PATHS", "_rs_apply_hot", "localStorage", "<", ">"): + self.assertIn(literal, mapping.values()) + self.assertNotIn(literal, protected) + self.assertEqual(MODULE.restore_text(protected, mapping), source) + + def test_custom_code_spans_keep_commands_literal(self): + for tag in ("codeNw", "codeXs"): + source = f"Run <{tag}>pct config 100." + with self.subTest(tag=tag): + protected, mapping = MODULE.protect_rich_tags(source) + self.assertEqual(protected, source) + self.assertEqual(mapping, {}) + with self.assertRaisesRegex(ValueError, "literal code changed"): + MODULE.validate_contract(source, source.replace("config", "configuration")) + with self.assertRaisesRegex(ValueError, "numeric residue"): + MODULE.validate_contract(source, source.replace(f"", f"0")) + self.assertTrue(MODULE.should_copy(f"<{tag}>pct config 100", ("example",))) + + def test_navigation_identifiers_are_distinct_from_visible_labels(self): + values = { + "categorySlug": ["system", "network"], + "menuCmd": ["menu"], + "branch": ["main", "develop", "feature/*"], + "kind": ["internal", "external"], + "key": ["nas", "linux", "windows"], + } + for key, items in values.items(): + for value in items: + with self.subTest(key=key, value=value): + self.assertTrue(MODULE.should_copy(value, ("cards", 0, key))) + self.assertFalse(MODULE.should_copy(value, ("label",))) + self.assertFalse(MODULE.should_copy("Translation", ("kind",))) + self.assertFalse(MODULE.should_copy("Storage", ("branch",))) + + def test_ui_breadcrumb_is_prose_while_paths_remain_literal(self): + source = "In the UI: Datacenter → Backup → Retention:" + self.assertFalse(MODULE.should_copy(source, ("retention", "uiPath"))) + self.assertTrue(MODULE.should_copy("/etc/pve/storage.cfg", ("uiPath",))) + self.assertTrue(MODULE.should_copy("/etc/pve/storage.cfg", ("configPath",))) + + def test_bare_sentinel_with_added_digit_is_rejected(self): + with mock.patch.object(MODULE, "BARE_SENTINELS", True): + protected, mapping = MODULE.protect_text("ProxMenux collects metrics.") + token = next(iter(mapping)) + with self.assertRaisesRegex(ValueError, "changed protected token"): + MODULE.restore_text(protected.replace(token, token + "0"), mapping) + self.assertEqual(MODULE.restore_text(protected, mapping), "ProxMenux collects metrics.") + + def test_numeric_residue_outside_intact_literal_is_rejected(self): + examples = [ + ('Run pct config first.', 'Spustite pct config0 najprv.'), + ('Restart=on-failure restarts the service.', + '0Restart=on-failure reštartuje službu.'), + ] + for source, target in examples: + with self.subTest(source=source): + with self.assertRaisesRegex(ValueError, 'numeric residue'): + MODULE.validate_contract(source, target) + MODULE.validate_contract('The result is echo0.', + 'Výsledok je echo0.') + + def test_product_and_kernel_module_names_are_protected(self): + source = "Flask uses SQLite and journald; rclone works with Coral, gasket, apex and Alpine." + protected, mapping = MODULE.protect_text(source) + for name in ("Flask", "SQLite", "journald", "rclone", "Coral", "gasket", "apex", "Alpine"): + self.assertIn(name, mapping.values()) + self.assertNotIn(name, protected) + self.assertEqual(MODULE.restore_text(protected, mapping), source) + + def test_literal_only_changes_sync_without_retranslating_human_prose(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = root / "messages/en/common.json" + target = root / "messages/de/common.json" + source.parent.mkdir(parents=True) + target.parent.mkdir(parents=True) + source.write_text(json.dumps({"title": "Updates", "code": "tool --version=2", "tone": "blue"})) + target.write_text(json.dumps({"title": "Aktualisierungen", "code": "tool --version=1", "tone": "blau"})) + provider = mock.Mock(side_effect=AssertionError("Existing prose must not be retranslated")) + self.assertEqual(self.run_generator(root, provider), 0) + self.assertEqual(json.loads(target.read_text()), { + "title": "Aktualisierungen", "code": "tool --version=2", "tone": "blue", + }) + provider.assert_not_called() + before = target.stat().st_mtime_ns + self.assertEqual(self.run_generator(root, provider), 0) + self.assertEqual(target.stat().st_mtime_ns, before) + provider.assert_not_called() + def test_changed_source_text_is_pending_even_with_existing_translation(self): source = {"title": "Updated installation guidance"} target = {"title": "Vorherige Installationsanleitung"} diff --git a/.github/scripts/tests/test_documentation_markdown.py b/.github/scripts/tests/test_documentation_markdown.py new file mode 100644 index 00000000..98255351 --- /dev/null +++ b/.github/scripts/tests/test_documentation_markdown.py @@ -0,0 +1,16 @@ +"""Keep the canonical conduct policy and its English website source in sync.""" +import json +from pathlib import Path +import unittest + +ROOT = Path(__file__).resolve().parents[3] + + +class DocumentationMarkdownTests(unittest.TestCase): + def test_english_conduct_body_matches_canonical_policy(self): + source = json.loads((ROOT / 'web/messages/en/docs/about/code-of-conduct.json').read_text(encoding='utf-8')) + self.assertEqual(source['contentMarkdown'], (ROOT / 'CODE_OF_CONDUCT.md').read_text(encoding='utf-8')) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/scripts/tests/test_documentation_review_workflows.py b/.github/scripts/tests/test_documentation_review_workflows.py new file mode 100644 index 00000000..49d8cd93 --- /dev/null +++ b/.github/scripts/tests/test_documentation_review_workflows.py @@ -0,0 +1,30 @@ +"""Keep machine generation explicit without losing automatic validation.""" +from pathlib import Path +import unittest + +import yaml + + +ROOT = Path(__file__).resolve().parents[3] + + +class DocumentationReviewWorkflowTests(unittest.TestCase): + def workflow(self, name): + return yaml.load( + (ROOT / '.github/workflows' / name).read_text(encoding='utf-8'), + Loader=yaml.BaseLoader, + ) + + def test_generation_requires_explicit_dispatch(self): + triggers = self.workflow('build-web-docs-i18n.yml')['on'] + self.assertEqual(set(triggers), {'workflow_dispatch'}) + + def test_website_changes_keep_automatic_offline_validation(self): + triggers = self.workflow('test-i18n.yml')['on'] + for event in ('pull_request', 'push'): + with self.subTest(event=event): + self.assertIn('web/**', triggers[event]['paths']) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/scripts/tests/test_installer_languages.py b/.github/scripts/tests/test_installer_languages.py new file mode 100644 index 00000000..0aa0ccef --- /dev/null +++ b/.github/scripts/tests/test_installer_languages.py @@ -0,0 +1,30 @@ +"""Every shipped terminal language must be selectable during installation.""" +from pathlib import Path +import re +import sys +import unittest + +SCRIPTS = Path(__file__).resolve().parents[1] +ROOT = SCRIPTS.parents[1] +sys.path.insert(0, str(SCRIPTS)) +from build_translation_cache import DEFAULT_LANGUAGES + + +class InstallerLanguageTests(unittest.TestCase): + def test_both_installers_offer_every_shipped_language(self): + expected = {'en', *DEFAULT_LANGUAGES} + for name in ('install_proxmenux.sh', 'install_proxmenux_beta.sh'): + with self.subTest(installer=name): + source = (ROOT / name).read_text(encoding='utf-8') + function = re.search(r'^select_language\(\) \{(.*?)^\}', source, re.M | re.S) + self.assertIsNotNone(function, 'Missing installer language selector') + offered = re.findall(r'^\s*"([a-z]{2})"\s+"[^"]+"', function.group(1), re.M) + self.assertEqual(set(offered), expected) + self.assertEqual(len(offered), len(expected), 'Duplicate language option') + # English is the source text in the scripts, not a JSON catalog. + for language in set(offered) - {'en'}: + self.assertTrue((ROOT / 'lang' / f'{language}.json').is_file(), language) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/scripts/tests/test_oci_recovery_inventory_wording.py b/.github/scripts/tests/test_oci_recovery_inventory_wording.py index 77e7afaa..338698ae 100644 --- a/.github/scripts/tests/test_oci_recovery_inventory_wording.py +++ b/.github/scripts/tests/test_oci_recovery_inventory_wording.py @@ -5,6 +5,7 @@ import json from pathlib import Path import re import runpy +import subprocess import sys import tempfile from types import ModuleType, SimpleNamespace @@ -52,7 +53,8 @@ class InventoryMessages(unittest.TestCase): state = ModuleType('oci_installation_state') state.parse_config = lambda raw: dict(line.split(': ', 1) for line in raw.decode().splitlines()) scope = {'sys': SimpleNamespace(path=[]), 'source_text': lambda text: text or '', - 'translate': translate, 're': re} + 'translate': translate, 're': re, 'subprocess': subprocess, + '_selected_configuration': remover.guest_config} summary = extracted(MENU, '_removal_summary', scope) with patch.dict(sys.modules, {'oci_instances': instances, 'oci_remove': remover, 'oci_installation_state': state}): diff --git a/.github/scripts/tests/test_oci_removal_firewall_wording.py b/.github/scripts/tests/test_oci_removal_firewall_wording.py index bde90937..8041f923 100644 --- a/.github/scripts/tests/test_oci_removal_firewall_wording.py +++ b/.github/scripts/tests/test_oci_removal_firewall_wording.py @@ -56,7 +56,8 @@ class RemovalWordings(unittest.TestCase): state = ModuleType('oci_installation_state') state.parse_config = lambda raw: {} scope = {'sys': SimpleNamespace(path=[]), 'source_text': lambda value: value or '', - 'translate': translated or (lambda value: value), 're': re} + 'translate': translated or (lambda value: value), 're': re, + '_selected_configuration': remover.guest_config, 'subprocess': subprocess} with patch.dict(sys.modules, {'oci_instances': instances, 'oci_remove': remover, 'oci_installation_state': state}): return extract(MENU, '_removal_summary', scope)(Path('/inert'), 101) @@ -87,7 +88,8 @@ class RemovalWordings(unittest.TestCase): with patch.dict(sys.modules, {'oci_instances': instances, 'oci_remove': remover, 'oci_installation_state': state}): scope = {'sys': SimpleNamespace(path=[]), 'source_text': lambda value: value or '', - 'translate': lambda value: value, 're': re} + 'translate': lambda value: value, 're': re, + '_selected_configuration': remover.guest_config, 'subprocess': subprocess} preview = extract(MENU, '_removal_summary', scope)(Path('/inert'), 102) self.assertIn(WHOLE, preview) diff --git a/.github/scripts/tests/test_oci_selection_setup_wording.py b/.github/scripts/tests/test_oci_selection_setup_wording.py index 9d35d7d5..7baa2ad5 100644 --- a/.github/scripts/tests/test_oci_selection_setup_wording.py +++ b/.github/scripts/tests/test_oci_selection_setup_wording.py @@ -42,13 +42,13 @@ class SelectionSetupWording(TestCase): shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: [], _clean_orphans=lambda project: None, recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None, - carry_records=lambda project, **options: {}, + carry_records=lambda project, **options: {}, _records_unread=lambda project: False, translate=lambda s: s) fn(Path('/fixture'), ui) ui.message.assert_called_once_with(EMPTY, 'OCI management') ui.choose.assert_not_called() - def _menu(self, inventories, selections): + def _menu(self, inventories, selections, unread=True): rows = iter(inventories) ui = SimpleNamespace(message=Mock(), choose=Mock(side_effect=selections)) refresh = Mock(return_value={}) @@ -57,7 +57,8 @@ class SelectionSetupWording(TestCase): shutil=SimpleNamespace(which=lambda _: '/fake/pct'), saved_inventory=lambda _: next(rows), _clean_orphans=lambda project: None, recover_automatically=lambda project: None, offer_recovery=lambda project, ui: None, - carry_records=refresh, manage_instance=manage, menu_size=lambda: 10, + carry_records=refresh, _records_unread=lambda project: unread, + manage_instance=manage, menu_size=lambda: 10, STATUS_LABELS={}, translate=lambda s: s) fn(Path('/fixture'), ui) return ui, refresh, manage @@ -67,7 +68,14 @@ class SelectionSetupWording(TestCase): ui, refresh, manage = self._menu([[row], [row]], ['101', None]) self.assertEqual(ui.choose.call_count, 2) manage.assert_called_once() - refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=True) + refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=True, + heading='Manage installed OCI applications') + + def test_the_list_opens_without_the_sync_notice_once_every_configuration_is_cached(self): + row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'} + _, refresh, _ = self._menu([[row]], [None], unread=False) + refresh.assert_called_once_with(Path('/fixture'), mount_stopped=False, shown=False, + heading='Manage installed OCI applications') def test_removing_the_last_application_returns_without_a_message(self): row = {'vmid': 101, 'title': 'Jellyfin', 'hostname': 'jellyfin', 'status': 'installed', 'image': 'jellyfin/jellyfin'} diff --git a/.github/scripts/tests/test_oci_update_preservation_wording.py b/.github/scripts/tests/test_oci_update_preservation_wording.py index 44146970..e3fdae92 100644 --- a/.github/scripts/tests/test_oci_update_preservation_wording.py +++ b/.github/scripts/tests/test_oci_update_preservation_wording.py @@ -3,6 +3,7 @@ import ast from contextlib import nullcontext import copy import json +import os from pathlib import Path import re import runpy @@ -129,7 +130,9 @@ class UpdateWording(unittest.TestCase): reconcile = ModuleType('oci_instance_reconcile') reconcile.propose = lambda *args: None scope = {'sys': SimpleNamespace(path=[], executable='python3'), + 'os': os, 'tempfile': tempfile, 'Path': Path, 'translate': lambda text: text, 'check_selected': lambda project, row: row, + '_selected_configuration': lambda vmid: instances.command('pct', 'config', str(vmid)), 'images': SimpleNamespace(offer_removal=lambda *args: None), 'carry_records': lambda project, **options: {}, '_work_backup_ready': lambda project, ui, vmid: True, diff --git a/.github/scripts/tests/test_terminal_catalogs.py b/.github/scripts/tests/test_terminal_catalogs.py new file mode 100644 index 00000000..f74966cb --- /dev/null +++ b/.github/scripts/tests/test_terminal_catalogs.py @@ -0,0 +1,82 @@ +"""Offline regressions for copyable commands and runtime format fields.""" +import importlib.util +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SCRIPTS = Path(__file__).resolve().parents[1] +ROOT = SCRIPTS.parents[1] +sys.path.insert(0, str(SCRIPTS)) +SPEC = importlib.util.spec_from_file_location('check_terminal_catalogs', SCRIPTS / 'check_terminal_catalogs.py') +MODULE = importlib.util.module_from_spec(SPEC) +assert SPEC and SPEC.loader +SPEC.loader.exec_module(MODULE) + + +class TerminalCatalogsTests(unittest.TestCase): + def test_extractor_includes_launchers_remote_scripts_and_catalog_text(self): + fixtures = { + 'menu': 'translate "Main launcher"', + 'install_proxmenux.sh': 'translate "Stable installer"', + 'install_proxmenux_beta.sh': 'translate "Beta installer"', + 'scripts/check.sh': 'translate "Host shell script"', + 'scripts/check.py': 'translate("Host Python script")', + 'oci/src/check.py': 'N_("OCI Python script")', + 'oci/remote/check.py': 'translate("Remote Python script")', + 'oci/remote/check.sh': 'translate "Remote shell script"', + 'oci/catalog/app.json': json.dumps({'catalog_ui': {'tagline': {'en_US': 'Application tagline'}}}), + } + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for name, content in fixtures.items(): + file = root / name + file.parent.mkdir(parents=True, exist_ok=True) + file.write_text(content, encoding='utf-8') + self.assertEqual(set(MODULE.active_sources(root)), { + 'Main launcher', 'Stable installer', 'Beta installer', + 'Host shell script', 'Host Python script', 'OCI Python script', + 'Remote Python script', 'Remote shell script', 'Application tagline', + }) + + def test_runtime_format_field_cannot_be_translated(self): + source = 'Kimai has no default account. Enter the container with: pct enter {main_vmid}' + with self.assertRaisesRegex(ValueError, 'placeholder'): + MODULE.validate_message(source, 'Kimai nemá účet: pct enter {main vmid}') + MODULE.validate_message(source, 'Kimai nemá účet: pct enter {main_vmid}') + + def test_copyable_command_and_environment_variable_are_literal(self): + examples = [ + ('Example: pct config 110 | grep unprivileged', 'Ejemplo: configuración pct 110 | grep sin privilegios'), + ("Use 'pct restore' to recover the disks.", "Verwenden Sie 'pct restart', um die Datenträger wiederherzustellen."), + ('Invalid ALLOWED_HOSTS value', 'Valeur HÔTES INDÉSIRABLES'), + ] + for source, target in examples: + with self.subTest(source=source): + with self.assertRaisesRegex(ValueError, 'literal command or identifier'): + MODULE.validate_message(source, target) + MODULE.validate_message('Example: pct config 110 | grep unprivileged', + 'Ejemplo: pct config 110 | grep unprivileged') + + def test_dns_capitalization_does_not_relax_file_path_case(self): + MODULE.validate_message('Cannot reach download.proxmox.com.', + 'Download.proxmox.com ist nicht erreichbar.') + with self.assertRaisesRegex(ValueError, 'literal command or identifier'): + MODULE.validate_message('Check /etc/pve/storage.cfg', 'Prüfen Sie /etc/PVE/storage.cfg') + + 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'], + 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) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/scripts/tests/test_web_docs_catalogs.py b/.github/scripts/tests/test_web_docs_catalogs.py new file mode 100644 index 00000000..3d851425 --- /dev/null +++ b/.github/scripts/tests/test_web_docs_catalogs.py @@ -0,0 +1,94 @@ +"""Validate complete catalogs without rejecting intentional English fallback.""" +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +SCRIPT = Path(__file__).resolve().parents[1] / "check_web_docs_catalogs.py" + + +class WebDocsCatalogTests(unittest.TestCase): + def run_check(self, root, *options): + return subprocess.run( + [sys.executable, "-B", str(SCRIPT), "--messages-dir", str(root), + "--languages", "de", *options], + capture_output=True, text=True, + ) + + def write_catalog(self, root, locale, content): + file = root / locale / "common.json" + file.parent.mkdir(parents=True, exist_ok=True) + file.write_text(json.dumps(content), encoding="utf-8") + + def test_partial_catalog_keeps_fallback_but_complete_mode_reports_gap(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.write_catalog(root, "en", {"title": "Updates", "body": "Check the host."}) + self.write_catalog(root, "de", {"title": "Aktualisierungen"}) + partial = self.run_check(root) + self.assertEqual(partial.returncode, 0, partial.stdout + partial.stderr) + self.assertIn("missing=1", partial.stdout) + complete = self.run_check(root, "--require-complete") + self.assertEqual(complete.returncode, 1, complete.stdout + complete.stderr) + self.assertIn("schema differs", complete.stdout) + + def test_translated_code_is_rejected_in_both_modes(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.write_catalog(root, "en", {"body": "Run apt update."}) + self.write_catalog(root, "de", {"body": "Führe apt aktualisieren aus."}) + for options in ((), ("--require-complete",)): + with self.subTest(options=options): + result = self.run_check(root, *options) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + + def test_bare_commands_and_operators_are_not_prose(self): + examples = [ + ("Restore with pct restore.", "Mit pct restart wiederherstellen."), + ("pveversion | grep pve-manager", "pveversion grep pve-manager"), + ("cd /opt/my-app && ./update.sh", "cd /opt/meine-app && ./update.sh"), + ] + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for source, target in examples: + with self.subTest(source=source): + self.write_catalog(root, "en", {"body": source}) + self.write_catalog(root, "de", {"body": target}) + result = self.run_check(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("literal command or identifier", result.stdout) + + def test_title_separator_can_change_without_relaxing_body_pipes(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.write_catalog(root, "en", {"meta": {"title": "Storage | ProxMenux"}}) + self.write_catalog(root, "de", {"meta": {"title": "Speicher — ProxMenux"}}) + result = self.run_check(root, "--require-complete") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.write_catalog(root, "en", {"meta": {"description": "pveversion | grep pve-manager"}}) + self.write_catalog(root, "de", {"meta": {"description": "pveversion grep pve-manager"}}) + result = self.run_check(root) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + + def test_shipped_web_catalogs_keep_literal_contracts(self): + messages = SCRIPT.parents[2] / "web" / "messages" + result = subprocess.run( + [sys.executable, "-B", str(SCRIPT), "--messages-dir", str(messages)], + capture_output=True, text=True, timeout=60, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_literal_metadata_is_required_but_labels_can_be_localized(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self.write_catalog(root, "en", {"label": "Updates", "tone": "blue"}) + self.write_catalog(root, "de", {"label": "Aktualisierungen", "tone": "blau"}) + invalid = self.run_check(root, "--require-complete") + self.assertEqual(invalid.returncode, 1, invalid.stdout + invalid.stderr) + self.assertIn("rendering metadata differ", invalid.stdout) + self.write_catalog(root, "de", {"label": "Aktualisierungen", "tone": "blue"}) + valid = self.run_check(root, "--require-complete") + self.assertEqual(valid.returncode, 0, valid.stdout + valid.stderr) diff --git a/.github/scripts/tests/test_web_export_language.py b/.github/scripts/tests/test_web_export_language.py new file mode 100644 index 00000000..143b0ccb --- /dev/null +++ b/.github/scripts/tests/test_web_export_language.py @@ -0,0 +1,41 @@ +"""Exercise the static-export language repair on real HTML files.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + +from test_command_descriptions import require_node + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = ROOT / "web/scripts/localize-export.mjs" + + +class WebExportLanguageTests(unittest.TestCase): + def test_locales_nested_pages_preserved_content_and_idempotence(self): + node = require_node() + html = '

Text

' + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + locales = ["en", "es", "de", "fr", "it", "pt", "sk", "sv"] + for locale in locales: + file = root / "out" / locale / "docs/monitor/index.html" + file.parent.mkdir(parents=True) + file.write_text(html) + for relative in ["out/index.html", "out/assets/index.html"]: + file = root / relative + file.parent.mkdir(parents=True, exist_ok=True) + file.write_text(html) + result = subprocess.run([node, str(SCRIPT)], cwd=root, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("8 localized pages", result.stdout) + for locale in locales: + actual = (root / "out" / locale / "docs/monitor/index.html").read_text() + self.assertEqual(actual, html.replace('=3,<4' + - name: Run qualified Python files env: PYTHONDONTWRITEBYTECODE: '1' run: python3 -I -B .github/scripts/run_offline_qualified.py --lane python @@ -82,7 +86,7 @@ jobs: run: | npm ci --prefix .github/ci/offline-node --legacy-peer-deps --ignore-scripts --no-audit --no-fund ln -s ../.github/ci/offline-node/node_modules AppImage/node_modules - - name: Run eleven qualified Node files + - name: Run qualified Node files env: NODE_PATH: ${{ github.workspace }}/AppImage/node_modules PYTHONDONTWRITEBYTECODE: '1' diff --git a/.github/workflows/test-pbs.yml b/.github/workflows/test-pbs.yml index 2d60623c..2dbc71b0 100644 --- a/.github/workflows/test-pbs.yml +++ b/.github/workflows/test-pbs.yml @@ -17,6 +17,9 @@ on: - 'scripts/global/repository-functions.sh' - 'scripts/menus/pbs_menu.sh' - 'scripts/utilities/pbs_update.sh' + - 'scripts/global/update-pbs.sh' + - 'scripts/menus/menu_post_install.sh' + - 'scripts/post_install/*.sh' push: branches: [main, develop] paths: @@ -49,7 +52,7 @@ jobs: run: | python -B -m unittest discover -s AppImage/scripts/tests -p test_pbs.py -v python -B -m unittest discover -s AppImage/scripts/tests -p test_auth_manager_ssl_reload.py -v - for script in scripts/global/platform-functions.sh scripts/global/repository-functions.sh scripts/menus/pbs_menu.sh scripts/utilities/pbs_update.sh scripts/menus/main_menu.sh scripts/post_install/auto_post_install.sh scripts/post_install/customizable_post_install.sh; do + for script in scripts/global/platform-functions.sh scripts/global/repository-functions.sh scripts/menus/pbs_menu.sh scripts/utilities/pbs_update.sh scripts/menus/main_menu.sh scripts/post_install/auto_post_install.sh scripts/post_install/customizable_post_install.sh scripts/post_install/uninstall-tools.sh scripts/global/update-pbs.sh scripts/menus/menu_post_install.sh; do bash -n "$script" done - uses: actions/setup-node@v6 diff --git a/AppImage/components/health-status-modal.tsx b/AppImage/components/health-status-modal.tsx index c8be541e..090a51fb 100644 --- a/AppImage/components/health-status-modal.tsx +++ b/AppImage/components/health-status-modal.tsx @@ -42,6 +42,7 @@ import { } from "lucide-react" import { ScriptTerminalModal } from "./script-terminal-modal" import { useT } from "@/lib/i18n/provider" +import { useIsPbs } from "@/lib/use-platform" interface CategoryCheck { status: string @@ -119,6 +120,7 @@ const CATEGORIES = [ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatusModalProps) { const t = useT() + const isPbs = useIsPbs() const [loading, setLoading] = useState(true) const [healthData, setHealthData] = useState(null) const [dismissedItems, setDismissedItems] = useState([]) @@ -460,6 +462,11 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu return originalStatus?.toUpperCase() || "UNKNOWN" } + // Checks the host reports as not applicable (guests on PBS) are not listed or counted. + const visibleCategories = CATEGORIES.filter(({ key }) => + !(healthData?.details?.[key as keyof typeof healthData.details] as { not_applicable?: boolean } | undefined)?.not_applicable) + const categoryLabel = (key: string) => t(`healthStatus.categories.${isPbs && key === "services" ? "pbsServices" : key}`) + const getHealthStats = () => { if (!healthData?.details) return { total: 0, healthy: 0, info: 0, warnings: 0, critical: 0, unknown: 0 } @@ -469,7 +476,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu let critical = 0 let unknown = 0 - CATEGORIES.forEach(({ key }) => { + visibleCategories.forEach(({ key }) => { const categoryData = healthData.details[key as keyof typeof healthData.details] if (categoryData) { const effectiveStatus = getEffectiveStatus(key, categoryData.status) @@ -481,7 +488,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu } }) - return { total: CATEGORIES.length, healthy, info, warnings, critical, unknown } + return { total: visibleCategories.length, healthy, info, warnings, critical, unknown } } const stats = getHealthStats() @@ -759,7 +766,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu {/* Category List */}
- {CATEGORIES.map(({ key, Icon }) => { + {visibleCategories.map(({ key, Icon }) => { const categoryData = healthData.details[key as keyof typeof healthData.details] const originalStatus = categoryData?.status || "UNKNOWN" const status = getEffectiveStatus(key, originalStatus) @@ -784,7 +791,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
-

{t(`healthStatus.categories.${key}`)}

+

{categoryLabel(key)}

{hasChecks && ( ({Object.values(checks).filter(c => c.installed !== false).length}) @@ -871,7 +878,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu {filteredDismissed.map((item) => { const catMeta = CATEGORIES.find(c => c.category === item.category || c.key === item.category) const CatIcon = catMeta?.Icon || BellOff - const catLabel = catMeta ? t(`healthStatus.categories.${catMeta.key}`) : item.category + const catLabel = catMeta ? categoryLabel(catMeta.key) : item.category const isPermanent = item.permanent || item.suppression_remaining_hours === -1 return ( @@ -936,7 +943,7 @@ export function HealthStatusModal({ open, onOpenChange, getApiUrl }: HealthStatu
- {catMeta ? t(`healthStatus.categories.${catMeta.key}`) : cs.label} + {catMeta ? categoryLabel(catMeta.key) : cs.label}
{durationLabel} diff --git a/AppImage/components/lxc-app-panel.tsx b/AppImage/components/lxc-app-panel.tsx index 59939f0b..22154ef6 100644 --- a/AppImage/components/lxc-app-panel.tsx +++ b/AppImage/components/lxc-app-panel.tsx @@ -19,7 +19,7 @@ * Secure Gateway. */ -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Loader2, Save, RefreshCw, Trash2, Package, ExternalLink, AlertTriangle, Info, PlusCircle, Pencil, ChevronDown, ChevronRight, EyeOff, @@ -93,6 +93,7 @@ interface AppConfig { health_path?: string logo_url?: string helper_slug?: string + discovery_id?: string // Set when the application runs inside Docker: registered for its identity // and installed version, updated by the image it comes from. update_via?: string @@ -121,7 +122,18 @@ interface PortDetail { logo_url: string } +interface NativeDiscovery { + vmid: number + status: "not_scanned" | "complete" | "partial" | "error" | "busy" + stale: boolean + candidates: DetectedApp[] +} + interface DetectedApp { + discovery_id?: string + source?: "native_metadata" + installed_version?: string | null + evidence?: Array<{ kind: string; value: string }> slug: string name: string logo_url?: string | null @@ -462,12 +474,37 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: const [loading, setLoading] = useState(!seed) const [sidecar, setSidecar] = useState(seed?.sidecar ?? null) const [suggestions, setSuggestions] = useState(seed?.suggestions ?? null) + const [nativeDiscovery, setNativeDiscovery] = useState(null) + const nativeRequest = useRef(0) + const targetRef = useRef(vmid) + targetRef.current = vmid + const targetGeneration = useRef(0) + const previousTarget = useRef(vmid) + if (previousTarget.current !== vmid) { + previousTarget.current = vmid + targetGeneration.current += 1 + } + useEffect(() => { + const generation = targetGeneration.current + const request = ++nativeRequest.current + setNativeDiscovery(null) + setEditing(null) + setDetectionNotice(null) + setSearchingApplications(false) + setSaving(false) + if (managed) return + fetchApi(`/api/vms/${vmid}/apps/native-discovery`).then(result => { + if (targetRef.current === vmid && targetGeneration.current === generation + && nativeRequest.current === request) setNativeDiscovery(result) + }).catch(() => { /* cached metadata is optional; never initiate a scan */ }) + return () => { targetGeneration.current += 1 } + }, [vmid, managed]) const [adguardSetupAvailable, setAdguardSetupAvailable] = useState(false) const [error, setError] = useState(null) const [searchingApplications, setSearchingApplications] = useState(false) const [detectionNotice, setDetectionNotice] = useState<{ found: boolean; text: string } | null>(null) // Editor state - const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig } | null>(null) + const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig; targetVmid: number } | null>(null) const [binaryArgsInput, setBinaryArgsInput] = useState("") const [commandArgvInput, setCommandArgvInput] = useState("") const [saving, setSaving] = useState(false) @@ -522,6 +559,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: // `initialData` or the shared cache, the sidecar is already // populated and the user shouldn't see a flash of "Loading…" // while we revalidate. + const generation = targetGeneration.current + const isCurrent = () => targetRef.current === vmid && targetGeneration.current === generation if (!sidecar) setLoading(true) setError(null) try { @@ -533,14 +572,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: // the "Loading applications…" flash that used to show while a // second, racing fetch caught up. const bundle = await fetchLxcApps(vmid) - if (bundle) { + if (bundle && isCurrent()) { setSidecar(bundle.sidecar) setSuggestions(bundle.suggestions) } } catch (e: any) { - setError(e?.message || t("vmLxc.appEditor.loadFailed")) + if (isCurrent()) setError(e?.message || t("vmLxc.appEditor.loadFailed")) } finally { - setLoading(false) + if (isCurrent()) setLoading(false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [vmid, managed]) @@ -617,7 +656,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: return msg } - const editorOpen = !!editing + const editorOpen = !!editing && editing.targetVmid === vmid // The picker catalog is only needed after the user opens the editor. useEffect(() => { @@ -653,8 +692,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: // (`extras[]` from the backend). Both use the same DetectedApp // shape so the empty state renders them uniformly. const detectedList: DetectedApp[] = useMemo(() => { - if (!suggestions) return [] - const out: DetectedApp[] = [] + const out: DetectedApp[] = [...(nativeDiscovery?.vmid === vmid ? nativeDiscovery.candidates : [])] + if (!suggestions) return out // A ProxMenux OCI install has no helper slug — nothing was installed by a // community script — so its template id identifies it instead. const primarySlug = suggestions.helper_slug || suggestions.oci_instance?.template_id @@ -679,12 +718,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: } } return out - }, [suggestions]) + }, [suggestions, nativeDiscovery, vmid]) // Registered slugs — used to filter the detection list down to // what the user hasn't already registered on this CT. const registeredSlugs = useMemo( - () => new Set(apps.map((a) => a.helper_slug).filter(Boolean) as string[]), + () => new Set(apps.flatMap((a) => [a.helper_slug, a.discovery_id]).filter(Boolean) as string[]), [apps], ) // Dismissed slugs — persisted in the sidecar. Chips the user @@ -750,14 +789,32 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: } const searchInstalledApplications = async () => { + const generation = targetGeneration.current + const request = ++nativeRequest.current + const isCurrent = () => targetRef.current === vmid && targetGeneration.current === generation + && nativeRequest.current === request const before = new Set([...visibleDetected, ...visibleWorkloads].map((item) => item.slug)) setSearchingApplications(true) setDetectionNotice(null) setError(null) try { + let native = await fetchApi(`/api/vms/${vmid}/apps/native-discovery`, { + method: "POST", + }) + if (!isCurrent()) return + setNativeDiscovery(native) + if (native.status === "error" || native.status === "busy") { + setDetectionNotice({ found: false, text: t("vmLxc.appEditor.nativeScanFailed") }) + return + } const result: Suggestions = await fetchApi(`/api/vms/${vmid}/apps/suggestions`, { method: "POST", }) + if (!isCurrent()) return + // Recompose against the refreshed catalog without another native scan. + native = await fetchApi(`/api/vms/${vmid}/apps/native-discovery`) + if (!isCurrent()) return + setNativeDiscovery(native) setSuggestions(result) if (sidecar) setLxcAppsCached(vmid, sidecar, result) @@ -765,11 +822,14 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: if (result.helper_slug) detected.add(result.helper_slug) for (const item of result.extras || []) detected.add(item.slug) for (const item of result.docker_workloads || []) detected.add(item.slug) + for (const item of native.candidates) detected.add(item.slug) const visible = [...detected].filter( (slug) => !registeredSlugs.has(slug) && !dismissedSlugs.has(slug), ) const newCount = visible.filter((slug) => !before.has(slug)).length - if (newCount === 1) { + if (native.status === "partial") { + setDetectionNotice({ found: visible.length > 0, text: t("vmLxc.appEditor.nativeScanPartial") }) + } else if (newCount === 1) { setDetectionNotice({ found: true, text: t("vmLxc.appEditor.oneNewApplicationDetected") }) } else if (newCount > 1) { setDetectionNotice({ @@ -785,9 +845,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: }) } } catch (e: any) { - setError(e?.message || t("vmLxc.appEditor.detectionFailed")) + if (isCurrent()) setError(e?.message || t("vmLxc.appEditor.detectionFailed")) } finally { - setSearchingApplications(false) + if (isCurrent()) setSearchingApplications(false) } } @@ -804,6 +864,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: existing?: AppEntry, opts?: { withTracking?: boolean, preset?: DetectedApp }, ) => { + const generation = targetGeneration.current let seed: AppConfig if (existing) { seed = { @@ -832,6 +893,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: health_path: existing.health_path || "", logo_url: existing.logo_url || "", helper_slug: existing.helper_slug || "", + ...(existing.discovery_id ? { discovery_id: existing.discovery_id } : {}), // Preserved explicitly: saving replaces the whole record, so dropping // it here would silently un-delegate the app the first time someone // edits one of its ports. @@ -862,7 +924,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: const p = opts.preset seed.name = p.name seed.logo_url = p.logo_url || "" - seed.helper_slug = p.slug + if (p.discovery_id) { + seed.discovery_id = p.discovery_id + seed.update_method = "none" + seed.update_command = "" + } else seed.helper_slug = p.slug // Docker endpoints come from the real published host-port mappings // listed under Web links. Do not pre-save a catalog default such as // 9000; the user explicitly chooses which workload links to add. @@ -943,9 +1009,10 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: setShowAdvanced(false) } } + if (targetRef.current !== vmid || targetGeneration.current !== generation) return setBinaryArgsInput((seed.binary_args || []).join(", ")) setCommandArgvInput((seed.command_argv || []).join(", ")) - setEditing({ appId: existing?.id || null, draft: seed }) + setEditing({ appId: existing?.id || null, draft: seed, targetVmid: vmid }) setDetectorTest(null) setError(null) }, [suggestions, vmid, sidecar, isOciAdguard, adguardSetupAvailable]) @@ -959,7 +1026,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: } const save = async () => { - if (!editing) return + if (!editing || editing.targetVmid !== vmid || targetRef.current !== vmid) return + const generation = targetGeneration.current + const isCurrent = () => targetRef.current === vmid && targetGeneration.current === generation setSaving(true) setError(null) try { @@ -973,6 +1042,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: body: JSON.stringify(editing.draft), }) if ((r as any).error) throw new Error((r as any).error) + if (!isCurrent()) return setSidecar(r) setLxcAppsCached(vmid, r, suggestions) setEditing(null) @@ -980,9 +1050,9 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: setCommandArgvInput("") onChange?.() } catch (e: any) { - setError(e?.message || t("vmLxc.appEditor.saveFailed")) + if (isCurrent()) setError(e?.message || t("vmLxc.appEditor.saveFailed")) } finally { - setSaving(false) + if (isCurrent()) setSaving(false) } } @@ -1283,7 +1353,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: } // ── Editor ───────────────────────────────────────────────────── - if (editing) { + if (editorOpen && editing) { const draft = editing.draft const method = draft.installed_via || "" const isPackaged = method === "dpkg" || method === "apk" @@ -2452,6 +2522,24 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData, oci }: ? t("vmLxc.appEditor.versionDetected", { version: d.tracking_suggestion.detected_version || "" }) : t("vmLxc.appEditor.detectedInContainer")}
+ {d.source === "native_metadata" && ( +
+
{d.installed_version + ? t("vmLxc.appEditor.versionDetected", { version: d.installed_version }) + : t("vmLxc.appEditor.nativeVersionUnknown")}
+ {(nativeDiscovery?.stale || nativeDiscovery?.status === "partial") && ( +
{t("vmLxc.appEditor.nativeEvidenceStale")}
+ )} + {d.evidence?.map((e, index) =>
+ {t(e.kind === "tcp_listener" ? "vmLxc.appEditor.nativeEvidencePort" + : e.kind === "installation_file" ? "vmLxc.appEditor.nativeEvidenceFile" + : e.kind === "process_executable" ? "vmLxc.appEditor.nativeEvidenceProcess" + : e.kind === "working_directory" ? "vmLxc.appEditor.nativeEvidenceDirectory" + : "vmLxc.appEditor.nativeEvidenceService")}: {e.value} +
)} +
{t("vmLxc.appEditor.nativeRegisterOnly")}
+
+ )} {d.tracking_suggestion?.detector_source === "legacy_fallback" && (
{t("vmLxc.appEditor.legacyDetectorUsed")} diff --git a/AppImage/components/monitor-switcher.tsx b/AppImage/components/monitor-switcher.tsx new file mode 100644 index 00000000..0cd2d34b --- /dev/null +++ b/AppImage/components/monitor-switcher.tsx @@ -0,0 +1,283 @@ +"use client" + +import { useEffect, useState } from "react" +import useSWR, { mutate as refreshCache } from "swr" +import { ArrowLeft, ChevronDown, DatabaseBackup, ExternalLink, Plus, Server, Trash2 } from "lucide-react" +import { Button } from "./ui/button" +import { Input } from "./ui/input" +import { Label } from "./ui/label" +import { Switch } from "./ui/switch" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog" +import { + DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, +} from "./ui/dropdown-menu" +import { fetchApi } from "../lib/api-config" +import { useT } from "../lib/i18n/provider" + +interface MonitorLink { + key: string + name: string + product: "pve" | "pbs" + group: "cluster" | "standalone" | "pbs" + url: string | null + status: "online" | "offline" | "unknown" + native_url?: string | null + manual: boolean + hidden: boolean + custom_url: boolean +} +interface MonitorLinks { self: { name: string; product: string }; links: MonitorLink[] } + +const GROUPS = ["cluster", "standalone", "pbs"] as const +const fetcher = (url: string) => fetchApi(url) +const ORIGIN_KEY = "proxmenux-came-from" + +interface Origin { url: string; name: string; product: "pve" | "pbs" } + +// The Monitor a jump came from, as the browser reached it. Only a bare http(s) +// origin is accepted, and the parameters leave the address at once. +function takeOrigin(): (Origin & { fresh?: boolean }) | null { + if (typeof window === "undefined") return null + const params = new URLSearchParams(window.location.search) + const from = params.get("pmx_from") + if (from !== null) { + const name = params.get("pmx_name") || "" + const product = params.get("pmx_product") === "pbs" ? "pbs" : "pve" + for (const key of ["pmx_from", "pmx_name", "pmx_product"]) params.delete(key) + const query = params.toString() + window.history.replaceState(null, "", window.location.pathname + (query ? `?${query}` : "") + window.location.hash) + try { + const parsed = new URL(from) + if (/^https?:$/.test(parsed.protocol) && parsed.origin === from && parsed.origin !== window.location.origin) { + const origin: Origin = { url: parsed.origin, name: (name || parsed.hostname).slice(0, 64), product } + try { sessionStorage.setItem(ORIGIN_KEY, JSON.stringify(origin)) } catch { /* private mode */ } + return { ...origin, fresh: true } + } + } catch { /* not an address */ } + } + try { + const stored = JSON.parse(sessionStorage.getItem(ORIGIN_KEY) || "null") + return stored && typeof stored.url === "string" && /^https?:\/\//.test(stored.url) ? stored : null + } catch { return null } +} + +// Where a link leads: its Monitor, or the native PBS interface when no Monitor answers there. +function destination(link: MonitorLink): { href: string; native: boolean } | null { + if (link.product === "pbs" && link.status !== "online" && link.native_url) return { href: link.native_url, native: true } + return link.url ? { href: link.url, native: false } : null +} + +function ProductIcon({ product, className }: { product: string; className?: string }) { + return product === "pbs" ? : +} + +export function MonitorSwitcher({ nodeName, compact = false }: { nodeName: string; compact?: boolean }) { + const t = useT() + const [manageOpen, setManageOpen] = useState(false) + // The backend keeps the list ready, so the menu opens already filled. + const { data, error, isLoading } = useSWR("/api/monitor-links", fetcher, { revalidateOnFocus: false, refreshInterval: 300000 }) + + const [origin, setOrigin] = useState<(Origin & { fresh?: boolean }) | null>(null) + const [offerOrigin, setOfferOrigin] = useState(false) + const [adding, setAdding] = useState(false) + const [addError, setAddError] = useState("") + useEffect(() => { setOrigin(takeOrigin()) }, []) + const originKnown = !!origin && !!data?.links.some(link => link.url === origin.url) + useEffect(() => { + // Offered once, right after arriving, when this Monitor has no way back yet. + if (origin?.fresh && data && !originKnown) setOfferOrigin(true) + }, [origin, data, originKnown]) + + // The address carries where the jump comes from, so the other Monitor can offer the way back. + const withOrigin = (href: string) => { + const params = new URLSearchParams({ pmx_from: window.location.origin, pmx_name: nodeName, pmx_product: data?.self.product === "pbs" ? "pbs" : "pve" }) + return `${href}/?${params.toString()}` + } + const open = (href: string, newTab: boolean) => { + if (newTab) window.open(href, "_blank", "noopener,noreferrer") + else window.location.assign(href) + } + const go = (link: MonitorLink, newTab: boolean) => { + const target = destination(link) + if (!target) return + if (target.native) open(target.href, true) + else open(withOrigin(target.href), newTab) + } + const addOrigin = async () => { + if (!origin) return + setAdding(true); setAddError("") + try { + await fetchApi("/api/monitor-links", { method: "POST", body: JSON.stringify({ name: origin.name, url: origin.url, product: origin.product }) }) + await refreshCache("/api/monitor-links") + setOfferOrigin(false) + } catch (err) { + setAddError(err instanceof Error ? err.message : t("monitorLinks.saveFailed")) + } finally { setAdding(false) } + } + + return
event.stopPropagation()}> + + + + + + {t("monitorLinks.thisNode")} + + + + {nodeName} + + {origin && !originKnown && open(withOrigin(origin.url), false)}> + + {t("monitorLinks.backTo", { name: origin.name })} + } + {isLoading &&

{t("monitorLinks.checking")}

} + {error &&

{t("monitorLinks.loadFailed")}

} + {data && data.links.length === 0 &&

{t("monitorLinks.empty")}

} + {data && GROUPS.map(group => { + const rows = data.links.filter(link => link.group === group) + if (rows.length === 0) return null + return
+ + {t(`monitorLinks.groups.${group}`)} + {rows.map(link => { + const target = destination(link) + return go(link, false)}> + + + {link.name} + {target?.native + ? {t("monitorLinks.pbsInterface")} + : link.status !== "online" && {t("monitorLinks.noResponse")}} + {target && } + + })} +
+ })} + + setManageOpen(true)}> + {t("monitorLinks.manage")} + +
+
+ {manageOpen && } + {origin && + event.stopPropagation()}> + + {t("monitorLinks.cameFromTitle")} + {t("monitorLinks.cameFromBody", { name: origin.name })} + +

{origin.url}

+ {addError &&

{addError}

} +
+ + +
+
+
} +
+} + +function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const t = useT() + const { data, mutate } = useSWR("/api/monitor-links?all=1", fetcher, { revalidateOnFocus: false }) + const [form, setForm] = useState({ name: "", url: "", product: "pve" }) + const [addresses, setAddresses] = useState>({}) + const [busy, setBusy] = useState(false) + const [error, setError] = useState("") + + const run = async (action: () => Promise) => { + setBusy(true); setError("") + try { + await action() + await mutate() + await refreshCache("/api/monitor-links") + } catch (err) { + setError(err instanceof Error ? err.message : t("monitorLinks.saveFailed")) + } finally { setBusy(false) } + } + const send = (path: string, method: string, body?: unknown) => + fetchApi(path, { method, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }) + + return + event.stopPropagation()}> + + {t("monitorLinks.dialogTitle")} + {t("monitorLinks.dialogDescription")} + + +
+

{t("monitorLinks.addTitle")}

+
+
+ + setForm({ ...form, name: event.target.value })} /> +
+
+ + setForm({ ...form, url: event.target.value })} /> +
+
+ + +
+
+ +
+ + {error &&

{error}

} + +
+ {data?.links.length === 0 &&

{t("monitorLinks.empty")}

} + {data?.links.map(link => { + const address = addresses[link.key] ?? (link.manual || link.custom_url ? link.url || "" : "") + return
+
+ + {link.name} + {link.manual ? t("monitorLinks.added") : t("monitorLinks.detected")} + {!link.manual && } + {link.manual && } +
+
+ setAddresses({ ...addresses, [link.key]: event.target.value })} /> + +
+
+ })} +
+
+
+} diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index e84590ee..194d5574 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -13,6 +13,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog" import { fetchApi } from "../lib/api-config" import { useT } from "../lib/i18n/provider" +import { useIsPbs } from "../lib/use-platform" import { Bell, BellOff, Send, CheckCircle2, XCircle, Loader2, AlertTriangle, Info, Settings2, Zap, Eye, EyeOff, @@ -154,6 +155,8 @@ function validateGotifyUrl(url: string): { error?: string; warning?: string } { } const EVENT_CATEGORIES = ["vm_ct", "backup", "resources", "storage", "network", "security", "cluster", "services", "health", "updates", "hardware", "system", "other"].map(key => ({ key })) +// Guests, vzdump backups and the cluster do not exist on Proxmox Backup Server. +const PVE_ONLY_CATEGORIES = new Set(["vm_ct", "backup", "cluster"]) const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email", "pushover", "apprise"] as const @@ -330,6 +333,7 @@ const DEFAULT_CONFIG: NotificationConfig = { export function NotificationSettings() { const t = useT() + const isPbs = useIsPbs() const { resolvedTheme } = useTheme() const [config, setConfig] = useState(DEFAULT_CONFIG) const [status, setStatus] = useState(null) @@ -703,7 +707,7 @@ export function NotificationSettings() {
- {EVENT_CATEGORIES.filter(cat => cat.key !== "other").map(cat => { + {EVENT_CATEGORIES.filter(cat => cat.key !== "other" && !(isPbs && PVE_ONLY_CATEGORIES.has(cat.key))).map(cat => { const isEnabled = overrides.categories[cat.key] ?? true const isExpanded = expandedCategories.has(`${chName}.${cat.key}`) // Hide the LXC update toggle when the user has disabled the @@ -922,7 +926,7 @@ export function NotificationSettings() { const wasEnabled = originalConfig.enabled const isNowDisabled = !nextConfig.enabled - if (wasEnabled && isNowDisabled) { + if (wasEnabled && isNowDisabled && !isPbs) { try { await fetchApi("/api/notifications/proxmox/cleanup-webhook", { method: "POST" }) } catch { @@ -1217,8 +1221,10 @@ export function NotificationSettings() { setConfig(newConfig) setOriginalConfig(newConfig) - // 2) Auto-configure PVE webhook - try { + // 2) Auto-configure PVE webhook; PBS has no /etc/pve notifications + if (isPbs) { + setWebhookSetup({ status: "idle", fallback_commands: [], error: "" }) + } else try { const setup = await fetchApi<{ configured: boolean secret?: string diff --git a/AppImage/components/pbs-dashboard.tsx b/AppImage/components/pbs-dashboard.tsx deleted file mode 100644 index fd90b1b6..00000000 --- a/AppImage/components/pbs-dashboard.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client" - -import { useState } from "react" -import useSWR from "swr" -import { PBSPanel } from "./pbs-panel" -import { Security } from "./security" -import { SystemLogs } from "./system-logs" -import { TerminalPanel } from "./terminal-panel" -import { ThemeToggle } from "./theme-toggle" -import { Button } from "./ui/button" -import { Card, CardContent } from "./ui/card" -import { fetchApi } from "../lib/api-config" -import { useT } from "../lib/i18n/provider" - -interface Vitals { hostname: string; cpu_usage: number; memory_usage: number; memory_used: number; memory_total: number; uptime: string } - -export function PBSDashboard() { - const t = useT() - const [tab, setTab] = useState("overview") - const { data } = useSWR("/api/system", url => fetchApi(url), { refreshInterval: 10000 }) - return
-
-

ProxMenux · PBS

{data?.hostname || t("pbs.loading")}

- - -
- - {tab === "overview" && <> -
- {[[t("pbs.cpu"), data ? `${data.cpu_usage.toFixed(1)}%` : "—"], - [t("pbs.memory"), data ? `${data.memory_used.toFixed(1)} / ${data.memory_total.toFixed(1)} GiB (${data.memory_usage.toFixed(1)}%)` : "—"], - [t("pbs.uptime"), data?.uptime || "—"]].map(([label, value]) => -

{label}

{value}

)} -
- - } - {tab === "logs" && } - {tab === "terminal" && } - {tab === "security" && } -
-} diff --git a/AppImage/components/pbs-datastore-usage.tsx b/AppImage/components/pbs-datastore-usage.tsx new file mode 100644 index 00000000..944ba340 --- /dev/null +++ b/AppImage/components/pbs-datastore-usage.tsx @@ -0,0 +1,112 @@ +"use client" + +import { CalendarClock, Database } from "lucide-react" +import { Badge } from "./ui/badge" +import { Progress } from "./ui/progress" +import { formatStorage } from "../lib/utils" +import { useT } from "../lib/i18n/provider" + +export interface PBSDatastore { + name: string + path?: string + total?: number | null + used?: number | null + avail?: number | null + error?: string + "backend-type"?: string + "estimated-full-date"?: number + "gc-status"?: { upid?: string | null; "removed-bytes"?: number; "pending-bytes"?: number } +} + +export function datastorePercent(store: PBSDatastore): number | null { + if (store.total == null || store.used == null || store.total <= 0) return null + return (store.used / store.total) * 100 +} + +// Same thresholds and colours as the PVE storage cards. +export function usageTone(percent: number): { bar: string; text: string; used: string } { + if (percent > 90) return { bar: "[&>div]:bg-red-500", text: "text-red-500", used: "text-red-400" } + if (percent > 75) return { bar: "[&>div]:bg-yellow-500", text: "text-yellow-500", used: "text-yellow-400" } + return { bar: "[&>div]:bg-blue-500", text: "text-muted-foreground", used: "text-blue-400" } +} + +const gib = (bytes: number) => formatStorage(bytes / 1024 ** 3) + +// A datastore without a readable capacity keeps its message: it is never shown as empty. +export function DatastoreUsageRow({ store, compact = false }: { store: PBSDatastore; compact?: boolean }) { + const t = useT() + const percent = datastorePercent(store) + if (percent == null) { + return
+
+ {store.name} + — +
+

{store.error || t("pbs.capacityUnavailable")}

+
+ } + const tone = usageTone(percent) + return
+
+ {store.name} + {percent.toFixed(1)}% +
+ + {!compact &&
+ {gib(store.used!)} / {gib(store.total!)} + {t("pbs.free")}: {gib(store.avail ?? store.total! - store.used!)} +
} +
+} + +// Same layout as the "Proxmox storage" cards of the PVE Monitor. +export function PBSDatastoreCard({ store }: { store: PBSDatastore }) { + const t = useT() + const percent = datastorePercent(store) + const tone = percent == null ? null : usageTone(percent) + const fullDate = store["estimated-full-date"] + const type = store["backend-type"] || "datastore" + return
+
+
+ +

{store.name}

+ {type} +
+
+ + {store.error ? t("storage.status.error") : t("storage.status.active")} + + {percent != null && {percent.toFixed(2)}%} +
+
+ {percent != null && tone ? ( +
+ +
+
+

{t("storage.total")}

+

{gib(store.total!)}

+
+
+

{t("storage.used")}

+

{gib(store.used!)}

+
+
+

{t("storage.available")}

+

{gib(store.avail ?? store.total! - store.used!)}

+
+
+
+ ) : ( +

{store.error || t("storage.capacityNotReported")}

+ )} + {(store.path || fullDate !== undefined) &&
+ {store.path && {store.path}} + {fullDate !== undefined && + {t("pbs.estimatedFull")}: + {fullDate > 0 ? new Date(fullDate * 1000).toLocaleDateString() : t("pbs.estimatedFullUnknown")} + } +
} +
+} diff --git a/AppImage/components/pbs-panel.tsx b/AppImage/components/pbs-panel.tsx index fb1354eb..d71b30a9 100644 --- a/AppImage/components/pbs-panel.tsx +++ b/AppImage/components/pbs-panel.tsx @@ -2,29 +2,74 @@ import { useEffect, useState } from "react" import useSWR from "swr" +import { + Activity, ArchiveRestore, Database, DatabaseBackup, Download, ListChecks, + RefreshCw, Scissors, Server, Settings2, ShieldCheck, Terminal, Trash2, +} from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" +import { Badge } from "./ui/badge" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { PBSDatastoreCard, type PBSDatastore } from "./pbs-datastore-usage" import { fetchApi } from "../lib/api-config" -import { formatBytes } from "../lib/utils" import { useT } from "../lib/i18n/provider" interface Connection { id: string; name: string; url: string; token_id: string; fingerprint: string; has_token: boolean } interface Connections { local_available: boolean; servers: Connection[] } -interface Datastore { name: string; path?: string; total?: number | null; used?: number | null; avail?: number | null; error?: string } interface Task { upid: string; worker_type?: string; worker_id?: string; starttime?: number; endtime?: number; status?: string } interface Job { id: string; store?: string; schedule?: string; "last-run-state"?: string } interface PBSStatus { name: string; version: string | null; fetched_at: number; services: Record; - datastores: Datastore[] | null; tasks: Task[] | null; + datastores: PBSDatastore[] | null; tasks: Task[] | null; sync: Job[] | null; verify: Job[] | null; prune: Job[] | null; errors: Record; } const emptyForm = { id: "", name: "", url: "", token_id: "", fingerprint: "", token_secret: "" } -const selectClass = "h-10 rounded-md border bg-background px-3 text-sm" +const NEW_CONNECTION = "__new__" +const badgeTone = { + ok: "bg-green-500/10 text-green-500 border-green-500/20", + warn: "bg-amber-500/10 text-amber-500 border-amber-500/20", + error: "bg-red-500/10 text-red-500 border-red-500/20", + running: "bg-blue-500/10 text-blue-500 border-blue-500/20", + muted: "bg-muted text-muted-foreground border-border", +} -export function PBSPanel() { +const TASK_ICONS: Record = { + backup: DatabaseBackup, reader: ArchiveRestore, verify: ShieldCheck, verificationjob: ShieldCheck, + verify_group: ShieldCheck, verify_snapshot: ShieldCheck, prune: Scissors, prunejob: Scissors, + garbage_collection: Trash2, sync: RefreshCw, syncjob: RefreshCw, aptupdate: Download, + termproxy: Terminal, dircreate: Database, "create-datastore": Database, +} + +// PBS reports "OK", "WARNINGS: n" or the error text; a task without one is still running. +function taskState(task: Task): keyof typeof badgeTone { + if (!task.status) return task.endtime ? "muted" : "running" + if (task.status === "OK") return "ok" + if (task.status.startsWith("WARNINGS")) return "warn" + return "error" +} + +function jobState(state?: string): keyof typeof badgeTone { + if (!state) return "muted" + if (state === "ok") return "ok" + if (state.toLowerCase().startsWith("warn")) return "warn" + return "error" +} + +function formatDuration(seconds: number) { + if (!Number.isFinite(seconds) || seconds < 0) return "—" + if (seconds < 60) return `${Math.round(seconds)}s` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ${Math.round(seconds % 60)}s` + const hours = Math.floor(minutes / 60) + return hours < 24 ? `${hours}h ${minutes % 60}m` : `${Math.floor(hours / 24)}d ${hours % 24}h` +} + +// `localOnly` is the PBS host's own Monitor: no connection picker, the +// header Refresh already reloads the page data. +export function PBSPanel({ localOnly = false }: { localOnly?: boolean } = {}) { const t = useT() const { data: connections, error: connectionError, mutate: reloadConnections } = useSWR( "/api/pbs/servers", url => fetchApi(url), { revalidateOnFocus: false }) @@ -69,36 +114,43 @@ export function PBSPanel() { } catch (err) { setError(err instanceof Error ? err.message : t("pbs.saveFailed")) } finally { setBusy(false) } } - const capacity = (value?: number | null) => value == null ? "—" : formatBytes(value) + const now = Date.now() / 1000 - return
-
-

{t("pbs.title")}

- - - -
+ return
+ {!localOnly &&
+

{t("pbs.title")}

+ + + +
} {(connectionError || statusError) &&

{String((connectionError || statusError)?.message || t("pbs.loadFailed"))}

} {isLoading &&

{t("pbs.loading")}

} {!selected && !connectionError && connections &&

{t("pbs.noServer")}

} - {manage && + {manage && !localOnly && {t("pbs.manage")}

{t("pbs.permissionsHelp")}

- +
{(["name", "url", "token_id", "token_secret", "fingerprint"] as const).map(field =>
@@ -118,32 +170,78 @@ export function PBSPanel() { } {data && <> -

{data.name} · PBS {data.version || "—"} · {t("pbs.sampled")} {new Date(data.fetched_at * 1000).toLocaleTimeString()}

- {Object.keys(data.services).length > 0 &&
- {Object.entries(data.services).map(([service, state]) => {service}: {state})} -
} +
+ {data.name} · PBS {data.version || "—"} · {t("pbs.sampled")} {new Date(data.fetched_at * 1000).toLocaleTimeString()} + {Object.entries(data.services).map(([service, state]) => + {service}: {state})} +
{Object.entries(data.errors).map(([section, detail]) =>

{t(`pbs.${section}`)}: {detail}

)} - {t("pbs.datastores")} - {data.datastores?.length === 0 &&

{t("pbs.noDatastores")}

} - {data.datastores && data.datastores.length > 0 &&
- - {data.datastores.map(row => )} -
{t("pbs.name")}{t("pbs.total")}{t("pbs.used")}{t("pbs.free")}
{row.name}{row.error &&

{row.error}

}
{capacity(row.total)}{capacity(row.used)}{capacity(row.avail)}
} -
- {t("pbs.tasks")} - {data.tasks?.length === 0 &&

{t("pbs.noTasks")}

} - {data.tasks && data.tasks.length > 0 &&
- - {data.tasks.map(row => )} -
{t("pbs.task")}{t("pbs.started")}{t("pbs.status")}
{row.worker_type} {row.worker_id}{row.starttime ? new Date(row.starttime * 1000).toLocaleString() : "—"}{row.status || (row.endtime ? "—" : t("pbs.running"))}
} -
- {t("pbs.jobs")} -
- - {(["sync", "verify", "prune"] as const).flatMap(kind => (data[kind] || []).map(row => - ))} -
{t("pbs.job")}{t("pbs.datastores")}{t("pbs.schedule")}{t("pbs.status")}
{t(`pbs.${kind}`)} · {row.id}{row.store || "—"}{row.schedule || "—"}{row["last-run-state"] || "—"}
-
+ + + {t("pbs.datastores")} + + {data.datastores?.length === 0 &&

{t("pbs.noDatastores")}

} + {data.datastores && data.datastores.length > 0 &&
+ {data.datastores.map(store => )} +
} +
+
+ + + {t("pbs.tasks")} + + {data.tasks?.length === 0 &&

{t("pbs.noTasks")}

} + {data.tasks && data.tasks.length > 0 &&
+ + + + + + + {data.tasks.map(row => { + const Icon = TASK_ICONS[row.worker_type || ""] || Activity + const state = taskState(row) + const end = row.endtime ?? (state === "running" ? now : undefined) + return + + + + + + })} +
{t("pbs.task")}{t("pbs.started")}{t("pbs.duration")}{t("pbs.status")}
+ + {row.worker_type} + {row.worker_id && {row.worker_id}} + {row.starttime ? new Date(row.starttime * 1000).toLocaleString() : "—"}{row.starttime && end ? formatDuration(end - row.starttime) : "—"} + {state === "running" ? t("pbs.running") : row.status || "—"} +
} +
+
+ + + {t("pbs.jobs")} + + {(() => { + const jobs = (["sync", "verify", "prune"] as const).flatMap(kind => (data[kind] || []).map(row => ({ kind, row }))) + if (jobs.length === 0) return

{t("pbs.noJobs")}

+ return
+ + + + + + + {jobs.map(({ kind, row }) => + + + + + )} +
{t("pbs.job")}{t("pbs.datastores")}{t("pbs.schedule")}{t("pbs.status")}
{t(`pbs.${kind}`)} · {row.id}{row.store || "—"}{row.schedule || "—"}{row["last-run-state"] || "—"}
+ })()} +
+
}
} diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 679c9aaf..4b426082 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -18,7 +18,8 @@ import { Security } from "./security" import { Profile } from "./profile" import { About } from "./about" import { BackupTab } from "./backup-tab" -import { PBSDashboard } from "./pbs-dashboard" +import { PBSPanel } from "./pbs-panel" +import { MonitorSwitcher } from "./monitor-switcher" import { OnboardingCarousel } from "./onboarding-carousel" import { HealthStatusModal } from "./health-status-modal" import { ReleaseNotesModal, useVersionCheck } from "./release-notes-modal" @@ -46,7 +47,9 @@ import { ClipboardCheck, Info, DatabaseBackup, + Database, ChevronDown, + Clock, } from "lucide-react" import Image from "next/image" import { ThemeToggle } from "./theme-toggle" @@ -105,10 +108,27 @@ export function ProxmoxDashboard() { } function PVEDashboard() { + return +} + +function PBSDashboard() { + return +} + +type Product = "pve" | "pbs" +type TopTab = TabId | "datastores" + +// Proxmox Backup Server has no guests, apps or PVE storage: it keeps the +// same header, health and administration with its own top-level sections. +const PBS_TABS: TopTab[] = ["overview", "datastores", "terminal", "admin"] + +function MonitorDashboard({ product }: { product: Product }) { const t = useT() - useSWR("/api/apps/custom-links", _dashboardPrefetchFetcher, { revalidateOnFocus: false }) - useSWR("/api/apps/categories", _dashboardPrefetchFetcher, { revalidateOnFocus: false }) - const { order: tabOrder } = useTabOrder() + const isPbs = product === "pbs" + useSWR(isPbs ? null : "/api/apps/custom-links", _dashboardPrefetchFetcher, { revalidateOnFocus: false }) + useSWR(isPbs ? null : "/api/apps/categories", _dashboardPrefetchFetcher, { revalidateOnFocus: false }) + const { order: userTabOrder } = useTabOrder() + const tabOrder: TopTab[] = isPbs ? PBS_TABS : userTabOrder const [systemStatus, setSystemStatus] = useState({ status: "healthy", uptime: "Loading...", @@ -126,8 +146,8 @@ function PVEDashboard() { // runs post-hydration; the tiny flash is acceptable and matches the // pattern next-themes uses for the same reason. useEffect(() => { - setActiveTab(firstActualTab()) - }, []) + setActiveTab(isPbs ? "overview" : firstActualTab()) + }, [isPbs]) const [infoCount, setInfoCount] = useState(0) const [updateAvailable, setUpdateAvailable] = useState(false) const [showNavigation, setShowNavigation] = useState(true) @@ -400,6 +420,7 @@ function PVEDashboard() { const getActiveTabLabel = () => { switch (activeTab) { case "overview": return t("navigation.overview") + case "datastores": return t("pbs.datastores") case "apps": return t("navigation.apps") case "vms": return t("navigation.virtualMachines") case "storage": return t("navigation.storage") @@ -419,8 +440,9 @@ function PVEDashboard() { return (
- - setShowReleaseNotes(false)} /> + {/* Both describe the PVE Monitor. */} + {!isPbs && } + {!isPbs && setShowReleaseNotes(false)} />} {!isServerConnected && (
@@ -473,22 +495,17 @@ function PVEDashboard() {

{t("app.title")}

-

{t("app.description")}

+

{t(isPbs ? "app.descriptionPbs" : "app.description")}

- - {t("status.node", { node: displayServerName })} + + {t("status.uptime", { uptime: displayUptime })}
{/* Desktop Actions */}
-
- -
-
{t("status.node", { node: displayServerName })}
-
-
+
@@ -589,9 +606,7 @@ function PVEDashboard() { )}
- - {t("status.uptime", { uptime: displayUptime })} - +
@@ -632,7 +647,7 @@ function PVEDashboard() { { value: "audit", label: t("navigation.audit"), Icon: ClipboardCheck, default: false }, { value: "settings", label: t("navigation.settings"), Icon: SettingsIcon, default: false }, { value: "about", label: t("navigation.about"), Icon: Info, default: false }, - ] + ].filter(item => !isPbs || item.value !== "audit") const activeNodeItem = NODE_ITEMS.find(i => i.value === activeTab) const activeAdminItem = ADMIN_ITEMS.find(i => i.value === activeTab) const isNodeActive = activeNodeItem !== undefined @@ -698,9 +713,10 @@ function PVEDashboard() { ) - const renderTop = (id: TabId) => { + const renderTop = (id: TopTab) => { switch (id) { case "overview": return renderDirect("overview", LayoutDashboard, t("navigation.overview")) + case "datastores": return renderDirect("datastores", Database, t("pbs.datastores")) case "apps": return renderDirect("apps", Grid3x3, t("navigation.apps")) case "vms": return renderDirect("vms", Boxes, t("navigation.virtualMachines")) case "backup": return renderDirect("backup", DatabaseBackup, t("navigation.backup")) @@ -710,7 +726,7 @@ function PVEDashboard() { } } return ( - + {tabOrder.map(renderTop)} ) @@ -771,6 +787,7 @@ function PVEDashboard() { {tabOrder.map((id): React.ReactNode => { switch (id) { case "overview": return btn("overview", LayoutDashboard, t("navigation.overview")) + case "datastores": return btn("datastores", Database, t("pbs.datastores")) case "apps": return btn("apps", Grid3x3, t("navigation.apps")) case "vms": return btn("vms", Boxes, t("navigation.virtualMachines")) case "backup": return btn("backup", DatabaseBackup, t("navigation.backup")) @@ -786,7 +803,7 @@ function PVEDashboard() { {btn("logs", ScrollText, t("navigation.systemLogs"))} {btn("security", ShieldCheck, t("navigation.security"))} - {btn("audit", ClipboardCheck, t("navigation.audit"))} + {!isPbs && btn("audit", ClipboardCheck, t("navigation.audit"))} {btn("settings", SettingsIcon, t("navigation.settings"))} {btn("about", Info, t("navigation.about"))} @@ -820,9 +837,16 @@ function PVEDashboard() { 5 s / 59 s refresh intervals keep the data fresh in the background — reopening the tab is instant. */} - + + {isPbs && ( + + + + )} + + {!isPbs && <> @@ -848,11 +872,13 @@ function PVEDashboard() { + } + {!isPbs && <> @@ -860,13 +886,14 @@ function PVEDashboard() { + } - + {/* Profile tab — not surfaced in the top tabs nav. The only diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index a49968f6..37e3edc8 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -16,6 +16,7 @@ import { Input } from "./ui/input" import { getNetworkUnit } from "../lib/format-network" import { fetchApi } from "../lib/api-config" import { SUPPORTED_LANGUAGES, useI18n } from "../lib/i18n/provider" +import { useIsPbs } from "../lib/use-platform" import type { LanguageCode } from "../lib/i18n/languages" import { NavTabOrderCard } from "./nav-tab-order-card" @@ -312,6 +313,7 @@ interface NetworkInterface { export function Settings() { const { language, setLanguage, t } = useI18n() + const isPbs = useIsPbs() const tFallback = (key: string, fallback: string) => { const translated = t(key) return translated === key ? fallback : translated @@ -1124,7 +1126,7 @@ export function Settings() {
{/* Navigation Tab Order — user-orderable top-level tabs */} - + {!isPbs && } {/* Network Units Settings */} @@ -1280,7 +1282,7 @@ export function Settings() { {/* Per-category rows */}
- {suppressionCategories.map((cat) => { + {suppressionCategories.filter(cat => !isPbs || healthCategoryKey(cat) !== "vms").map((cat) => { const IconComp = CATEGORY_ICONS[cat.icon] || HeartPulse const effectiveHours = getEffectiveHours(cat) const isCustomMode = effectiveHours === -2 || (cat.key in customValues) @@ -1288,7 +1290,8 @@ export function Settings() { const isLong = effectiveHours >= 720 && effectiveHours !== -1 && effectiveHours !== -2 const hasChanged = cat.key in pendingChanges && pendingChanges[cat.key] !== cat.hours const selectVal = isCustomMode ? "custom" : getSelectValue(effectiveHours, cat.key) - const catLabel = tFallback(`settings.healthMonitor.categories.${healthCategoryKey(cat)}`, cat.label) + const categoryKey = isPbs && healthCategoryKey(cat) === "services" ? "pbsServices" : healthCategoryKey(cat) + const catLabel = tFallback(`settings.healthMonitor.categories.${categoryKey}`, cat.label) return (
@@ -1496,6 +1499,7 @@ export function Settings() { + {!isPbs && <> {/* Remote Storage Exclusions */} @@ -1678,6 +1682,7 @@ export function Settings() { )} + } {/* Network Interface Exclusions */} @@ -1875,6 +1880,7 @@ export function Settings() { is re-enabled). */} + {!isPbs && <> {/* GitHub API — optional authentication for app upstream checks. */} @@ -1981,15 +1987,16 @@ export function Settings() { )} + } - {/* Notification Settings */} - + {/* Notification Settings (PVE only for now) */} + {!isPbs && } {/* Issue #195: snippets storage selector. Only renders when more than one storage advertises content=snippets — on a typical standalone host with just `local` there's nothing to choose, so showing an empty selector would be noise. */} - {snippetsCandidates.length > 1 && ( + {!isPbs && snippetsCandidates.length > 1 && (
diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx index ab443d42..ecbeeef7 100644 --- a/AppImage/components/system-overview.tsx +++ b/AppImage/components/system-overview.tsx @@ -1,10 +1,12 @@ "use client" import { useState, useEffect } from "react" +import useSWR from "swr" import { Card, CardContent, CardHeader, CardTitle } from "./ui/card" import { Progress } from "./ui/progress" import { Badge } from "./ui/badge" -import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network, ChevronRight } from "lucide-react" +import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network, ChevronRight, Database } from "lucide-react" +import { DatastoreUsageRow, type PBSDatastore } from "./pbs-datastore-usage" import { NodeMetricsCharts } from "./node-metrics-charts" import { NetworkTrafficChart } from "./network-traffic-chart" import { TemperatureDetailModal } from "./temperature-detail-modal" @@ -185,9 +187,17 @@ const getUnitsSettings = (): "Bytes" | "Bits" => { return raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes" } -export function SystemOverview() { +export function SystemOverview({ product = "pve" }: { product?: "pve" | "pbs" } = {}) { const t = useT() const { language } = useI18n() + const isPbs = product === "pbs" + // On Proxmox Backup Server the datastores take the place of guests and PVE storage. + const { data: pbsStatus, error: pbsError } = useSWR<{ datastores: PBSDatastore[] | null }>( + isPbs ? "/api/pbs/status?server=local" : null, + (url: string) => fetchApi<{ datastores: PBSDatastore[] | null }>(url), + { refreshInterval: 60000, revalidateOnFocus: false }, + ) + const datastores = pbsStatus?.datastores ?? null const [systemData, setSystemData] = useState(null) const [vmData, setVmData] = useState([]) const [storageData, setStorageData] = useState(null) @@ -212,8 +222,8 @@ export function SystemOverview() { const fetchAllData = async () => { const [systemResult, vmResult, storageResults, networkResult] = await Promise.all([ fetchSystemData().finally(() => setLoadingStates((prev) => ({ ...prev, system: false }))), - fetchVMData().finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))), - Promise.all([fetchStorageData(), fetchProxmoxStorageData()]).finally(() => + (isPbs ? Promise.resolve([]) : fetchVMData()).finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))), + Promise.all([fetchStorageData(), isPbs ? Promise.resolve(null) : fetchProxmoxStorageData()]).finally(() => setLoadingStates((prev) => ({ ...prev, storage: false })), ), fetchNetworkData().finally(() => setLoadingStates((prev) => ({ ...prev, network: false }))), @@ -248,12 +258,13 @@ export function SystemOverview() { }, 5000) const vmInterval = setInterval(async () => { + if (isPbs) return const data = await fetchVMData() setVmData(data) }, 59000) const storageInterval = setInterval(async () => { - const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), fetchProxmoxStorageData()]) + const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), isPbs ? Promise.resolve(null) : fetchProxmoxStorageData()]) if (storage) setStorageData(storage) if (proxmoxStorage) setProxmoxStorageData(proxmoxStorage) }, 59000) @@ -278,7 +289,7 @@ export function SystemOverview() { clearInterval(networkInterval) window.removeEventListener("networkUnitChanged" as any, handleUnitChange) } - }, [t]) + }, [t, isPbs]) if (!hasAttemptedLoad || loadingStates.system) { return ( @@ -527,7 +538,46 @@ export function SystemOverview() { - {/* ── Active VM & LXC (preview restyle v2: pills mismo tamaño que "X running") ── */} + {isPbs ? ( + + + {t("pbs.datastores")} + + + + {!datastores && !pbsError ? ( +
+
+
+
+ ) : !datastores ? ( +

{t("pbs.loadFailed")}

+ ) : datastores.length === 0 ? ( +

{t("pbs.noDatastores")}

+ ) : ( +
+
+ {datastores.length} + {datastores.some(store => store.error) ? ( + + {t("pbs.datastoresUnavailable", { count: datastores.filter(store => store.error).length })} + + ) : ( + + {t("pbs.datastoresOnline")} + + )} +
+ {datastores.slice(0, 2).map(store => )} + {datastores.length > 2 && ( +

{t("pbs.moreDatastores", { count: datastores.length - 2 })}

+ )} +
+ )} +
+
+ ) : ( + /* ── Active VM & LXC (preview restyle v2: pills mismo tamaño que "X running") ── */ {t("overview.activeVmLxc")} @@ -565,6 +615,7 @@ export function SystemOverview() { )} + )} 0 ? "cursor-pointer hover:bg-white/5 transition-colors" : ""}`} @@ -708,7 +759,16 @@ export function SystemOverview() {
- {vmLxcStorages && vmLxcStorages.length > 0 ? ( + {isPbs ? ( +
+
{t("pbs.datastores")}
+ {!datastores ? ( +
{pbsError ? t("pbs.loadFailed") : t("pbs.loading")}
+ ) : datastores.length === 0 ? ( +
{t("pbs.noDatastores")}
+ ) : datastores.map(store => )} +
+ ) : vmLxcStorages && vmLxcStorages.length > 0 ? (
{t("overview.vmLxcStorage")}
@@ -744,7 +804,7 @@ export function SystemOverview() {
)} - {localStorage && ( + {!isPbs && localStorage && (
{t("overview.localStorageSystem")}
@@ -899,7 +959,7 @@ export function SystemOverview() { {formatSystemUptime(systemData.uptime)}
- {t("overview.proxmoxVersion")} + {t(isPbs ? "overview.pbsVersion" : "overview.proxmoxVersion")} {systemData.proxmox_version || t("app.notAvailable")}
diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index a538627a..1bf5b78c 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -11,7 +11,7 @@ import { Badge } from "./ui/badge" import { Progress } from "./ui/progress" import { Button } from "./ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "./ui/dialog" -import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, ChevronRight, Terminal, Archive, Plus, PlusCircle, Loader2, Clock, Database, Shield, Bell, FileText, Settings2, Activity, Package, RefreshCw, EthernetPort, ArrowUpCircle, Info, CheckCircle2, EyeOff, Eye, Trash2, Check, X, AlertTriangle, AlertCircle, Search, Pin, Tag as TagIcon } from 'lucide-react' +import { Server, Play, Square, Cpu, MemoryStick, HardDrive, Network, Power, RotateCcw, StopCircle, Container, ChevronDown, ChevronUp, ChevronRight, Terminal, Archive, Plus, PlusCircle, Loader2, Database, Shield, Bell, FileText, Settings2, Activity, Package, RefreshCw, EthernetPort, ArrowUpCircle, Info, CheckCircle2, EyeOff, Eye, Trash2, Check, X, AlertTriangle, AlertCircle, Search, Pin, Tag as TagIcon } from 'lucide-react' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { Checkbox } from "./ui/checkbox" import { Switch } from "./ui/switch" @@ -2863,8 +2863,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { onClick?: () => void, ) => { if (!uc) return null - const stale = uc.stale === true || (!!uc.last_check && isLxcUpdateStale(uc)) - if (!stale && (!uc.available || !uc.count || uc.count <= 0)) return null + if (!uc.available || !uc.count || uc.count <= 0) return null const last = uc.last_check ? new Date(uc.last_check).toLocaleString() : "—" @@ -2912,8 +2911,8 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { space — matters most on the LXC card row where uptime, IP and other chips compete for width. Count alone with the up-arrow reads unambiguously as "pending updates". */} - {stale ? : } - {stale ? t("vmLxc.updates.checkStale") : uc.count} + + {uc.count} {/* Chevron only when the badge is wired up as a clickable shortcut — its absence on the dashboard card avoids implying interactivity where there isn't any (the whole diff --git a/AppImage/lib/use-platform.ts b/AppImage/lib/use-platform.ts new file mode 100644 index 00000000..cbb99cc6 --- /dev/null +++ b/AppImage/lib/use-platform.ts @@ -0,0 +1,8 @@ +import useSWR from "swr" +import { fetchApi } from "./api-config" + +// Shares the dashboard's /api/platform request through the SWR cache. +export function useIsPbs(): boolean { + const { data } = useSWR<{ product: string }>("/api/platform", (url: string) => fetchApi<{ product: string }>(url), { revalidateOnFocus: false }) + return data?.product === "pbs" +} diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json index 9fa0c7cf..4cc5b7e0 100644 --- a/AppImage/messages/de/common.json +++ b/AppImage/messages/de/common.json @@ -8,7 +8,8 @@ "unknown": "Unbekannt", "notAvailable": "N/A", "serverOffline": "Server offline", - "supportProject": "Unterstützen Sie das Projekt und tragen Sie dazu bei" + "supportProject": "Unterstützen Sie das Projekt und tragen Sie dazu bei", + "descriptionPbs": "Proxmox Backup Server Dashboard" }, "common": { "notAvailable": "N/A", @@ -165,7 +166,8 @@ }, "runningCountOne": "{count} läuft", "runningCountFew": "{count} laufen", - "runningCountMany": "{count} laufen" + "runningCountMany": "{count} laufen", + "pbsVersion": "PBS-Version:" }, "storage": { "loadingTitle": "Speicherdaten werden geladen...", @@ -1735,7 +1737,17 @@ "portCategoryCustomPlaceholder": "Geben Sie eine Kategorie ein und drücken Sie Enter (Esc zum Abbrechen)", "portCustomUrlPlaceholder": "Benutzerdefinierte URL (z. B. https://vault.example.com) überschreibt IP:port", "stackMemberTitle": "Teil einer Multi-Container-Anwendung", - "stackMemberBody": "Dieser Container gehört zu einer Anwendung, deren Hauptcontainer {primary} ist. Die Anwendung, ihre Weblinks und ihre Version werden dort angezeigt." + "stackMemberBody": "Dieser Container gehört zu einer Anwendung, deren Hauptcontainer {primary} ist. Die Anwendung, ihre Weblinks und ihre Version werden dort angezeigt.", + "nativeEvidencePort": "Lauschender TCP-Port", + "nativeEvidenceFile": "Installierte Datei", + "nativeEvidenceProcess": "Laufende ausführbare Datei", + "nativeEvidenceDirectory": "Arbeitsverzeichnis", + "nativeEvidenceService": "Dienstdefinition", + "nativeScanFailed": "Die Anwendungssuche konnte nicht abgeschlossen werden. Frühere Ergebnisse sind möglicherweise veraltet; versuchen Sie es erneut.", + "nativeScanPartial": "Die Anwendungssuche ist unvollständig. Prüfen Sie die gefundenen Hinweise und versuchen Sie es erneut.", + "nativeVersionUnknown": "Installierte Version unbekannt", + "nativeEvidenceStale": "Die Hinweise sind möglicherweise veraltet oder unvollständig.", + "nativeRegisterOnly": "Bestätigen Sie die Registrierung im Editor. Eine bekannte Version wird als manueller Wert gespeichert; es wird kein Updater eingerichtet." }, "statusFilter": { "ariaLabel": "Virtuelle Maschinen und Container filtern", @@ -1831,7 +1843,8 @@ "logs": "Systemprotokolle", "updates": "Systemaktualisierungen", "security": "Sicherheit & Zertifikate", - "temperature": "Temperatur" + "temperature": "Temperatur", + "pbsServices": "PBS Services" } }, "remoteStorage": { @@ -4824,7 +4837,8 @@ "services": "PVE-Dienste", "logs": "Systemprotokolle", "updates": "Systemaktualisierungen", - "security": "Sicherheit & Zertifikate" + "security": "Sicherheit & Zertifikate", + "pbsServices": "PBS-Dienste" }, "checks": { "cpu_usage": "CPU-Auslastung", @@ -7320,6 +7334,47 @@ "navigation": "PBS-Navigation", "cpu": "CPU", "memory": "Arbeitsspeicher", - "uptime": "Betriebszeit" + "uptime": "Betriebszeit", + "capacityUnavailable": "Kapazität nicht verfügbar.", + "datastoresUnavailable": "{count} nicht verfügbar", + "datastoresOnline": "Alle verfügbar", + "moreDatastores": "+{count} weitere Datenspeicher", + "estimatedFull": "Voraussichtlich voll", + "estimatedFullUnknown": "Noch nicht genügend Daten", + "duration": "Dauer", + "noJobs": "Es sind keine Synchronisierungs-, Überprüfungs- oder Löschzeitpläne konfiguriert." + }, + "monitorLinks": { + "groups": { + "cluster": "Cluster", + "standalone": "Eigenständige Knoten", + "pbs": "Proxmox Backup Server" + }, + "thisNode": "Dieser Knoten", + "checking": "Knoten werden überprüft…", + "loadFailed": "Die Knotenliste konnte nicht geladen werden.", + "empty": "Noch keine anderen Knoten. Fügen Sie einen hinzu, um seinen Monitor von hier aus zu öffnen.", + "noResponse": "Keine Antwort", + "pbsInterface": "PBS-Schnittstelle", + "openNewTab": "In einem neuen Tab öffnen", + "manage": "Knoten hinzufügen oder verwalten…", + "dialogTitle": "Verknüpfte Knoten", + "dialogDescription": "Clusterknoten und konfigurierte Proxmox Backup Server werden automatisch erkannt. Jeder Monitor behält seine eigene Anmeldung.", + "addTitle": "Einen Knoten hinzufügen", + "name": "Name", + "address": "Adresse überwachen", + "type": "Geben Sie ein", + "add": "Hinzufügen", + "save": "Speichern", + "remove": "Entfernen", + "visible": "Sichtbar", + "autoAddress": "Automatisch", + "detected": "Erkannt", + "added": "Hinzugefügt", + "saveFailed": "Die Knotenliste konnte nicht gespeichert werden.", + "backTo": "Zurück zu {name}", + "cameFromTitle": "Monitorknoten hinzufügen?", + "cameFromBody": "{name} zu den verknüpften Knoten dieses Monitors hinzufügen.", + "notNow": "Nicht jetzt" } } diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index fa596302..884db22e 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -50,11 +50,20 @@ "navigation": "PBS navigation", "cpu": "CPU", "memory": "Memory", - "uptime": "Uptime" + "uptime": "Uptime", + "capacityUnavailable": "Capacity not available.", + "datastoresUnavailable": "{count} unavailable", + "datastoresOnline": "All available", + "moreDatastores": "+{count} more datastores", + "estimatedFull": "Estimated full", + "estimatedFullUnknown": "Not enough data yet", + "duration": "Duration", + "noJobs": "No sync, verification or prune schedules are configured." }, "app": { "title": "ProxMenux Monitor", "description": "Proxmox System Dashboard", + "descriptionPbs": "Proxmox Backup Server Dashboard", "loading": "Loading...", "connecting": "Connecting to ProxMenux Monitor", "unknown": "Unknown", @@ -195,6 +204,7 @@ "dayMany": "{count} days" }, "proxmoxVersion": "Proxmox Version:", + "pbsVersion": "PBS Version:", "kernel": "Kernel:", "availableUpdates": "Available Updates:", "packages": "packages", @@ -1730,6 +1740,16 @@ "searchApplications": "Find applications", "searchingApplications": "Searching applications…", "noApplicationsDetected": "No applications were detected.", + "nativeEvidencePort": "Listening TCP port", + "nativeEvidenceFile": "Installed file", + "nativeEvidenceProcess": "Running executable", + "nativeEvidenceDirectory": "Working directory", + "nativeEvidenceService": "Service definition", + "nativeScanFailed": "Application search could not finish. Previous evidence may be outdated; try again.", + "nativeScanPartial": "Application search is incomplete. Review the available evidence and try again.", + "nativeVersionUnknown": "Installed version unknown", + "nativeEvidenceStale": "Evidence may be outdated or incomplete.", + "nativeRegisterOnly": "Confirm registration in the editor. A known version is a manual value; no updater is configured.", "noNewApplicationsDetected": "No new applications were detected.", "dockerDetectedWithWorkloads": "Docker detected with {count} containerised application(s)", "dockerWorkloadsHeading": "Running inside Docker", @@ -1880,6 +1900,7 @@ "network": "Network Interfaces", "vms": "VMs & Containers", "services": "PVE Services", + "pbsServices": "PBS Services", "logs": "System Logs", "updates": "System Updates", "security": "Security & Certificates", @@ -4874,6 +4895,7 @@ "network": "Network interfaces", "vms": "VMs & containers", "services": "PVE services", + "pbsServices": "PBS services", "logs": "System logs", "updates": "System updates", "security": "Security & certificates" @@ -6529,5 +6551,38 @@ "cancel": "Cancel" } }, - "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"oci_update_completed":{"title":"{hostname}: {app_name} updated","body":"{app_name} was updated to its new image and its data was kept.\nContainers: {containers}","label":"OCI application updated"},"oci_update_failed":{"title":"{hostname}: {app_name} update did not complete","body":"The update of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application update failed"},"oci_recreate_completed":{"title":"{hostname}: {app_name} recreated","body":"{app_name} was recreated with its saved configuration and its data was kept.\nContainers: {containers}","label":"OCI application recreated"},"oci_recreate_failed":{"title":"{hostname}: {app_name} recreation did not complete","body":"The recreation of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application recreation failed"},"oci_modify_completed":{"title":"{hostname}: {app_name} modified","body":"{app_name} was modified with its new options and its data was kept.\nContainers: {containers}","label":"OCI application modified"},"oci_modify_failed":{"title":"{hostname}: {app_name} modification did not complete","body":"The modification of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application modification failed"},"oci_watchdog_restarted":{"title":"{hostname}: {app_name} restarted after it stopped","body":"{app_name} stopped on its own and the watchdog started it again.\nContainers: {containers}","label":"OCI application restarted by the watchdog"},"oci_watchdog_failed":{"title":"{hostname}: {app_name} could not be kept running","body":"The watchdog could not keep {app_name} running: it stopped again or did not start after several attempts, and it is left stopped.\nContainers: {containers}\nStart it by hand once the cause is solved; the watchdog then watches it again.","label":"OCI application left stopped by the watchdog"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High sensor temperature — {value}°C","body":"Sensor temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: Cluster event reported","body":"A cluster event was reported:\n{reason}","label":"Cluster event"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuest configurations copied: {guests}\nBind-mount stub directories created: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or open the ProxMenux menu → Settings post-install Proxmox → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Security > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none","temperatureAlertTitle":"{hostname}: Sensor temperature alert","temperatureAlertBody":"Temperature alert recorded without a complete measurement payload.","recordedReason":"Recorded reason: {reason}","recordedDetails":"Recorded details: {details}"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started","unconfirmed":"Unconfirmed","completed_with_warnings":"Completed with warnings"}}},"temperature":{"sampleSpan":"High samples span {duration}."},"backup":{"unconfirmedTitle":"{hostname}: Backup outcome unconfirmed","confirmedTitle":"{hostname}: Backup complete","confirmedBody":"Backup completed successfully.","errorTitle":"{hostname}: Backup error reported","errorBody":"The backup report contains an error.","unconfirmedBody":"The backup outcome is not confirmed.","warningTitle":"{hostname}: Backup completed with warnings","warningBody":"Backup completed with warnings.","diagnosticsOmitted":"Additional diagnostic lines or text omitted: {count}. Original report retained."}}} + "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"oci_update_completed":{"title":"{hostname}: {app_name} updated","body":"{app_name} was updated to its new image and its data was kept.\nContainers: {containers}","label":"OCI application updated"},"oci_update_failed":{"title":"{hostname}: {app_name} update did not complete","body":"The update of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application update failed"},"oci_recreate_completed":{"title":"{hostname}: {app_name} recreated","body":"{app_name} was recreated with its saved configuration and its data was kept.\nContainers: {containers}","label":"OCI application recreated"},"oci_recreate_failed":{"title":"{hostname}: {app_name} recreation did not complete","body":"The recreation of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application recreation failed"},"oci_modify_completed":{"title":"{hostname}: {app_name} modified","body":"{app_name} was modified with its new options and its data was kept.\nContainers: {containers}","label":"OCI application modified"},"oci_modify_failed":{"title":"{hostname}: {app_name} modification did not complete","body":"The modification of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application modification failed"},"oci_watchdog_restarted":{"title":"{hostname}: {app_name} restarted after it stopped","body":"{app_name} stopped on its own and the watchdog started it again.\nContainers: {containers}","label":"OCI application restarted by the watchdog"},"oci_watchdog_failed":{"title":"{hostname}: {app_name} could not be kept running","body":"The watchdog could not keep {app_name} running: it stopped again or did not start after several attempts, and it is left stopped.\nContainers: {containers}\nStart it by hand once the cause is solved; the watchdog then watches it again.","label":"OCI application left stopped by the watchdog"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High sensor temperature — {value}°C","body":"Sensor temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: Cluster event reported","body":"A cluster event was reported:\n{reason}","label":"Cluster event"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuest configurations copied: {guests}\nBind-mount stub directories created: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or open the ProxMenux menu → Settings post-install Proxmox → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Security > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none","temperatureAlertTitle":"{hostname}: Sensor temperature alert","temperatureAlertBody":"Temperature alert recorded without a complete measurement payload.","recordedReason":"Recorded reason: {reason}","recordedDetails":"Recorded details: {details}"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started","unconfirmed":"Unconfirmed","completed_with_warnings":"Completed with warnings"}}},"temperature":{"sampleSpan":"High samples span {duration}."},"backup":{"unconfirmedTitle":"{hostname}: Backup outcome unconfirmed","confirmedTitle":"{hostname}: Backup complete","confirmedBody":"Backup completed successfully.","errorTitle":"{hostname}: Backup error reported","errorBody":"The backup report contains an error.","unconfirmedBody":"The backup outcome is not confirmed.","warningTitle":"{hostname}: Backup completed with warnings","warningBody":"Backup completed with warnings.","diagnosticsOmitted":"Additional diagnostic lines or text omitted: {count}. Original report retained."}}}, + "monitorLinks": { + "thisNode": "This node", + "checking": "Checking nodes…", + "loadFailed": "Could not load the node list.", + "empty": "No other nodes yet. Add one to open its Monitor from here.", + "groups": { + "cluster": "Cluster", + "standalone": "Standalone nodes", + "pbs": "Proxmox Backup Server" + }, + "noResponse": "No response", + "pbsInterface": "PBS interface", + "openNewTab": "Open in a new tab", + "manage": "Add or manage nodes…", + "dialogTitle": "Linked nodes", + "dialogDescription": "Cluster nodes and configured Proxmox Backup Servers are detected automatically. Each Monitor keeps its own sign-in.", + "addTitle": "Add a node", + "name": "Name", + "address": "Monitor address", + "type": "Type", + "add": "Add", + "save": "Save", + "remove": "Remove", + "visible": "Visible", + "autoAddress": "Automatic", + "detected": "Detected", + "added": "Added", + "saveFailed": "Could not save the node list.", + "backTo": "Back to {name}", + "cameFromTitle": "Add Monitor node?", + "cameFromBody": "Add {name} to the linked nodes of this Monitor.", + "notNow": "Not now" + } } diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json index 322ea52a..91a29007 100644 --- a/AppImage/messages/es/common.json +++ b/AppImage/messages/es/common.json @@ -50,12 +50,21 @@ "navigation": "Navegación de PBS", "cpu": "CPU", "memory": "Memoria", - "uptime": "Tiempo de actividad" + "uptime": "Tiempo de actividad", + "capacityUnavailable": "Capacidad no disponible.", + "datastoresUnavailable": "{count} no disponibles", + "datastoresOnline": "Todos disponibles", + "moreDatastores": "+{count} datastores más", + "estimatedFull": "Lleno previsto", + "estimatedFullUnknown": "Aún no hay datos suficientes", + "duration": "Duración", + "noJobs": "No hay programaciones de sincronización, verificación ni prune." }, "_meta": "Community translation target. Copy keys from ../en/common.json and translate the values.", "app": { "title": "ProxMenux Monitor", "description": "Proxmox System Dashboard", + "descriptionPbs": "Proxmox Backup Server Dashboard", "loading": "Cargando...", "connecting": "Conexión al ProxMenux Monitor", "unknown": "Desconocido", @@ -193,6 +202,7 @@ "dayMany": "{count} días" }, "proxmoxVersion": "Versión Proxmox:", + "pbsVersion": "Versión PBS:", "kernel": "Núcleo:", "availableUpdates": "Actualizaciones disponibles:", "packages": "paquetes", @@ -1713,6 +1723,16 @@ "searchApplications": "Buscar aplicaciones", "searchingApplications": "Buscando aplicaciones…", "noApplicationsDetected": "No se han detectado aplicaciones.", + "nativeEvidencePort": "Puerto TCP a la escucha", + "nativeEvidenceFile": "Archivo instalado", + "nativeEvidenceProcess": "Ejecutable en marcha", + "nativeEvidenceDirectory": "Directorio de trabajo", + "nativeEvidenceService": "Definición del servicio", + "nativeScanFailed": "La búsqueda de aplicaciones no ha podido terminar. Lo detectado antes puede estar desactualizado; vuelve a intentarlo.", + "nativeScanPartial": "La búsqueda de aplicaciones está incompleta. Revisa lo que se ha encontrado y vuelve a intentarlo.", + "nativeVersionUnknown": "Versión instalada desconocida", + "nativeEvidenceStale": "Lo detectado puede estar desactualizado o incompleto.", + "nativeRegisterOnly": "Confirma el registro en el editor. Una versión conocida se guarda como valor manual; no se configura ningún actualizador.", "noNewApplicationsDetected": "No se han detectado aplicaciones nuevas.", "oneNewApplicationDetected": "Se ha detectado una aplicación nueva.", "newApplicationsDetected": "Se han detectado {count} aplicaciones nuevas.", @@ -1881,6 +1901,7 @@ "network": "Interfaces de red", "vms": "Máquinas virtuales y contenedores", "services": "Servicios PVE", + "pbsServices": "Servicios PBS", "logs": "Registros del sistema", "updates": "Actualizaciones del sistema", "security": "Seguridad y certificados", @@ -2652,7 +2673,7 @@ "expires": "Vence", "selfSignedWarning": "Los certificados autofirmados pueden mostrar una advertencia en el navegador. Esto es normal para las configuraciones locales de Proxmox.", "configuring": "Configurando...", - "useProxmoxCertificate": "Utilice el certificado Proxmox", + "useProxmoxCertificate": "Usar certificado de Proxmox", "noProxmoxCertificate": "No se detectó ningún certificado Proxmox.", "useCustomCertificate": "Usar certificado personalizado", "customPaths": "Rutas de certificados personalizadas", @@ -4874,7 +4895,8 @@ "disks": "E/S de disco y errores", "network": "Interfaces de red", "vms": "Máquinas virtuales y contenedores", - "services": "servicios PVE", + "services": "Servicios PVE", + "pbsServices": "Servicios PBS", "logs": "Registros del sistema", "updates": "Actualizaciones del sistema", "security": "Seguridad y certificados" @@ -7321,5 +7343,38 @@ "diagnosticsOmitted": "Líneas o texto de diagnóstico omitidos: {count}. Se conserva el informe original." } } + }, + "monitorLinks": { + "thisNode": "Este nodo", + "checking": "Comprobando nodos…", + "loadFailed": "No se pudo cargar la lista de nodos.", + "empty": "Aún no hay otros nodos. Añade uno para abrir su Monitor desde aquí.", + "groups": { + "cluster": "Clúster", + "standalone": "Nodos independientes", + "pbs": "Proxmox Backup Server" + }, + "noResponse": "Sin respuesta", + "pbsInterface": "Interfaz de PBS", + "openNewTab": "Abrir en una pestaña nueva", + "manage": "Añadir o gestionar nodos…", + "dialogTitle": "Nodos enlazados", + "dialogDescription": "Los nodos del clúster y los Proxmox Backup Server configurados se detectan automáticamente. Cada Monitor mantiene su propio inicio de sesión.", + "addTitle": "Añadir un nodo", + "name": "Nombre", + "address": "Dirección del Monitor", + "type": "Tipo", + "add": "Añadir", + "save": "Guardar", + "remove": "Eliminar", + "visible": "Visible", + "autoAddress": "Automática", + "detected": "Detectado", + "added": "Añadido", + "saveFailed": "No se pudo guardar la lista de nodos.", + "backTo": "Volver a {name}", + "cameFromTitle": "¿Añadir nodo Monitor?", + "cameFromBody": "Añade {name} a los nodos enlazados de este Monitor.", + "notNow": "Ahora no" } } diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json index 7dea3a29..7d542fe5 100644 --- a/AppImage/messages/fr/common.json +++ b/AppImage/messages/fr/common.json @@ -8,7 +8,8 @@ "unknown": "Inconnu", "notAvailable": "N/A", "serverOffline": "Serveur hors ligne", - "supportProject": "Soutenir et contribuer au projet" + "supportProject": "Soutenir et contribuer au projet", + "descriptionPbs": "Tableau de bord Proxmox Backup Server" }, "common": { "notAvailable": "N/A", @@ -165,7 +166,8 @@ }, "runningCountOne": "{count} en cours d'exécution", "runningCountFew": "{count} en cours d'exécution", - "runningCountMany": "{count} en cours d'exécution" + "runningCountMany": "{count} en cours d'exécution", + "pbsVersion": "Version PBS :" }, "storage": { "loadingTitle": "Chargement des données de stockage...", @@ -1735,7 +1737,17 @@ "portCategoryCustomPlaceholder": "Tapez une catégorie et appuyez sur Entrée (Esc pour annuler)", "portCustomUrlPlaceholder": "URL personnalisée (par exemple https://vault.example.com) — remplace IP:port", "stackMemberTitle": "Fait partie d'une application multi-conteneurs", - "stackMemberBody": "Ce conteneur fait partie d'une application dont le conteneur principal est {primary}. L'application, ses liens web et sa version y sont affichés." + "stackMemberBody": "Ce conteneur fait partie d'une application dont le conteneur principal est {primary}. L'application, ses liens web et sa version y sont affichés.", + "nativeEvidencePort": "Port TCP en écoute", + "nativeEvidenceFile": "Fichier installé", + "nativeEvidenceProcess": "Exécutable en cours d'exécution", + "nativeEvidenceDirectory": "Répertoire de travail", + "nativeEvidenceService": "Définition du service", + "nativeScanFailed": "La recherche d'applications n'a pas pu se terminer. Les résultats précédents peuvent être obsolètes ; réessayez.", + "nativeScanPartial": "La recherche d'applications est incomplète. Vérifiez les éléments trouvés et réessayez.", + "nativeVersionUnknown": "Version installée inconnue", + "nativeEvidenceStale": "Les éléments trouvés peuvent être obsolètes ou incomplets.", + "nativeRegisterOnly": "Confirmez l'enregistrement dans l'éditeur. Une version connue est enregistrée comme valeur manuelle ; aucun outil de mise à jour n'est configuré." }, "statusFilter": { "ariaLabel": "Filtrer les machines virtuelles et les conteneurs", @@ -1831,7 +1843,8 @@ "logs": "Journaux système", "updates": "Mises à jour du système", "security": "Sécurité et certificats", - "temperature": "Température" + "temperature": "Température", + "pbsServices": "Services PBS" } }, "remoteStorage": { @@ -4824,7 +4837,8 @@ "services": "Services PVE", "logs": "Journaux système", "updates": "Mises à jour du système", - "security": "Sécurité et certificats" + "security": "Sécurité et certificats", + "pbsServices": "services PBS" }, "checks": { "cpu_usage": "Utilisation du processeur", @@ -7320,6 +7334,47 @@ "navigation": "Navigation PBS", "cpu": "CPU", "memory": "Mémoire", - "uptime": "Durée de fonctionnement" + "uptime": "Durée de fonctionnement", + "capacityUnavailable": "Capacité non disponible.", + "datastoresUnavailable": "{count} indisponible", + "datastoresOnline": "Tous disponibles", + "moreDatastores": "+{count} banques de données supplémentaires", + "estimatedFull": "Estimé complet", + "estimatedFullUnknown": "Pas encore assez de données", + "duration": "Durée", + "noJobs": "Aucune planification de synchronisation, de vérification ou d'élagage n'est configurée." + }, + "monitorLinks": { + "groups": { + "cluster": "Cluster", + "standalone": "nœuds autonomes", + "pbs": "Proxmox Backup Server" + }, + "thisNode": "Ce nœud", + "checking": "Vérification des nœuds…", + "loadFailed": "Impossible de charger la liste des nœuds.", + "empty": "Aucun autre nœud pour l'instant. Ajoutez-en un pour ouvrir son Monitor depuis ici.", + "noResponse": "Pas de réponse", + "pbsInterface": "interface PBS", + "openNewTab": "Ouvrir dans un nouvel onglet", + "manage": "Ajouter ou gérer des nœuds…", + "dialogTitle": "nœuds liés", + "dialogDescription": "Les nœuds du cluster et les Proxmox Backup Server configurés sont détectés automatiquement. Chaque Monitor conserve sa propre connexion.", + "addTitle": "ajouter un nœud", + "name": "Nom", + "address": "Adresse du moniteur", + "type": "Type", + "add": "Ajouter", + "save": "Enregistrer", + "remove": "Supprimer", + "visible": "Visible", + "autoAddress": "Automatique", + "detected": "Détecté", + "added": "ajouté", + "saveFailed": "Impossible d'enregistrer la liste des nœuds.", + "backTo": "Retour à {name}", + "cameFromTitle": "Ajouter un nœud Moniteur ?", + "cameFromBody": "Ajoutez {name} aux nœuds liés de ce moniteur.", + "notNow": "Pas maintenant" } } diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json index 9fb2d83e..0846c5c0 100644 --- a/AppImage/messages/it/common.json +++ b/AppImage/messages/it/common.json @@ -8,7 +8,8 @@ "unknown": "Sconosciuto", "notAvailable": "N/A", "serverOffline": "Server offline", - "supportProject": "Sostieni e contribuisci al progetto" + "supportProject": "Sostieni e contribuisci al progetto", + "descriptionPbs": "Dashboard di Proxmox Backup Server" }, "common": { "notAvailable": "N/A", @@ -165,7 +166,8 @@ }, "runningCountOne": "{count} in esecuzione", "runningCountFew": "{count} in esecuzione", - "runningCountMany": "{count} in esecuzione" + "runningCountMany": "{count} in esecuzione", + "pbsVersion": "Versione PBS:" }, "storage": { "loadingTitle": "Caricamento dati storage...", @@ -1735,7 +1737,17 @@ "portCategoryCustomPlaceholder": "Digita una categoria e premi Enter (Esc per annullare)", "portCustomUrlPlaceholder": "URL personalizzato (ad es. https://vault.example.com) — sostituisce IP:port", "stackMemberTitle": "Parte di un'applicazione multi-container", - "stackMemberBody": "Questo container supporta un’applicazione il cui container principale è {primary}. L’applicazione, i suoi collegamenti web e la sua versione sono mostrati lì." + "stackMemberBody": "Questo container supporta un’applicazione il cui container principale è {primary}. L’applicazione, i suoi collegamenti web e la sua versione sono mostrati lì.", + "nativeEvidencePort": "Porta TCP in ascolto", + "nativeEvidenceFile": "File installato", + "nativeEvidenceProcess": "Eseguibile in esecuzione", + "nativeEvidenceDirectory": "Directory di lavoro", + "nativeEvidenceService": "Definizione del servizio", + "nativeScanFailed": "La ricerca delle applicazioni non è stata completata. I risultati precedenti potrebbero non essere aggiornati; riprova.", + "nativeScanPartial": "La ricerca delle applicazioni è incompleta. Controlla gli elementi trovati e riprova.", + "nativeVersionUnknown": "Versione installata sconosciuta", + "nativeEvidenceStale": "Gli elementi trovati potrebbero non essere aggiornati o essere incompleti.", + "nativeRegisterOnly": "Conferma la registrazione nell'editor. Una versione nota viene salvata come valore manuale; non viene configurato alcun aggiornamento automatico." }, "statusFilter": { "ariaLabel": "Filtra macchine virtuali e container", @@ -1831,7 +1843,8 @@ "logs": "Log di sistema", "updates": "Aggiornamenti di sistema", "security": "Sicurezza e certificati", - "temperature": "Temperatura" + "temperature": "Temperatura", + "pbsServices": "Servizi PBS" } }, "remoteStorage": { @@ -4824,7 +4837,8 @@ "services": "Servizi PVE", "logs": "Log di sistema", "updates": "Aggiornamenti di sistema", - "security": "Sicurezza e certificati" + "security": "Sicurezza e certificati", + "pbsServices": "servizi PBS" }, "checks": { "cpu_usage": "Utilizzo CPU", @@ -7320,6 +7334,47 @@ "navigation": "Navigazione PBS", "cpu": "CPU", "memory": "Memoria", - "uptime": "Tempo di attività" + "uptime": "Tempo di attività", + "capacityUnavailable": "Capacità non disponibile.", + "datastoresUnavailable": "{count} non disponibile", + "datastoresOnline": "Tutti disponibili", + "moreDatastores": "+{count} altri archivi dati", + "estimatedFull": "Pieno stimato", + "estimatedFullUnknown": "dati non ancora sufficienti", + "duration": "Durata", + "noJobs": "non è configurata alcuna pianificazione di sincronizzazione, verifica o eliminazione." + }, + "monitorLinks": { + "groups": { + "cluster": "cluster", + "standalone": "nodi autonomi", + "pbs": "Proxmox Backup Server" + }, + "thisNode": "questo nodo", + "checking": "Controllo dei nodi…", + "loadFailed": "impossibile caricare l'elenco dei nodi.", + "empty": "Ancora nessun altro nodo. Aggiungine uno per aprire il suo Monitor da qui.", + "noResponse": "Nessuna risposta", + "pbsInterface": "interfaccia PBS", + "openNewTab": "apri in una nuova scheda", + "manage": "aggiungi o gestisci nodi...", + "dialogTitle": "nodi collegati", + "dialogDescription": "I nodi del cluster e i Proxmox Backup Server configurati vengono rilevati automaticamente. Ogni Monitor mantiene il proprio accesso.", + "addTitle": "aggiungi un nodo", + "name": "Nome", + "address": "monitora l'indirizzo", + "type": "digita", + "add": "aggiungi", + "save": "salva", + "remove": "rimuovere", + "visible": "visibile", + "autoAddress": "automatico", + "detected": "rilevato", + "added": "aggiunto", + "saveFailed": "impossibile salvare l'elenco dei nodi.", + "backTo": "torna a {name}", + "cameFromTitle": "aggiungi il nodo Monitor?", + "cameFromBody": "aggiungi {name} ai nodi collegati di questo monitor.", + "notNow": "Non ora" } } diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json index d60c1806..c8fa95c5 100644 --- a/AppImage/messages/pt/common.json +++ b/AppImage/messages/pt/common.json @@ -8,7 +8,8 @@ "unknown": "Desconhecido", "notAvailable": "N/D", "serverOffline": "Servidor off-line", - "supportProject": "Apoie e contribua para o projeto" + "supportProject": "Apoie e contribua para o projeto", + "descriptionPbs": "Painel do Proxmox Backup Server" }, "common": { "notAvailable": "N/D", @@ -165,7 +166,8 @@ }, "runningCountOne": "{count} em execução", "runningCountFew": "{count} em execução", - "runningCountMany": "{count} em execução" + "runningCountMany": "{count} em execução", + "pbsVersion": "Versão PBS:" }, "storage": { "loadingTitle": "Carregando dados de armazenamento...", @@ -1735,7 +1737,17 @@ "portCategoryCustomPlaceholder": "Digite uma categoria e pressione Enter (Esc para cancelar)", "portCustomUrlPlaceholder": "URL personalizada (por exemplo, https://vault.example.com) — substitui IP:port", "stackMemberTitle": "Parte de um aplicativo multicontêiner", - "stackMemberBody": "Este contêiner faz parte de um aplicativo cujo contêiner principal é {primary}. O aplicativo, seus links da web e sua versão são mostrados lá." + "stackMemberBody": "Este contêiner faz parte de um aplicativo cujo contêiner principal é {primary}. O aplicativo, seus links da web e sua versão são mostrados lá.", + "nativeEvidencePort": "Porta TCP em escuta", + "nativeEvidenceFile": "Arquivo instalado", + "nativeEvidenceProcess": "Executável em execução", + "nativeEvidenceDirectory": "Diretório de trabalho", + "nativeEvidenceService": "Definição do serviço", + "nativeScanFailed": "A pesquisa de aplicativos não pôde ser concluída. Os resultados anteriores podem estar desatualizados; tente novamente.", + "nativeScanPartial": "A pesquisa de aplicativos está incompleta. Revise os itens encontrados e tente novamente.", + "nativeVersionUnknown": "Versão instalada desconhecida", + "nativeEvidenceStale": "Os itens encontrados podem estar desatualizados ou incompletos.", + "nativeRegisterOnly": "Confirme o registro no editor. Uma versão conhecida é salva como valor manual; nenhum atualizador é configurado." }, "statusFilter": { "ariaLabel": "Filtrar máquinas virtuais e contêineres", @@ -1831,7 +1843,8 @@ "logs": "Registros do sistema", "updates": "Atualizações do sistema", "security": "Segurança e Certificados", - "temperature": "Temperatura" + "temperature": "Temperatura", + "pbsServices": "Serviços PBS" } }, "remoteStorage": { @@ -4824,7 +4837,8 @@ "services": "Serviços PVE", "logs": "Registros do sistema", "updates": "Atualizações do sistema", - "security": "Segurança e certificados" + "security": "Segurança e certificados", + "pbsServices": "serviços PBS" }, "checks": { "cpu_usage": "Uso da CPU", @@ -7320,6 +7334,47 @@ "navigation": "Navegação PBS", "cpu": "CPU", "memory": "Memória", - "uptime": "Tempo de atividade" + "uptime": "Tempo de atividade", + "capacityUnavailable": "Capacidade não disponível.", + "datastoresUnavailable": "{count} indisponível", + "datastoresOnline": "Todos disponíveis", + "moreDatastores": "+{count} mais armazenamentos de dados", + "estimatedFull": "estimativa completa", + "estimatedFullUnknown": "Not enough data yet", + "duration": "Duração", + "noJobs": "nenhuma programação de sincronização, verificação ou remoção está configurada." + }, + "monitorLinks": { + "groups": { + "cluster": "Cluster", + "standalone": "nós autônomos", + "pbs": "Proxmox Backup Server" + }, + "thisNode": "Este nó", + "checking": "Verificando nós…", + "loadFailed": "não foi possível carregar a lista de nós.", + "empty": "Ainda não há outros nós. Adicione um para abrir o seu Monitor a partir daqui.", + "noResponse": "Sem resposta", + "pbsInterface": "interface PBS", + "openNewTab": "abrir em uma nova guia", + "manage": "Adicionar ou gerenciar nós…", + "dialogTitle": "nós vinculados", + "dialogDescription": "Os nós do cluster e os Proxmox Backup Server configurados são detectados automaticamente. Cada Monitor mantém o seu próprio login.", + "addTitle": "Adicionar um nó", + "name": "Nome", + "address": "endereço do monitor", + "type": "Tipo", + "add": "Adicionar", + "save": "Salvar", + "remove": "Remover", + "visible": "Visível", + "autoAddress": "Automático", + "detected": "Detectado", + "added": "Adicionado", + "saveFailed": "não foi possível salvar a lista de nós.", + "backTo": "Voltar para {name}", + "cameFromTitle": "Adicionar nó Monitor?", + "cameFromBody": "Adicione {name} aos nós vinculados deste Monitor.", + "notNow": "agora não" } } diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index 692fcce6..f699d7dd 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -7,7 +7,8 @@ "unknown": "Neznáme", "notAvailable": "Nedostupné", "serverOffline": "Server je offline", - "supportProject": "Podporte projekt alebo prispejte vlastnými úpravami" + "supportProject": "Podporte projekt alebo prispejte vlastnými úpravami", + "descriptionPbs": "Proxmox Backup Server Dashboard" }, "common": { "notAvailable": "Nedostupné", @@ -164,7 +165,8 @@ "errors": { "serverUnavailableTitle": "Server Monitora nie je dostupný", "serverUnavailableDescription": "Nedá sa pripojiť k serveru Monitora. Skontrolujte, či služba beží, a skúste to znova." - } + }, + "pbsVersion": "Verzia PBS:" }, "storage": { "loadingTitle": "Načítavam úložiská...", @@ -1682,6 +1684,16 @@ "searchApplications": "Hľadať aplikácie", "searchingApplications": "Hľadajú sa aplikácie…", "noApplicationsDetected": "Neboli zistené žiadne aplikácie.", + "nativeEvidencePort": "Otvorený TCP port", + "nativeEvidenceFile": "Nainštalovaný súbor", + "nativeEvidenceProcess": "Spustený program", + "nativeEvidenceDirectory": "Pracovný priečinok", + "nativeEvidenceService": "Definícia služby", + "nativeScanFailed": "Vyhľadávanie aplikácií sa nepodarilo dokončiť. Predchádzajúce dôkazy môžu byť zastarané; skús to znova.", + "nativeScanPartial": "Vyhľadávanie aplikácií je neúplné. Skontroluj dostupné dôkazy a skús to znova.", + "nativeVersionUnknown": "Nainštalovaná verzia nie je známa", + "nativeEvidenceStale": "Dôkazy môžu byť zastarané alebo neúplné.", + "nativeRegisterOnly": "Registráciu potvrď v editore. Známa verzia je ručný údaj; aktualizátor nie je nastavený.", "noNewApplicationsDetected": "Neboli zistené žiadne nové aplikácie.", "oneNewApplicationDetected": "Bola zistená jedna nová aplikácia.", "newApplicationsDetected": "Boli zistené {count} nové aplikácie.", @@ -1830,7 +1842,8 @@ "logs": "Systémové záznamy", "updates": "Aktualizácie systému", "security": "Bezpečnosť a certifikáty", - "temperature": "Teplota" + "temperature": "Teplota", + "pbsServices": "Služby PBS" } }, "remoteStorage": { @@ -4823,7 +4836,8 @@ "services": "Služby PVE", "logs": "Systémové záznamy", "updates": "Aktualizácie systému", - "security": "Bezpečnosť a certifikáty" + "security": "Bezpečnosť a certifikáty", + "pbsServices": "Služby PBS" }, "checks": { "cpu_usage": "Využitie CPU", @@ -7319,6 +7333,47 @@ "navigation": "Navigácia PBS", "cpu": "CPU", "memory": "Pamäť", - "uptime": "Čas prevádzky" + "uptime": "Čas prevádzky", + "capacityUnavailable": "Kapacita nie je k dispozícii.", + "datastoresUnavailable": "{count} nie je k dispozícii", + "datastoresOnline": "Všetky dostupné", + "moreDatastores": "+{count} ďalších dátových úložísk", + "estimatedFull": "Odhadovaný plný", + "estimatedFullUnknown": "Zatiaľ nie je dostatok údajov", + "duration": "Trvanie", + "noJobs": "Nie sú nakonfigurované žiadne plány synchronizácie, overovania ani odstraňovania." + }, + "monitorLinks": { + "groups": { + "cluster": "Cluster", + "standalone": "Samostatné uzly", + "pbs": "Proxmox Backup Server" + }, + "thisNode": "Tento uzol", + "checking": "Kontrolujú sa uzly…", + "loadFailed": "Nepodarilo sa načítať zoznam uzlov.", + "empty": "Zatiaľ žiadne ďalšie uzly. Pridajte jeden, aby ste odtiaľto otvorili jeho Monitor.", + "noResponse": "Žiadna odpoveď", + "pbsInterface": "rozhranie PBS", + "openNewTab": "Otvoriť na novej karte", + "manage": "Pridať alebo spravovať uzly…", + "dialogTitle": "Prepojené uzly", + "dialogDescription": "Uzly klastra a nakonfigurované Proxmox Backup Server sa zisťujú automaticky. Každý Monitor má vlastné prihlásenie.", + "addTitle": "Pridať uzol", + "name": "Meno", + "address": "Adresa monitora", + "type": "Typ", + "add": "Pridať", + "save": "Uložiť", + "remove": "Odstrániť", + "visible": "Viditeľné", + "autoAddress": "Automaticky", + "detected": "Zistené", + "added": "Pridané", + "saveFailed": "Nepodarilo sa uložiť zoznam uzlov.", + "backTo": "Späť na {name}", + "cameFromTitle": "Pridať uzol monitora?", + "cameFromBody": "Pridajte {name} do prepojených uzlov tohto monitora.", + "notNow": "Teraz nie" } } diff --git a/AppImage/messages/sv/common.json b/AppImage/messages/sv/common.json index 5a74449a..c74cd1c1 100644 --- a/AppImage/messages/sv/common.json +++ b/AppImage/messages/sv/common.json @@ -8,7 +8,8 @@ "unknown": "Okänd", "notAvailable": "N/A", "serverOffline": "Servern är frånkopplad", - "supportProject": "Stöd projektet och bidra" + "supportProject": "Stöd projektet och bidra", + "descriptionPbs": "Proxmox Backup Server Dashboard" }, "common": { "notAvailable": "N/A", @@ -165,7 +166,8 @@ }, "runningCountOne": "{count} körs", "runningCountFew": "{count} körs", - "runningCountMany": "{count} körs" + "runningCountMany": "{count} körs", + "pbsVersion": "PBS-version:" }, "storage": { "loadingTitle": "Laddar lagringsdata...", @@ -1735,7 +1737,17 @@ "portCategoryCustomPlaceholder": "Skriv en kategori och tryck på Enter (Esc för att avbryta)", "portCustomUrlPlaceholder": "Anpassad URL (t.ex. https://vault.example.com) - åsidosätter IP:port", "stackMemberTitle": "En del av en multicontainerapplikation", - "stackMemberBody": "Den här behållaren stöder en applikation vars huvudbehållare är {primary}. Applikationen, dess webblänkar och dess version visas där." + "stackMemberBody": "Den här behållaren stöder en applikation vars huvudbehållare är {primary}. Applikationen, dess webblänkar och dess version visas där.", + "nativeEvidencePort": "Lyssnande TCP-port", + "nativeEvidenceFile": "Installerad fil", + "nativeEvidenceProcess": "Körande programfil", + "nativeEvidenceDirectory": "Arbetskatalog", + "nativeEvidenceService": "Tjänstdefinition", + "nativeScanFailed": "Programsökningen kunde inte slutföras. Tidigare resultat kan vara inaktuella; försök igen.", + "nativeScanPartial": "Programsökningen är ofullständig. Granska det som hittades och försök igen.", + "nativeVersionUnknown": "Installerad version okänd", + "nativeEvidenceStale": "Det som hittades kan vara inaktuellt eller ofullständigt.", + "nativeRegisterOnly": "Bekräfta registreringen i redigeraren. En känd version sparas som ett manuellt värde; ingen uppdatering konfigureras." }, "statusFilter": { "ariaLabel": "Filtrera virtuella maskiner och containrar", @@ -1831,7 +1843,8 @@ "logs": "Systemloggar", "updates": "Systemuppdateringar", "security": "Säkerhet och certifikat", - "temperature": "Temperatur" + "temperature": "Temperatur", + "pbsServices": "PBS Services" } }, "remoteStorage": { @@ -4824,7 +4837,8 @@ "services": "PVE-tjänster", "logs": "Systemloggar", "updates": "Systemuppdateringar", - "security": "Säkerhet & certifikat" + "security": "Säkerhet & certifikat", + "pbsServices": "PBS-tjänster" }, "checks": { "cpu_usage": "CPU-användning", @@ -7320,6 +7334,47 @@ "navigation": "PBS-navigering", "cpu": "CPU", "memory": "Minne", - "uptime": "Drifttid" + "uptime": "Drifttid", + "capacityUnavailable": "Kapacitet inte tillgänglig.", + "datastoresUnavailable": "{count} inte tillgänglig", + "datastoresOnline": "Alla tillgängliga", + "moreDatastores": "+{count} fler databutiker", + "estimatedFull": "Beräknad full", + "estimatedFullUnknown": "Inte tillräckligt med data ännu", + "duration": "Varaktighet", + "noJobs": "Inga synkroniserings-, verifierings- eller beskärningsscheman är konfigurerade." + }, + "monitorLinks": { + "groups": { + "cluster": "Kluster", + "standalone": "Fristående noder", + "pbs": "Proxmox Backup Server" + }, + "thisNode": "Denna nod", + "checking": "Kontrollerar noder...", + "loadFailed": "Kunde inte ladda nodlistan.", + "empty": "Inga andra noder än. Lägg till en för att öppna dess Monitor härifrån.", + "noResponse": "Inget svar", + "pbsInterface": "PBS-gränssnitt", + "openNewTab": "Öppna i en ny flik", + "manage": "Lägg till eller hantera noder...", + "dialogTitle": "Länkade noder", + "dialogDescription": "Klusternoder och konfigurerade Proxmox Backup Server identifieras automatiskt. Varje Monitor behåller sin egen inloggning.", + "addTitle": "Lägg till en nod", + "name": "Namn", + "address": "Övervaka adress", + "type": "Typ", + "add": "Lägg till", + "save": "Spara", + "remove": "Ta bort", + "visible": "Synlig", + "autoAddress": "Automatisk", + "detected": "Upptäckt", + "added": "Tillagd", + "saveFailed": "Kunde inte spara nodlistan.", + "backTo": "Tillbaka till {name}", + "cameFromTitle": "Lägg till monitornod?", + "cameFromBody": "Lägg till {name} till de länkade noderna på denna monitor.", + "notNow": "Inte nu" } } diff --git a/AppImage/scripts/build_appimage.sh b/AppImage/scripts/build_appimage.sh index 40560c21..4bee9953 100755 --- a/AppImage/scripts/build_appimage.sh +++ b/AppImage/scripts/build_appimage.sh @@ -149,6 +149,7 @@ cp "$SCRIPT_DIR/managed_installs.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo " cp "$SCRIPT_DIR/lxc_update_observation.py" "$APP_DIR/usr/bin/" cp "$SCRIPT_DIR/lxc_update_finalization.py" "$APP_DIR/usr/bin/" cp "$SCRIPT_DIR/lxc_check_job.py" "$APP_DIR/usr/bin/" +cp "$SCRIPT_DIR/native_app_discovery.py" "$APP_DIR/usr/bin/" cp "$SCRIPT_DIR/lxc_apps.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ lxc_apps.py not found" cp "$SCRIPT_DIR/custom_links.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ custom_links.py not found" cp "$SCRIPT_DIR/recreate_docker_container.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ recreate_docker_container.py not found" @@ -163,6 +164,8 @@ cp "$SCRIPT_DIR/temperature_sensor_resolver.py" "$APP_DIR/usr/bin/" 2>/dev/null cp "$SCRIPT_DIR/proxmox_storage_monitor.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ proxmox_storage_monitor.py not found" cp "$SCRIPT_DIR/pbs_manager.py" "$APP_DIR/usr/bin/" cp "$SCRIPT_DIR/flask_pbs_routes.py" "$APP_DIR/usr/bin/" +cp "$SCRIPT_DIR/monitor_links.py" "$APP_DIR/usr/bin/" +cp "$SCRIPT_DIR/flask_monitor_links_routes.py" "$APP_DIR/usr/bin/" cp "$APPIMAGE_ROOT/../scripts/global/proxmox_platform.py" "$APP_DIR/usr/bin/" cp "$SCRIPT_DIR/flask_script_runner.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_script_runner.py not found" cp "$SCRIPT_DIR/security_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ security_manager.py not found" diff --git a/AppImage/scripts/flask_monitor_links_routes.py b/AppImage/scripts/flask_monitor_links_routes.py new file mode 100644 index 00000000..24c7d3ee --- /dev/null +++ b/AppImage/scripts/flask_monitor_links_routes.py @@ -0,0 +1,58 @@ +"""Links between ProxMenux Monitors for the node selector in the header.""" + +from flask import Blueprint, jsonify, request +import monitor_links +from jwt_middleware import require_auth, require_admin_scope + +monitor_links_bp = Blueprint("monitor_links", __name__) + + +@monitor_links_bp.after_request +def no_cache(response): + response.headers["Cache-Control"] = "no-store" + return response + + +@monitor_links_bp.route("/api/monitor-links") +@require_auth +def list_links(): + return jsonify(monitor_links.cached_links(include_hidden=request.args.get("all") == "1")) + + +@monitor_links_bp.route("/api/monitor-links", methods=["POST"]) +@require_admin_scope +def add_link(): + try: + key = monitor_links.add_link(request.get_json(silent=True)) + monitor_links.refresh() + return jsonify(success=True, key=key) + except monitor_links.LinkError as exc: + return jsonify(error=str(exc)), 400 + except OSError: + return jsonify(error="Could not save the node list."), 500 + + +@monitor_links_bp.route("/api/monitor-links/", methods=["PATCH"]) +@require_admin_scope +def update_link(key): + try: + monitor_links.update_link(key, request.get_json(silent=True)) + monitor_links.refresh() + return jsonify(success=True) + except monitor_links.LinkError as exc: + return jsonify(error=str(exc)), 400 + except OSError: + return jsonify(error="Could not save the node list."), 500 + + +@monitor_links_bp.route("/api/monitor-links/", methods=["DELETE"]) +@require_admin_scope +def delete_link(key): + try: + monitor_links.delete_link(key) + monitor_links.refresh() + return jsonify(success=True) + except monitor_links.LinkError as exc: + return jsonify(error=str(exc)), 400 + except OSError: + return jsonify(error="Could not save the node list."), 500 diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py index 2354141d..907ad3b6 100644 --- a/AppImage/scripts/flask_notification_routes.py +++ b/AppImage/scripts/flask_notification_routes.py @@ -1156,6 +1156,14 @@ def setup_pve_webhook_core() -> dict: 'fallback_commands': [], 'error': None, } + try: # Proxmox Backup Server has no /etc/pve notification configuration + from pbs_manager import is_pbs_host + on_pbs = is_pbs_host() + except Exception: + on_pbs = False + if on_pbs: + result['not_applicable'] = True + return result try: if not _pve_cluster_fs_mounted(): @@ -1339,6 +1347,13 @@ def cleanup_pve_webhook_core() -> dict: Only removes blocks named 'proxmenux-webhook' / 'proxmenux-default'. """ result = {'cleaned': False, 'error': None} + try: # Proxmox Backup Server has no /etc/pve notification configuration + from pbs_manager import is_pbs_host + on_pbs = is_pbs_host() + except Exception: + on_pbs = False + if on_pbs: + return {'cleaned': True, 'error': None, 'not_applicable': True} try: if not _pve_cluster_fs_mounted(): diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 3ffa0c4c..d1980c36 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -91,6 +91,7 @@ from flask_health_routes import health_bp # noqa: E402 from flask_auth_routes import auth_bp # noqa: E402 from flask_proxmenux_routes import proxmenux_bp # noqa: E402 from flask_pbs_routes import pbs_bp # noqa: E402 +from flask_monitor_links_routes import monitor_links_bp # noqa: E402 from pbs_manager import get_platform, is_pbs_host # noqa: E402 from flask_security_routes import security_bp # noqa: E402 from flask_notification_routes import notification_bp # noqa: E402 @@ -198,6 +199,7 @@ app.register_blueprint(auth_bp) app.register_blueprint(health_bp) app.register_blueprint(proxmenux_bp) app.register_blueprint(pbs_bp) +app.register_blueprint(monitor_links_bp) app.register_blueprint(security_bp) app.register_blueprint(notification_bp) app.register_blueprint(oci_bp) @@ -2276,6 +2278,8 @@ def _handle_guest_lifecycle(vmid: str, vm_type: str, action: str) -> None: # or stops; drop the cached IP so the next /api/vms poll re-reads # it via `lxc-info`. QEMU guests do not touch this cache. if guest_type == 'lxc': + import native_app_discovery + native_app_discovery.mark_stale(guest_id) _invalidate_lxc_ip(guest_id) if action in ('start', 'reboot'): if guest_type == 'lxc': @@ -12214,6 +12218,17 @@ def _get_node_metrics_payload(timeframe, max_age=_NODE_METRICS_TTL): lock.release() +def _node_rrd_command(local_node, timeframe, cf=None): + """Node RRD query; Proxmox Backup Server keeps the same fields under its own API.""" + if is_pbs_host(): + return ['proxmox-backup-debug', 'api', 'get', '/nodes/localhost/rrd', + '--timeframe', timeframe, '--cf', cf or 'AVERAGE', '--output-format', 'json'] + command = ['pvesh', 'get', f'/nodes/{local_node}/rrddata', '--timeframe', timeframe] + if cf: + command += ['--cf', cf] + return command + ['--output-format', 'json'] + + def _compute_node_metrics_payload(timeframe: str) -> dict: """Fetch and shape one node RRD snapshot within a bounded query budget.""" valid_timeframes = ('hour', 'day', 'week', 'month', 'year') @@ -12237,8 +12252,7 @@ def _compute_node_metrics_payload(timeframe: str) -> dict: try: rrd_result = subprocess.run( - ['pvesh', 'get', f'/nodes/{local_node}/rrddata', - '--timeframe', timeframe, '--output-format', 'json'], + _node_rrd_command(local_node, timeframe), capture_output=True, text=True, timeout=_NODE_METRICS_TIMEOUT, ) except subprocess.TimeoutExpired: @@ -12332,9 +12346,7 @@ def _compute_node_metrics_payload(timeframe: str) -> dict: return None try: extra = subprocess.run( - ['pvesh', 'get', f'/nodes/{local_node}/rrddata', - '--timeframe', timeframe, '--cf', cf, - '--output-format', 'json'], + _node_rrd_command(local_node, timeframe, cf), capture_output=True, text=True, timeout=remaining, ) if extra.returncode == 0 and extra.stdout: @@ -14090,6 +14102,20 @@ def api_vm_apps_suggestions(vmid): return jsonify({'error': str(e)}), 500 +@app.route('/api/vms//apps/native-discovery', methods=['GET', 'POST']) +@require_auth +def api_vm_apps_native_discovery(vmid): + """Only the Find applications button requests POST. GET never probes.""" + import native_app_discovery + import lxc_apps + result = (native_app_discovery.discover(vmid) if request.method == 'POST' + else native_app_discovery.cached(vmid)) + suggestions = _vm_cache_get(_vm_app_suggestions_cache, vmid, _VM_APP_SUGGESTIONS_TTL) or {} + sidecar = lxc_apps.load_sidecar(vmid) or {} + return jsonify(native_app_discovery.filter_candidates( + result, suggestions, sidecar.get('apps') or [])) + + @app.route('/api/vms//apps/adguard-setup', methods=['GET']) @require_auth def api_vm_apps_adguard_setup(vmid): @@ -23339,8 +23365,8 @@ if __name__ == '__main__': # the 24 h PollingCollector cycle. Runs in a daemon thread with # a delay so it doesn't block startup — notification_manager needs # a moment to bind channels before we emit. - # The guest services below read VMs, LXCs and the PVE node metrics, - # none of which exist on a Proxmox Backup Server. + # The guest services below read VMs and LXCs, which do not exist on a + # Proxmox Backup Server. pve_host = not is_pbs_host() try: def _startup_emit_pending_app_updates(): @@ -23359,13 +23385,19 @@ if __name__ == '__main__': # Prewarm only the Overview's default day range; other ranges are lazy. try: - if pve_host: - metrics_thread = threading.Thread(target=_node_metrics_prewarmer_loop, daemon=True, name='node-metrics-prewarmer') - metrics_thread.start() - print("[ProxMenux] Node-metrics prewarmer started (day range, 90s interval)") + metrics_thread = threading.Thread(target=_node_metrics_prewarmer_loop, daemon=True, name='node-metrics-prewarmer') + metrics_thread.start() + print("[ProxMenux] Node-metrics prewarmer started (day range, 90s interval)") except Exception as e: print(f"[ProxMenux] Node-metrics prewarmer failed to start: {e}") + # Linked Monitors for the header node selector, renewed every 5 minutes. + try: + import monitor_links + monitor_links.start_prewarmer() + except Exception as e: + print(f"[ProxMenux] Monitor links prewarmer failed to start: {e}") + # ── VM/CT modal-cache prewarmer ── # Keeps _vm_details_cache / _vm_backups_cache / app snapshots / # _vm_schedule_cache warm from the backend so the "Loading diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py index 8b6168a9..8f52bed0 100644 --- a/AppImage/scripts/health_monitor.py +++ b/AppImage/scripts/health_monitor.py @@ -6076,6 +6076,8 @@ class HealthMonitor: if status == 'stale': checks[check_label] = { 'status': 'CRITICAL', + 'error_key': error_key, + 'dismissable': True, 'detail': f'{fstype} mount stale: {error or "unreachable"}', 'fstype': fstype, 'source': m.get('source', ''), @@ -6105,6 +6107,8 @@ class HealthMonitor: elif status == 'readonly': checks[check_label] = { 'status': 'WARNING', + 'error_key': error_key, + 'dismissable': True, 'detail': f'{fstype} mount unexpectedly read-only', 'fstype': fstype, 'source': m.get('source', ''), diff --git a/AppImage/scripts/lxc_apps.py b/AppImage/scripts/lxc_apps.py index 494c9db5..4b6a5a2e 100644 --- a/AppImage/scripts/lxc_apps.py +++ b/AppImage/scripts/lxc_apps.py @@ -1179,6 +1179,13 @@ def validate_config(payload: dict) -> tuple[bool, Any]: return _err("logo_url must be an http(s) URL (max 512 chars)") conf["logo_url"] = logo + # Native discovery identity is separate from a curated helper/updater. + discovery_id = payload.get("discovery_id") + if discovery_id is not None: + if not isinstance(discovery_id, str) or not re.fullmatch(r"native-[0-9a-f]{24}", discovery_id): + return _err("invalid discovery_id") + conf["discovery_id"] = discovery_id + # Optional helper_slug — set by the frontend when the user picks # an auto-detected app (primary or extra). Persisted so we can # filter the "also detected" chip list against apps already diff --git a/AppImage/scripts/monitor_links.py b/AppImage/scripts/monitor_links.py new file mode 100644 index 00000000..35ff58a2 --- /dev/null +++ b/AppImage/scripts/monitor_links.py @@ -0,0 +1,321 @@ +"""Other ProxMenux Monitors reachable from this one: cluster nodes, PBS servers +and entries added by hand. + +Only links are kept here. No credential is stored or sent: the browser opens +the other Monitor, which keeps its own sign-in. +""" + +import ipaddress +import json +import os +import re +import secrets +import socket +import ssl +import subprocess +import threading +import time +import urllib.parse +import urllib.request +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +import pbs_manager + +LINKS_FILE = Path(os.environ.get("PROXMENUX_MONITOR_LINKS", "/usr/local/share/proxmenux/monitor_links.json")) +SSL_CONFIG_FILE = Path(os.environ.get("PROXMENUX_SSL_CONFIG", "/etc/proxmenux/ssl_config.json")) +STORAGE_CFG = Path("/etc/pve/storage.cfg") +MONITOR_PORT = 8008 +PBS_UI_PORT = 8007 +PROBE_TIMEOUT = 3 +PROBE_TTL = 300 +MAX_MANUAL = 64 +PRODUCTS = ("pve", "pbs") + +_lock = threading.Lock() +_probe_cache = {} +_snapshot = {"at": 0.0, "data": None} +_refresh_lock = threading.Lock() + + +class LinkError(ValueError): + pass + + +def _load(): + try: + data = json.loads(LINKS_FILE.read_text()) + except (OSError, ValueError): + return {"manual": [], "overrides": {}} + manual = data.get("manual") if isinstance(data.get("manual"), list) else [] + overrides = data.get("overrides") if isinstance(data.get("overrides"), dict) else {} + return {"manual": [row for row in manual if isinstance(row, dict)], "overrides": overrides} + + +def _save(data): + LINKS_FILE.parent.mkdir(parents=True, exist_ok=True) + temporary = LINKS_FILE.with_suffix(".tmp") + temporary.write_text(json.dumps(data, indent=2)) + os.chmod(temporary, 0o600) + os.replace(temporary, LINKS_FILE) + + +def normalize_url(value): + """Return the origin of an http(s) address, or raise LinkError.""" + if not isinstance(value, str) or not value.strip(): + raise LinkError("Enter the address of the Monitor.") + text = value.strip() + if "://" not in text: + text = "https://" + text + parsed = urllib.parse.urlsplit(text) + if parsed.scheme not in ("http", "https") or not parsed.hostname: + raise LinkError("The address must start with http:// or https:// and name a host.") + if parsed.username or parsed.password: + raise LinkError("The address must not contain a user or password.") + try: + port = parsed.port + except ValueError as exc: + raise LinkError("The port of the address is not valid.") from exc + host = parsed.hostname + if ":" in host: + host = f"[{host}]" + return f"{parsed.scheme}://{host}" + (f":{port}" if port else "") + + +def _local_scheme(): + try: + return "https" if json.loads(SSL_CONFIG_FILE.read_text()).get("enabled") else "http" + except (OSError, ValueError, AttributeError): + return "http" + + +def _host_url(host, port, scheme=None): + if ":" in host and not host.startswith("["): + host = f"[{host}]" + return f"{scheme or _local_scheme()}://{host}:{port}" + + +def _is_private(host): + """Only addresses in private ranges are probed without being asked to.""" + try: + addresses = {info[4][0] for info in socket.getaddrinfo(host, None)} + except OSError: + return False + try: + return bool(addresses) and all(ipaddress.ip_address(a.split("%")[0]).is_private for a in addresses) + except ValueError: + return False + + +def _cluster_nodes(): + try: + result = subprocess.run(["pvesh", "get", "/cluster/status", "--output-format", "json"], + capture_output=True, text=True, timeout=8) + rows = json.loads(result.stdout) if result.returncode == 0 else [] + except (OSError, subprocess.SubprocessError, ValueError): + return [] + nodes = [] + for row in rows if isinstance(rows, list) else []: + if isinstance(row, dict) and row.get("type") == "node" and not row.get("local") and row.get("name") and row.get("ip"): + nodes.append({"key": f"cluster:{row['name']}", "name": str(row["name"]), "product": "pve", + "group": "cluster", "host": str(row["ip"])}) + return nodes + + +def _storage_pbs(): + try: + text = STORAGE_CFG.read_text() + except OSError: + return [] + servers, current = [], None + for line in text.splitlines(): + header = re.match(r"^(\w+):\s*(\S+)", line) + if header: + current = {"name": header.group(2), "port": PBS_UI_PORT} if header.group(1) == "pbs" else None + if current is not None: + servers.append(current) + continue + option = re.match(r"^\s+(server|port)\s+(\S+)", line) + if current is not None and option: + current[option.group(1)] = option.group(2) + return [row for row in servers if row.get("server")] + + +def _pbs_servers(): + found = {} + for row in _storage_pbs(): + host = row["server"] + port = row.get("port") if str(row.get("port", "")).isdigit() else PBS_UI_PORT + found.setdefault(host, {"name": row["name"], "native_url": _host_url(host, port, "https")}) + try: + for row in pbs_manager.public_servers(): + host = urllib.parse.urlsplit(row.get("url", "")).hostname + if host: + found.setdefault(host, {"name": row.get("name") or host, "native_url": normalize_url(row["url"])}) + except Exception: + pass + return [{"key": f"pbs:{host}", "name": info["name"], "product": "pbs", "group": "pbs", + "host": host, "native_url": info["native_url"]} for host, info in found.items()] + + +def _probe(url): + """True when a ProxMenux Monitor answers at url; nothing is sent but the request.""" + now = time.monotonic() + with _lock: + cached = _probe_cache.get(url) + if cached and cached[0] > now: + return cached[1] + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE # liveness only: no credential or token is ever sent + online = False + try: + request = urllib.request.Request(url + "/api/auth/status", headers={"Accept": "application/json"}) + with urllib.request.urlopen(request, timeout=PROBE_TIMEOUT, context=context) as response: + online = response.status == 200 and "auth_enabled" in json.loads(response.read(4096)) + except Exception: + online = False + with _lock: + if len(_probe_cache) > 256: + _probe_cache.clear() + _probe_cache[url] = (now + PROBE_TTL, online) + return online + + +def _resolve(entry, override): + """Pick the Monitor address and its state for one discovered or manual entry.""" + if override.get("url"): + return override["url"], "online" if _probe(override["url"]) else "offline" + if entry.get("url"): + return entry["url"], "online" if _probe(entry["url"]) else "offline" + host = entry["host"] + if not _is_private(host): + return None, "unknown" + for scheme in (_local_scheme(), "https" if _local_scheme() == "http" else "http"): + url = _host_url(host, MONITOR_PORT, scheme) + if _probe(url): + return url, "online" + return _host_url(host, MONITOR_PORT), "offline" + + +def list_links(include_hidden=False): + platform = pbs_manager.get_platform() + data = _load() + entries = [] + if platform.get("product") != "pbs": + entries += _cluster_nodes() + _pbs_servers() + for row in data["manual"]: + entries.append({"key": f"manual:{row.get('id')}", "name": row.get("name") or row.get("url"), + "product": row.get("product") if row.get("product") in PRODUCTS else "pve", + "group": "pbs" if row.get("product") == "pbs" else "standalone", + "url": row.get("url"), "manual": True}) + visible = [e for e in entries if include_hidden or not data["overrides"].get(e["key"], {}).get("hidden")] + with ThreadPoolExecutor(max_workers=8) as executor: + resolved = list(executor.map(lambda e: _resolve(e, data["overrides"].get(e["key"], {})), visible)) + links = [] + for entry, (url, state) in zip(visible, resolved): + override = data["overrides"].get(entry["key"], {}) + links.append({"key": entry["key"], "name": entry["name"], "product": entry["product"], + "group": entry["group"], "url": url, "status": state, + "native_url": entry.get("native_url"), "manual": bool(entry.get("manual")), + "hidden": bool(override.get("hidden")), "custom_url": bool(override.get("url"))}) + return {"self": {"name": platform.get("hostname") or socket.gethostname(), "product": platform.get("product")}, + "links": links} + + +def refresh(): + """Recompute the list with fresh probes and keep it for the header.""" + with _refresh_lock: + with _lock: + _probe_cache.clear() + data = list_links(include_hidden=True) + _snapshot.update(at=time.monotonic(), data=data) + return data + + +def _refresh_quietly(): + try: + refresh() + except Exception: + pass + + +def cached_links(include_hidden=False): + """The last list at once; a stale one is renewed in the background.""" + data = _snapshot["data"] + if data is None: + data = refresh() + elif time.monotonic() - _snapshot["at"] > PROBE_TTL and not _refresh_lock.locked(): + threading.Thread(target=_refresh_quietly, daemon=True, name="monitor-links-refresh").start() + if include_hidden: + return data + return {**data, "links": [row for row in data["links"] if not row["hidden"]]} + + +def start_prewarmer(): + def loop(): + time.sleep(10) + while True: + _refresh_quietly() + time.sleep(PROBE_TTL) + threading.Thread(target=loop, daemon=True, name="monitor-links-prewarmer").start() + + +def add_link(payload): + if not isinstance(payload, dict): + raise LinkError("Invalid request.") + name = str(payload.get("name") or "").strip()[:64] + url = normalize_url(payload.get("url")) + product = payload.get("product") if payload.get("product") in PRODUCTS else "pve" + with _lock: + data = _load() + if len(data["manual"]) >= MAX_MANUAL: + raise LinkError(f"At most {MAX_MANUAL} nodes can be added.") + if any(row.get("url") == url for row in data["manual"]): + raise LinkError("This address is already in the list.") + identifier = secrets.token_hex(6) + data["manual"].append({"id": identifier, "name": name or urllib.parse.urlsplit(url).hostname, + "url": url, "product": product}) + _save(data) + return f"manual:{identifier}" + + +def update_link(key, payload): + if not isinstance(payload, dict): + raise LinkError("Invalid request.") + with _lock: + data = _load() + if key.startswith("manual:"): + row = next((r for r in data["manual"] if f"manual:{r.get('id')}" == key), None) + if row is None: + raise LinkError("Node not found.") + if "name" in payload: + row["name"] = str(payload["name"] or "").strip()[:64] or row["name"] + if payload.get("url"): + row["url"] = normalize_url(payload["url"]) + if payload.get("product") in PRODUCTS: + row["product"] = payload["product"] + elif re.fullmatch(r"(cluster|pbs):[\w.:\-\[\]]{1,253}", key): + override = data["overrides"].setdefault(key, {}) + if "url" in payload: + if payload["url"]: + override["url"] = normalize_url(payload["url"]) + else: + override.pop("url", None) + if "hidden" in payload: + override["hidden"] = bool(payload["hidden"]) + if not override: + data["overrides"].pop(key, None) + else: + raise LinkError("Node not found.") + _save(data) + + +def delete_link(key): + with _lock: + data = _load() + remaining = [r for r in data["manual"] if f"manual:{r.get('id')}" != key] + if len(remaining) == len(data["manual"]): + raise LinkError("Only nodes added by hand can be removed; detected ones can be hidden.") + data["manual"] = remaining + _save(data) diff --git a/AppImage/scripts/native_app_discovery.py b/AppImage/scripts/native_app_discovery.py new file mode 100644 index 00000000..3ba83ec5 --- /dev/null +++ b/AppImage/scripts/native_app_discovery.py @@ -0,0 +1,750 @@ +#!/usr/bin/env python3 +# ========================================================== +# ProxMenux - A menu-driven script for Proxmox VE management +# Author : VAIO73 +# GitHub : github.com/Vaso73 +# Maintainer : MacRimi +# Copyright : (c) 2026 MacRimi & contributors +# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE) +# Version : 1.0 +# Last Updated: 14/10/2026 +# ========================================================== +# Description: +# On-demand native LXC discovery from service and installation metadata. +# Reads bounded data only; never executes discovered programs, starts guests, +# registers applications or infers their update mechanism. +# ========================================================== +"""Separate from catalog detection: callers must explicitly request a scan.""" + +import copy +from contextlib import contextmanager +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import selectors +import signal +import stat +import subprocess +import threading +import time + +TOTAL_SECONDS = 20 +PROBE_SECONDS = 3 +OUTPUT_BYTES = 64 * 1024 +MAX_RECORDS = 256 +MAX_CANDIDATES = 50 +CACHE_SECONDS = 300 +LOCAL_CONFIG = Path('/etc/pve/local/lxc') +OCI_ROOT = Path('/usr/local/share/proxmenux/oci/instances') +UPDATE_LOCK_ROOT = Path('/run/lock') +_cache = {} +_locks = {} +_generations = {} +_mutex = threading.Lock() +_PATH = re.compile(r'/[A-Za-z0-9_.+/@-]{1,510}\Z') +_NAME = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.+@-]{0,63}\Z') +_VERSION = re.compile(r'v?\d+(?:\.\d+){1,4}(?:[-+._][A-Za-z0-9.-]+)?\Z') +_SYSTEM = { + 'ssh', 'sshd', 'cron', 'crond', 'postfix', 'rsyslog', 'syslog', 'syslog-ng', + # Alpine/OpenRC system logging and service supervision are not apps. + 'klogd', 'syslogd', 'openrc-user', 'runit', 'runsvdir', 'runsv', 'supervise-daemon', + 'dbus', 'dbus-broker', 'systemd', 'udev', 'networking', 'network', 'networkmanager', + 'dhcpcd', 'dhclient', 'chrony', 'chronyd', 'ntp', 'ntpd', 'openntpd', 'agetty', + 'getty', 'login', 'polkit', 'polkitd', 'accounts-daemon', 'acpid', 'anacron', + 'docker', 'containerd', 'containerd-shim', 'podman', 'qemu-guest-agent', 'rngd', + 'auditd', 'logrotate', 'local', 'bootmisc', 'hostname', 'hwclock', 'modules', 'master', + 'proxmox-boot-sync', 'pve-container', 'pve-firewall', 'pvefw-logger', + 'pvedaemon', 'pvestatd', 'pveproxy', 'pvescheduler', 'spiceproxy', 'lxc', + 'mount-ro', 'mdev', 'sysctl', 'sysfs', 'devfs', 'cgroups', 'rc', 'rcs', 'swap', + 'sleep', 'true', 'false', 'echo', 'cat', 'head', 'tail', 'tee', 'logger', + 'apt', 'apt-get', 'dpkg', 'dpkg-query', 'apk', 'unattended-upgrades', + 'fstrim', 'fsck', 'fsckd', 'e2scrub', 'e2scrub-all', 'e2scrub-reap', + 'irqbalance', 'smartd', 'smartmontools', 'rpcbind', 'nfs-common', 'nfs-server', + 'rpc-statd', 'rpc-statd-notify', 'rpc-gssd', 'rpc-svcgssd', 'nftables', 'iptables', + 'ip6tables', 'firewalld', 'ufw', 'resolvconf', 'wpa-supplicant', 'modemmanager', + 'console-setup', 'keyboard-setup', 'plymouth', 'halt', 'reboot', 'shutdown', + 'rc-local', 'ntpsec', 'networkd-dispatcher', 'busybox', 'atd', 'uuidd', + # Common packaged daemons are infrastructure, not a user-managed app. + 'nginx', 'apache', 'apache2', 'httpd', 'lighttpd', 'haproxy', 'traefik', + 'redis', 'redis-server', 'memcached', 'mysql', 'mysqld', 'mariadb', 'mariadbd', + 'postgres', 'postgresql', 'mongod', 'mongodb', 'rabbitmq-server', + 'elasticsearch', 'opensearch', 'fail2ban', 'named', 'bind9', 'dnsmasq', + 'avahi-daemon', 'smbd', 'nmbd', 'samba', 'vsftpd', 'proftpd', 'cups', 'tor', +} +_RUNTIMES = re.compile(r'(?:python[0-9.]*|node(?:js)?|npm|pnpm|yarn|bun|sh|bash|dash|env|java|ruby|perl)\Z') +_PACKAGE_ROOT_RUNTIMES = {'node', 'nodejs', 'npm', 'pnpm', 'yarn', 'bun'} +_HELPER_ROLES = { + 'backup', 'bootstrap', 'bridge', 'cleanup', 'housekeeping', 'maintenance', + 'migrate', 'migration', 'restore', 'rotate', 'update', 'updater', 'upgrade', + 'watch', 'watchdog', 'worker', +} +_HESTIA_NGINX = '/usr/local/hestia/nginx/sbin/hestia-nginx' +_HESTIA_ROOT = '/usr/local/hestia' +_HESTIA_PANEL_PORT = 8083 + + +class ProbeError(Exception): + """Safe error code only; guest output must never become an API error.""" + + +def bounded_run(argv, deadline): + """Cap combined stdout/stderr while reading, and kill/reap the process group.""" + remaining = min(PROBE_SECONDS, deadline - time.monotonic()) + if remaining <= 0: + raise ProbeError('deadline') + end = time.monotonic() + remaining + try: + proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + start_new_session=True) + except OSError: + raise ProbeError('unavailable') from None + outputs = [bytearray(), bytearray()] + try: + with selectors.DefaultSelector() as poll: + for index, stream in enumerate((proc.stdout, proc.stderr)): + os.set_blocking(stream.fileno(), False) + poll.register(stream, selectors.EVENT_READ, index) + while poll.get_map(): + wait = end - time.monotonic() + if wait <= 0: + raise ProbeError('timeout') + for key, _ in poll.select(wait): + block = os.read(key.fileobj.fileno(), 4096) + if not block: + poll.unregister(key.fileobj) + continue + if sum(map(len, outputs)) + len(block) > OUTPUT_BYTES: + raise ProbeError('output_limit') + outputs[key.data].extend(block) + try: + proc.wait(timeout=max(0.001, end - time.monotonic())) + except subprocess.TimeoutExpired: + raise ProbeError('timeout') from None + if proc.returncode: + raise ProbeError('read_failed') + return outputs[0].decode('utf-8', errors='strict') + except UnicodeError: + raise ProbeError('invalid_data') from None + finally: + # Also clean up descendants that outlive a successfully exited wrapper. + try: + os.killpg(proc.pid, signal.SIGKILL) + except ProcessLookupError: + pass + proc.wait() + proc.stdout.close() + proc.stderr.close() + + +# These are fixed read-only scripts, not guest-supplied shell or init scripts. +# awk emits only executable/script paths and WorkingDirectory, never arguments +# or environment values. OpenRC files are parsed as data, never sourced. +SERVICE_SCRIPT = r''' +command -v awk >/dev/null && command -v head >/dev/null || exit 1 +n=0 +for dir in /etc/systemd/system /usr/lib/systemd/system /lib/systemd/system /etc/init.d; do + # With a merged /usr, /lib is /usr/lib: the same units must not count twice. + [ "$dir" = /lib/systemd/system ] && [ /lib/systemd/system -ef /usr/lib/systemd/system ] && continue + [ ! -d "$dir" ] || [ -r "$dir" ] || exit 1 + for f in "$dir"/*; do + [ -f "$f" ] || continue + case "$dir:$f" in /etc/init.d:*) kind=openrc;; *:*.service) kind=systemd;; *) continue;; esac + n=$((n+1)); [ "$n" -le 256 ] || { printf 'LIMIT\n'; exit 0; } + [ -r "$f" ] || exit 1 + content=$(timeout -s KILL 1 head -c 16385 "$f") || exit 1 + [ "${#content}" -le 16384 ] || { printf 'LIMIT\n'; exit 0; } + printf '%s\n' "$content" | awk -v kind="$kind" -v file="$f" ' + function clean(v) {gsub(/^[ \t]+|[ \t]+$/, "", v); gsub(/^["\047]|["\047]$/, "", v); return v} + /^ExecStart=/ {v=substr($0,11); sub(/^[-+!:@]+/, "", v); split(v,a,/[ \t]+/); exe=clean(a[1]); script=clean(a[2]); if(script !~ /^\//) script=""} + /^WorkingDirectory=/ {dir=clean(substr($0,18))} + /^command=/ {exe=clean(substr($0,9))} + /^directory=/ {dir=clean(substr($0,11))} + /^command_args=/ {v=clean(substr($0,14)); split(v,a,/[ \t]+/); script=clean(a[1]); if(script !~ /^\//) script=""} + END {printf "S\t%s\t%s\t%s\t%s\t%s\n", file, kind, exe, script, dir}' + done +done +printf 'END\n' +''' +PROCESS_SCRIPT = r''' +command -v readlink >/dev/null && command -v head >/dev/null || exit 1 +n=0 +for dir in /proc/[0-9]*; do + [ -d "$dir" ] || continue + n=$((n+1)); [ "$n" -le "$1" ] || { printf 'LIMIT\n'; exit 0; } + # Never inspect Docker/OCI child workloads or read cmdline/environ. + [ -d "$dir" ] || continue + [ -r "$dir/cgroup" ] || exit 1 + group=$(timeout -s KILL 1 head -c 8193 "$dir/cgroup") || exit 1 + [ "${#group}" -le 8192 ] || { printf 'LIMIT\n'; exit 0; } + case "$group" in *docker*|*libpod*|*kubepods*|*containerd*) continue;; esac + # Kernel threads and vanished processes have no application executable. + [ -L "$dir/exe" ] || continue + exe=$(timeout -s KILL 1 readlink "$dir/exe") || continue + cwd=$(timeout -s KILL 1 readlink "$dir/cwd") || continue + printf 'P\t%s\t%s\t%s\n' "${dir##*/}" "$exe" "$cwd" +done +printf 'END\n' +''' +PORT_SCRIPT = r''' +command -v awk >/dev/null || exit 1 +if command -v ss >/dev/null; then + (timeout -s KILL 1 ss -H -lntp || printf 'FAILED\n') | awk -v cap="$1" ' + /^FAILED$/ {failed=1; exit} + NR > cap {limited=1; exit} + {port=$4; sub(/^.*:/,"",port); rest=$0 + # More than one owner is ambiguous; never choose the first arbitrarily. + if (gsub(/pid=/,"pid=",rest)!=1) next + if (match(rest,/pid=[0-9]+/)) {pid=substr(rest,RSTART+4,RLENGTH-4); printf "L\t%s\t%s\n",pid,port}} + END {print failed ? "FAILED" : (limited ? "LIMIT" : "END")}' +elif command -v netstat >/dev/null; then + (timeout -s KILL 1 netstat -lntp || printf 'FAILED\n') | awk -v cap="$1" ' + /^FAILED$/ {failed=1; exit} + /^tcp/ {n++; if(n>cap) {limited=1; exit}; port=$4; sub(/^.*:/,"",port) + pid=$7; sub(/\/.*$/,"",pid); if(pid ~ /^[0-9]+$/) printf "L\t%s\t%s\n",pid,port} + END {print failed ? "FAILED" : (limited ? "LIMIT" : "END")}' +else + # Without ss or netstat there is no listener evidence; a retry would not change it. + printf 'END\n' +fi +''' +HESTIA_PANEL_SCRIPT = r''' +# The generic process collector intentionally stops after a bounded number of +# records and ignores shared listener ownership. Hestia's master/worker pair +# can therefore be outside that generic result. This narrow fallback never +# accepts a name alone: it requires Hestia's exact executable and its panel +# listener on the fixed local port. +[ -x /usr/local/hestia/nginx/sbin/hestia-nginx ] || { printf 'END\n'; exit 0; } +command -v pgrep >/dev/null && command -v readlink >/dev/null && command -v ss >/dev/null && command -v head >/dev/null || exit 1 +n=0 +for pid in $(pgrep -x hestia-nginx 2>/dev/null | head -n 9); do + case "$pid" in *[!0-9]*|'') continue;; esac + n=$((n+1)); [ "$n" -le 8 ] || { printf 'LIMIT\n'; exit 0; } + exe=$(timeout -s KILL 1 readlink "/proc/$pid/exe") || continue + [ "$exe" = /usr/local/hestia/nginx/sbin/hestia-nginx ] || continue + cwd=$(timeout -s KILL 1 readlink "/proc/$pid/cwd") || continue + if timeout -s KILL 1 ss -H -lntp | awk -v pid="$pid" ' + {port=$4; sub(/^.*:/,"",port) + if (port == 8083 && index($0, "pid=" pid ",")) found=1} + END {exit(found ? 0 : 1)}'; then + printf 'H\t%s\t%s\t%s\t8083\n' "$pid" "$exe" "$cwd" + fi +done +printf 'END\n' +''' +FOOTPRINT_SCRIPT = r''' +[ -f "$1" ] && [ -r "$1" ] || exit 1 +printf 'FILE\n' +if [ -f "$2/package.json" ]; then + [ -r "$2/package.json" ] || exit 1 + timeout -s KILL 1 head -c 8192 "$2/package.json" +fi +''' +MANIFEST_FOOTPRINT_SCRIPT = r''' +command -v head >/dev/null && command -v readlink >/dev/null || exit 1 +# Resolve only the operator-owned working directory and its declared parent. +# A release symlink may change the concrete version directory, but it must not +# escape the bounded custom application root supplied by the host. +workdir=$(timeout -s KILL 1 readlink -f "$1") || exit 1 +anchor=$(timeout -s KILL 1 readlink -f "$2") || exit 1 +parent=$(timeout -s KILL 1 readlink -f "$3") || exit 1 +case "$anchor" in "$parent"/*) ;; *) printf 'OUTSIDE\n'; exit 0;; esac +case "$workdir" in "$anchor"|"$anchor"/*) ;; *) printf 'OUTSIDE\n'; exit 0;; esac +for relative in package.json server/package.json; do + candidate="$workdir/$relative" + [ -f "$candidate" ] && [ -r "$candidate" ] || continue + resolved=$(timeout -s KILL 1 readlink -f "$candidate") || exit 1 + case "$resolved" in "$workdir"/*) ;; *) printf 'OUTSIDE\n'; exit 0;; esac + printf 'MANIFEST\t%s\n' "$relative" + timeout -s KILL 1 head -c 8192 "$resolved" + exit 0 +done +printf 'MISSING\n' +''' + + +def _path(value): + return bool(isinstance(value, str) and _PATH.fullmatch(value) + and '..' not in value.split('/')) + + +def _system(name): + raw = name.lower().removesuffix('.service') + stem = raw.split('@')[0] + return (raw.startswith('user@') or stem in _SYSTEM + or (raw.startswith('php') and raw.endswith('-fpm')) + or stem.startswith(( + 'systemd-', 'dbus-', 'getty', 'containerd-', 'apt-', 'dpkg-', 'e2scrub', + 'unattended-', 'ifup', 'ifdown', 'user@', 'btrfs-', 'lvm2-', 'lxc-'))) + + +def _helper(name): + """Reject service roles that support an application but are not one themselves.""" + raw = name.lower().removesuffix('.service') + if raw.endswith('@'): + return True # A systemd template is not a concrete running application. + return any(part in _HELPER_ROLES for part in re.split(r'[._-]+', raw) if part) + + +def _hestia_panel_process(executable, ports): + """Recognize only Hestia's own bound control-panel process. + + Hestia's generated init unit does not expose an ExecStart that the generic + service parser may treat as an application. Its panel process and port are + stable, local runtime metadata, so this stays narrower than accepting + arbitrary vendor services or a generic web server. + """ + return executable == _HESTIA_NGINX and _HESTIA_PANEL_PORT in ports + + +def _custom(path): + if not _path(path): + return False + if path.startswith(('/opt/', '/srv/', '/usr/local/')): + return True + # Native apps commonly keep their runtime below /home//. + # Do not accept a home directory itself: a second path component is required. + parts = path.split('/') + return len(parts) >= 4 and parts[1] == 'home' and bool(parts[2]) and bool(parts[3]) + + +def _manifest_bounds(executable, directory): + """Return a narrow, operator-owned boundary for a runtime manifest probe.""" + try: + shared = os.path.commonpath((executable, directory)) + except ValueError: + shared = '' + anchor = shared if _custom(shared) else directory + parts = anchor.split('/') + if anchor.startswith(('/opt/', '/srv/')): + parent = '/' + parts[1] + elif anchor.startswith('/usr/local/'): + parent = '/usr/local' + elif len(parts) >= 4 and parts[1] == 'home' and parts[2]: + parent = f'/home/{parts[2]}' + else: + return None + return anchor, parent + + +def _manifest_footprint(record, run, deadline): + """Read one of two fixed Node manifest locations without following an escape.""" + output = run(['pct', 'exec', str(record['vmid']), '--', 'timeout', '-s', 'KILL', '2', + 'sh', '-c', MANIFEST_FOOTPRINT_SCRIPT, 'discovery', + record['root'], record['manifest_anchor'], record['manifest_parent']], deadline) + header, separator, metadata = output.partition('\n') + if header == 'MISSING': + if metadata: + raise ProbeError('invalid_data') + return None, None + if header == 'OUTSIDE': + if metadata: + raise ProbeError('invalid_data') + raise ProbeError('unsafe_manifest_root') + prefix = 'MANIFEST\t' + relative = header.removeprefix(prefix) + if not header.startswith(prefix) or relative not in ('package.json', 'server/package.json'): + raise ProbeError('invalid_data') + if not separator: + raise ProbeError('invalid_data') + candidate_record = dict(record) + candidate_record['path'] = str(Path(record['root']) / relative) + return candidate_record, 'FILE\n' + metadata + + +def parse_records(text, kind): + lines = text.splitlines() + if not lines or lines[-1] not in ('END', 'LIMIT'): + raise ProbeError('incomplete') + if len(lines) - 1 > MAX_RECORDS: + raise ProbeError('metadata_limit') + records = [] + seen_services = set() + for line in lines[:-1]: + parts = line.split('\t') + if kind == 'S' and len(parts) == 6 and parts[0] == 'S': + _, source, manager, exe, script, directory = parts + if not _path(source) or manager not in ('systemd', 'openrc'): + raise ProbeError('invalid_data') + service = Path(source).name.removesuffix('.service') + if service in seen_services: + continue # /etc overrides vendor units; no duplicate installations. + seen_services.add(service) + if (not _NAME.fullmatch(service) or _system(service) or _helper(service) + or not _path(exe)): + continue + runtime_name = Path(exe).name + runtime = bool(_RUNTIMES.fullmatch(runtime_name)) + manifest_bounds = None + if runtime and not _path(script): + # A Node/npm wrapper may keep its entry point relative to an + # operator-owned working directory. Only two fixed manifest + # locations can prove that root; no argv, environment, recursive + # search or guest-provided path is ever used. + if runtime_name not in _PACKAGE_ROOT_RUNTIMES or not _custom(directory): + continue + install = str(Path(directory) / 'package.json') + manifest_bounds = _manifest_bounds(exe, directory) + if manifest_bounds is None: + continue + else: + install = script if runtime else exe + if (not _path(install) or _system(Path(install).name) + or install.startswith(('/usr/lib/systemd/', '/lib/systemd/', + '/usr/lib/udev/', '/lib/udev/', '/usr/lib/apt/'))): + continue + # /etc units are operator-owned. Do not infer an application from an + # arbitrary packaged systemd unit: without a package-db probe it is + # impossible to distinguish vendor infrastructure from a user app. + if (manager == 'systemd' + and source.startswith(('/usr/lib/systemd/system/', '/lib/systemd/system/')) + and not _custom(install)): + continue + root = directory if _custom(directory) else str(Path(install).parent) + # Generic runtime without an absolute application script is ambiguous. + if runtime and not _custom(install): + continue + record = {'name': service, 'path': install, 'root': root, + 'service': source, 'manager': manager, 'executable': exe} + if manifest_bounds: + record['manifest_anchor'], record['manifest_parent'] = manifest_bounds + records.append(record) + elif kind == 'P' and len(parts) == 4 and parts[0] == 'P': + _, pid, exe, directory = parts + # A process rooted at / is normally too ambiguous for generic + # discovery, but it is valid process metadata for the exact Hestia + # panel signature below. It never becomes a generic candidate. + if not pid.isdigit() or not _path(exe) or (directory != '/' and not _path(directory)): + continue + records.append({'pid': pid, 'executable': exe, 'root': directory}) + elif kind == 'L' and len(parts) == 3 and parts[0] == 'L': + _, pid, port = parts + if pid.isdigit() and port.isdigit() and 0 < int(port) < 65536: + records.append({'pid': pid, 'port': int(port)}) + else: + raise ProbeError('invalid_data') + elif kind == 'H' and len(parts) == 5 and parts[0] == 'H': + _, pid, exe, directory, port = parts + if (pid.isdigit() and exe == _HESTIA_NGINX + and (directory == '/' or _path(directory)) + and port == str(_HESTIA_PANEL_PORT)): + records.append({'pid': pid, 'executable': exe, 'root': directory, + 'port': _HESTIA_PANEL_PORT}) + else: + raise ProbeError('invalid_data') + else: + raise ProbeError('invalid_data') + return records, lines[-1] == 'LIMIT', len(lines) - 1 + + +def candidate(vmid, record, footprint): + if not footprint.startswith('FILE\n'): + raise ProbeError('invalid_data') + version, metadata_issue = None, None + metadata = footprint[5:].strip() + if metadata: + try: + manifest = json.loads(metadata) + except (ValueError, TypeError): + manifest = None + # Only the whitelisted version field leaves this function. Never retain + # manifest scripts, repository guesses, config or arbitrary extra fields. + if not isinstance(manifest, dict): + metadata_issue = 'invalid_metadata' + manifest = {} + value = manifest.get('version') + if (isinstance(value, str) and len(value) <= 64 and _VERSION.fullmatch(value) + and record['path'].startswith(record['root'].rstrip('/') + '/')): + version = value + identity = 'native-' + hashlib.sha256( + f'{int(vmid)}:{record["path"]}:{record["root"]}'.encode()).hexdigest()[:24] + evidence = [{'kind': 'installation_file', 'value': record['path']}] + if record.get('service'): + evidence.append({'kind': record['manager'], 'value': record['service']}) + else: + evidence.extend([{'kind': 'process_executable', 'value': record['process']}, + {'kind': 'working_directory', + 'value': record.get('process_root', record['root'])}]) + ports = sorted(set(record.get('listening_ports') or [])) + # Hestia's control panel is a narrow, verified exception to generic port + # inference: its own executable serves the panel on fixed HTTPS port 8083. + # Keep this declaration on the exact runtime signature so an unrelated + # application merely listening on 8083 never receives an HTTPS guess. + hestia_panel = (record['name'] == 'Hestia' + and record['path'] == _HESTIA_NGINX + and record['root'] == _HESTIA_ROOT + and _HESTIA_PANEL_PORT in ports) + evidence.extend({'kind': 'tcp_listener', 'value': str(port)} for port in ports) + return {'slug': identity, 'discovery_id': identity, 'source': 'native_metadata', + 'name': record['name'], 'installed_version': version, 'evidence': evidence, + 'installation_path': record['path'], + 'default_ports': [_HESTIA_PANEL_PORT] if hestia_panel else [], + 'listening_ports': ports, + **({'scheme': 'https'} if hestia_panel else {}), + 'metadata_issue': metadata_issue, + 'tracking_suggestion': ({'installed_via': 'manual', 'installed_version': version, + 'detected_version': version, 'detector_verified': True} + if version else None)} + + +def _guard(vmid, deadline, run): + # The local node path rejects QEMU, missing and remote CTs before pct exec. + path = LOCAL_CONFIG / f'{vmid}.conf' + if not path.is_file(): + raise ProbeError('not_local_lxc') + # Presence, including a damaged/in-progress record, excludes generic probes. + try: + os.lstat(OCI_ROOT / str(vmid)) + except FileNotFoundError: + pass + except OSError: + raise ProbeError('oci_unknown') from None + else: + raise ProbeError('managed_oci') + try: + with path.open('rb') as handle: + raw = handle.read(OUTPUT_BYTES + 1) + if len(raw) > OUTPUT_BYTES: + raise ProbeError('config_limit') + config = raw.decode('utf-8') + except (OSError, UnicodeError): + raise ProbeError('config_unavailable') from None + if re.search(r'^lock:', config, re.M): + raise ProbeError('guest_busy') + update_lock = UPDATE_LOCK_ROOT / f'proxmenux-lxc-update-{vmid}.lock' + try: + fd = os.open(update_lock, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(fd, 'rb') as handle: + info = os.fstat(handle.fileno()) + if not stat.S_ISREG(info.st_mode): + raise ProbeError('lock_unavailable') + if info.st_size: + raise ProbeError('guest_busy') + try: + fcntl.flock(handle, fcntl.LOCK_SH | fcntl.LOCK_NB) + except BlockingIOError: + raise ProbeError('guest_busy') from None + finally: + fcntl.flock(handle, fcntl.LOCK_UN) + except FileNotFoundError: + pass + except OSError: + raise ProbeError('lock_unavailable') from None + if re.search(r'^ostype:\s*unmanaged\s*$', config, re.M): + raise ProbeError('unsupported_guest') + if not re.search(r'^ostype:\s*(?:debian|ubuntu|alpine|archlinux|centos|fedora|opensuse|gentoo)\s*$', config, re.M): + raise ProbeError('unsupported_guest') + status = run(['pct', 'status', str(vmid)], deadline).strip() + if status != 'status: running': + raise ProbeError('guest_stopped' if status == 'status: stopped' else 'status_unknown') + + +@contextmanager +def _scan_reservation(vmid): + """Exclude cooperating updates/control tasks for this bounded scan only. + + Keep an empty lock inode after first use, as the update runner does. + Never truncate a pending control-task reservation or unlink its inode. + """ + path = UPDATE_LOCK_ROOT / f'proxmenux-lxc-update-{vmid}.lock' + fd = None + try: + fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600) + if not stat.S_ISREG(os.fstat(fd).st_mode): + raise ProbeError('lock_unavailable') + try: + fcntl.flock(fd, fcntl.LOCK_SH | fcntl.LOCK_NB) + except BlockingIOError: + raise ProbeError('guest_busy') from None + if os.fstat(fd).st_size: + raise ProbeError('guest_busy') + yield + except OSError: + raise ProbeError('lock_unavailable') from None + finally: + if fd is not None: + os.close(fd) + + +def scan(vmid, run=bounded_run): + deadline = time.monotonic() + TOTAL_SECONDS + _guard(vmid, deadline, run) + with _scan_reservation(vmid): + result = _scan_metadata(vmid, deadline, run) + # External PVE lifecycle operations do not share our flock. Fail closed + # if the guest stopped, changed ownership or became locked meanwhile. + _guard(vmid, deadline, run) + return result + + +def _scan_metadata(vmid, deadline, run): + records, issues, consumed, processes, listeners, hestia_panels = [], [], 0, [], [], [] + for kind, script in (('S', SERVICE_SCRIPT), ('P', PROCESS_SCRIPT), ('L', PORT_SCRIPT)): + try: + if consumed >= MAX_RECORDS: + issues.append('metadata_limit') + break + parsed, limited, count = parse_records(run( + ['pct', 'exec', str(vmid), '--', 'timeout', '-s', 'KILL', '2', + 'sh', '-c', script, 'discovery', str(MAX_RECORDS - consumed)], deadline), kind) + if consumed + count > MAX_RECORDS: + issues.append('metadata_limit') + parsed = parsed[:MAX_RECORDS - consumed] + consumed = min(MAX_RECORDS, consumed + count) + # The entire collector, not each metadata source, has 256 records. + capacity = MAX_RECORDS - len(records) + if kind == 'S': + records.extend(parsed[:capacity]) + elif kind == 'P': + processes = parsed[:capacity] + else: + listeners = parsed[:capacity] + if limited or len(parsed) > capacity: + issues.append('metadata_limit') + except ProbeError as exc: + issues.append(str(exc)) + try: + # This is outside the generic record budget: it returns only an exact, + # fixed Hestia signature, not additional arbitrary process metadata. + hestia_panels, limited, _ = parse_records(run( + ['pct', 'exec', str(vmid), '--', 'timeout', '-s', 'KILL', '2', + 'sh', '-c', HESTIA_PANEL_SCRIPT, 'discovery'], deadline), 'H') + if limited: + issues.append('metadata_limit') + except ProbeError as exc: + issues.append(str(exc)) + for process in processes: + exe, directory = process['executable'], process['root'] + related = [record for record in records if record['executable'] == exe and ( + not _RUNTIMES.fullmatch(Path(exe).name) or record['root'] == directory)] + ports = [entry['port'] for entry in listeners if entry['pid'] == process['pid']] + if len(related) == 1: + related[0].setdefault('listening_ports', []).extend(ports) + elif not related and _hestia_panel_process(exe, ports): + records.append({'name': 'Hestia', 'path': exe, 'root': _HESTIA_ROOT, + 'process': exe, 'process_root': directory, 'executable': exe, + 'listening_ports': ports}) + elif not related and (_custom(exe) and _custom(directory) + and not _system(Path(exe).name) + and not _helper(Path(exe).name) + and not _RUNTIMES.fullmatch(Path(exe).name) + and exe.startswith(directory.rstrip('/') + '/') + and _NAME.fullmatch(Path(exe).name)): + records.append({'name': Path(exe).name, 'path': exe, 'root': directory, + 'process': exe, 'executable': exe, 'listening_ports': ports}) + for panel in hestia_panels: + records.append({'name': 'Hestia', 'path': panel['executable'], 'root': _HESTIA_ROOT, + 'process': panel['executable'], 'process_root': panel['root'], + 'executable': panel['executable'], + 'listening_ports': [panel['port']]}) + items, seen = [], set() + for record in records: + key = (record['path'], record['root']) + if key in seen: + continue + seen.add(key) + if len(items) >= MAX_CANDIDATES: + issues.append('candidate_limit') + break + try: + probe_record = dict(record) + probe_record['vmid'] = vmid + if 'manifest_anchor' in record: + probe_record, footprint = _manifest_footprint(probe_record, run, deadline) + if probe_record is None: + continue + else: + footprint = run(['pct', 'exec', str(vmid), '--', 'timeout', '-s', 'KILL', '2', + 'sh', '-c', FOOTPRINT_SCRIPT, + 'discovery', record['path'], record['root']], deadline) + item = candidate(vmid, probe_record, footprint) + items.append(item) + if item['metadata_issue']: + issues.append(item['metadata_issue']) + except ProbeError as exc: + issues.append(str(exc)) + if str(exc) == 'deadline': + break + return {'vmid': vmid, 'status': 'partial' if issues else 'complete', + 'candidates': items, 'issues': sorted(set(issues)), 'stale': False, + 'scanned_at': time.time()} + + +def cached(vmid): + with _mutex: + value = copy.deepcopy(_cache.get(int(vmid))) + if value is None: + return {'vmid': int(vmid), 'status': 'not_scanned', 'candidates': [], 'stale': False} + if time.time() - value['scanned_at'] >= CACHE_SECONDS: + value['stale'] = True + return value + + +def mark_stale(vmid): + """Lifecycle events retire freshness, never collect new guest metadata.""" + with _mutex: + _generations[int(vmid)] = _generations.get(int(vmid), 0) + 1 + if int(vmid) in _cache: + _cache[int(vmid)]['stale'] = True + + +def discover(vmid): + vmid = int(vmid) + with _mutex: + lock = _locks.setdefault(vmid, threading.Lock()) + if not lock.acquire(blocking=False): + return {**cached(vmid), 'status': 'busy', 'stale': True} + try: + with _mutex: + generation = _generations.get(vmid, 0) + try: + result = scan(vmid) + except ProbeError as exc: + result = {**cached(vmid), 'status': 'error', 'issues': [str(exc)], 'stale': True} + result.setdefault('scanned_at', time.time()) + # Comparing the generation and publishing must be atomic with lifecycle + # invalidation, including the first scan when no cache exists yet. + with _mutex: + previous = copy.deepcopy(_cache.get(vmid)) + if _generations.get(vmid, 0) != generation: + result = {**(previous or {'vmid': vmid, 'candidates': [], + 'scanned_at': time.time()}), + 'status': 'error', 'issues': ['guest_changed'], 'stale': True} + elif result['status'] != 'complete' and previous and previous['candidates']: + # Partial output cannot prove that previously seen apps disappeared. + ids = {item['discovery_id'] for item in result['candidates']} + result['candidates'].extend(item for item in previous['candidates'] + if item['discovery_id'] not in ids) + result['candidates'] = result['candidates'][:MAX_CANDIDATES] + result['stale'] = True + _cache[vmid] = copy.deepcopy(result) + return result + finally: + lock.release() + + +def filter_candidates(result, suggestions, apps): + """Pure composition: never run catalog probes, load or write registries.""" + known = [*apps, *(suggestions.get('extras') or []), + *(suggestions.get('docker_workloads') or [])] + if suggestions.get('helper_slug'): + known.append({'name': suggestions.get('name_suggestion'), + 'helper_slug': suggestions['helper_slug'], + 'tracking_suggestion': suggestions.get('tracking_suggestion')}) + identities = {a.get('discovery_id') for a in known if a.get('discovery_id')} + names, paths = set(), set() + for app in known: + for field in ('name', 'slug', 'helper_slug', 'package'): + if app.get(field): + names.add(str(app[field]).casefold()) + detector = app.get('tracking_suggestion') or app + for field in ('binary_path', 'file_path'): + if detector.get(field): + paths.add(detector[field]) + items = [item for item in result['candidates'] + if item['discovery_id'] not in identities + and item['name'].casefold() not in names + and item['installation_path'] not in paths] + return {**result, 'candidates': items} diff --git a/AppImage/scripts/tests/test_fwbr_udev_rule_order.py b/AppImage/scripts/tests/test_fwbr_udev_rule_order.py index 70f61b5f..231fc61d 100644 --- a/AppImage/scripts/tests/test_fwbr_udev_rule_order.py +++ b/AppImage/scripts/tests/test_fwbr_udev_rule_order.py @@ -26,18 +26,18 @@ class FwbrUdevRuleOrderTests(unittest.TestCase): def test_auto_post_install_uses_late_rule_and_removes_legacy_rule(self): source = network_optimization_block(AUTO_POST_INSTALL) self.assertIn('local FUNC_VERSION="1.2"', source) - self.assertIn(f"rm -f /etc/udev/rules.d/{OLD_RULE}", source) - self.assertIn(f"cat > /etc/udev/rules.d/{NEW_RULE}", source) + self.assertIn(f"pmx_remove_file /etc/udev/rules.d/{OLD_RULE}", source) + self.assertIn(f"pmx_write_file /etc/udev/rules.d/{NEW_RULE}", source) self.assertIn(f"chmod 0644 /etc/udev/rules.d/{NEW_RULE}", source) - self.assertNotIn(f"cat > /etc/udev/rules.d/{OLD_RULE}", source) + self.assertNotIn(f"pmx_write_file /etc/udev/rules.d/{OLD_RULE}", source) def test_customizable_post_install_uses_late_rule_and_removes_legacy_rule(self): source = network_optimization_block(CUSTOM_POST_INSTALL) self.assertIn('local FUNC_VERSION="1.2"', source) - self.assertIn(f"rm -f /etc/udev/rules.d/{OLD_RULE}", source) - self.assertIn(f"cat > /etc/udev/rules.d/{NEW_RULE}", source) + self.assertIn(f"pmx_remove_file /etc/udev/rules.d/{OLD_RULE}", source) + self.assertIn(f"pmx_write_file /etc/udev/rules.d/{NEW_RULE}", source) self.assertIn(f"chmod 0644 /etc/udev/rules.d/{NEW_RULE}", source) - self.assertNotIn(f"cat > /etc/udev/rules.d/{OLD_RULE}", source) + self.assertNotIn(f"pmx_write_file /etc/udev/rules.d/{OLD_RULE}", source) def test_uninstall_removes_new_and_legacy_rule_names(self): source = UNINSTALL_TOOLS.read_text() diff --git a/AppImage/scripts/tests/test_monitor_links.py b/AppImage/scripts/tests/test_monitor_links.py new file mode 100644 index 00000000..48036393 --- /dev/null +++ b/AppImage/scripts/tests/test_monitor_links.py @@ -0,0 +1,150 @@ +import json +from pathlib import Path +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest import mock + +ROOT = Path(__file__).resolve().parents[3] +sys.path.insert(0, str(ROOT / "AppImage/scripts")) + +import monitor_links as links + + +class MonitorLinksTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + storage = self.root / "storage.cfg" + storage.write_text("dir: local\n\tpath /var/lib/vz\n\npbs: Lab\n\tdatastore store1\n\tserver 10.0.0.49\n\n" + "pbs: Cloud\n\tdatastore x\n\tserver pbs.example.com\n\tport 8443\n") + patches = [ + mock.patch.object(links, "LINKS_FILE", self.root / "links.json"), + mock.patch.object(links, "SSL_CONFIG_FILE", self.root / "ssl.json"), + mock.patch.object(links, "STORAGE_CFG", storage), + mock.patch.object(links.pbs_manager, "get_platform", return_value={"product": "pve", "hostname": "amd"}), + mock.patch.object(links.pbs_manager, "public_servers", return_value=[]), + ] + for patch in patches: + patch.start() + self.addCleanup(patch.stop) + links._probe_cache.clear() + cluster = [{"type": "cluster", "name": "Casa"}, + {"type": "node", "name": "amd", "ip": "10.0.0.55", "local": 1}, + {"type": "node", "name": "constructor", "ip": "10.0.0.50", "local": 0}] + self.run_patch = mock.patch.object(links.subprocess, "run", + return_value=SimpleNamespace(returncode=0, stdout=json.dumps(cluster))) + self.run_patch.start() + self.addCleanup(self.run_patch.stop) + + def test_normalize_url_keeps_only_the_origin_and_rejects_credentials(self): + self.assertEqual(links.normalize_url("192.168.0.49:8008/dashboard?x=1"), "https://192.168.0.49:8008") + self.assertEqual(links.normalize_url("http://pve.lan"), "http://pve.lan") + for bad in ("", "ftp://host", "https://user:pass@host", "https://host:99999", "javascript:alert(1)"): + with self.subTest(bad=bad), self.assertRaises(links.LinkError): + links.normalize_url(bad) + + def test_discovers_other_cluster_nodes_and_pbs_without_the_local_node(self): + probed = [] + with mock.patch.object(links, "_probe", side_effect=lambda url: probed.append(url) or url.endswith("10.0.0.50:8008")), \ + mock.patch.object(links, "_is_private", side_effect=lambda host: host.startswith("10.")): + result = links.list_links() + by_key = {row["key"]: row for row in result["links"]} + self.assertEqual(result["self"]["name"], "amd") + self.assertNotIn("cluster:amd", by_key) + self.assertEqual(by_key["cluster:constructor"]["status"], "online") + self.assertEqual(by_key["pbs:10.0.0.49"]["native_url"], "https://10.0.0.49:8007") + self.assertEqual(by_key["pbs:10.0.0.49"]["status"], "offline") + # A public PBS is listed with its own interface but never probed unasked. + self.assertEqual(by_key["pbs:pbs.example.com"]["status"], "unknown") + self.assertEqual(by_key["pbs:pbs.example.com"]["native_url"], "https://pbs.example.com:8443") + self.assertFalse(any("example.com" in url for url in probed)) + + def test_pbs_host_lists_only_manual_entries(self): + links.pbs_manager.get_platform.return_value = {"product": "pbs", "hostname": "pbs-lab"} + links.add_link({"name": "amd", "url": "https://10.0.0.55:8008", "product": "pve"}) + with mock.patch.object(links, "_probe", return_value=True): + result = links.list_links() + self.assertEqual([row["name"] for row in result["links"]], ["amd"]) + self.links_run_not_called() + + def links_run_not_called(self): + self.assertFalse(any(call.args and call.args[0][:1] == ["pvesh"] for call in links.subprocess.run.call_args_list)) + + def test_manual_entries_overrides_and_hidden_detected_nodes(self): + key = links.add_link({"name": "lab", "url": "10.0.0.46:8008", "product": "pve"}) + with self.assertRaises(links.LinkError): + links.add_link({"url": "https://10.0.0.46:8008"}) + links.update_link("cluster:constructor", {"url": "https://constructor.lan:8008"}) + links.update_link("pbs:10.0.0.49", {"hidden": True}) + with mock.patch.object(links, "_probe", return_value=False), \ + mock.patch.object(links, "_is_private", return_value=True): + visible = {row["key"]: row for row in links.list_links()["links"]} + everything = {row["key"]: row for row in links.list_links(include_hidden=True)["links"]} + self.assertEqual(visible[key]["url"], "https://10.0.0.46:8008") + self.assertEqual(visible["cluster:constructor"]["url"], "https://constructor.lan:8008") + self.assertNotIn("pbs:10.0.0.49", visible) + self.assertTrue(everything["pbs:10.0.0.49"]["hidden"]) + with self.assertRaises(links.LinkError): + links.delete_link("cluster:constructor") + links.delete_link(key) + self.assertEqual(json.loads((self.root / "links.json").read_text())["manual"], []) + self.assertEqual((self.root / "links.json").stat().st_mode & 0o777, 0o600) + + def test_cached_list_is_served_without_new_probes_and_renewed_when_stale(self): + links._snapshot.update(at=0.0, data=None) + self.addCleanup(links._snapshot.update, at=0.0, data=None) + links.update_link("pbs:10.0.0.49", {"hidden": True}) + with mock.patch.object(links, "_probe", return_value=True) as probe, \ + mock.patch.object(links, "_is_private", return_value=True): + first = links.cached_links() + calls = probe.call_count + again = links.cached_links() + self.assertEqual(probe.call_count, calls) + self.assertEqual(first, again) + self.assertNotIn("pbs:10.0.0.49", [row["key"] for row in first["links"]]) + self.assertIn("pbs:10.0.0.49", [row["key"] for row in links.cached_links(include_hidden=True)["links"]]) + links._snapshot["at"] -= links.PROBE_TTL + 1 + with mock.patch.object(links.threading, "Thread") as thread: + stale = links.cached_links() + self.assertEqual(stale, first) + thread.assert_called_once() + + def test_unknown_keys_are_rejected(self): + for key in ("other:x", "cluster:../../etc", "manual:nope"): + with self.subTest(key=key), self.assertRaises(links.LinkError): + links.update_link(key, {"hidden": True}) + + def test_probe_sends_no_credentials_and_needs_a_monitor_answer(self): + seen = [] + + class Response: + status = 200 + + def __init__(self, body): + self.body = body + + def read(self, _size): + return self.body + + def __enter__(self): + return self + + def __exit__(self, *_): + return False + + def urlopen(request, timeout, context): + seen.append(request) + return Response(b'{"auth_enabled": true}' if "monitor" in request.full_url else b"") + + with mock.patch.object(links.urllib.request, "urlopen", side_effect=urlopen): + self.assertTrue(links._probe("https://monitor:8008")) + self.assertFalse(links._probe("https://other:8008")) + self.assertTrue(all(request.full_url.endswith("/api/auth/status") for request in seen)) + self.assertTrue(all("Authorization" not in request.headers for request in seen)) + + +if __name__ == "__main__": + unittest.main() diff --git a/AppImage/scripts/tests/test_native_app_discovery.py b/AppImage/scripts/tests/test_native_app_discovery.py new file mode 100644 index 00000000..dc24424a --- /dev/null +++ b/AppImage/scripts/tests/test_native_app_discovery.py @@ -0,0 +1,785 @@ +"""Host-independent discovery fixtures; no real pct, guest, auth or registry.""" +import ast +import copy +import fcntl +from functools import wraps +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import threading +import time +import unittest +from unittest.mock import Mock, patch + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +import native_app_discovery as native +import lxc_apps + + +def service(name='handmade', exe='/opt/handmade/server', directory='/opt/handmade', manager='systemd', script='', source=None): + path = source or ('/etc/systemd/system/' + name + '.service' if manager == 'systemd' + else '/etc/init.d/' + name) + return '\t'.join(['S', path, manager, exe, script, directory]) + '\nEND\n' + + +class DiscoveryFixtures(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.config = self.root / 'lxc' + self.config.mkdir() + (self.config / '101.conf').write_text('ostype: debian\n') + self.oci = self.root / 'oci' + self.oci.mkdir() + for context in (patch.object(native, 'LOCAL_CONFIG', self.config), + patch.object(native, 'OCI_ROOT', self.oci), + patch.object(native, 'UPDATE_LOCK_ROOT', self.root)): + context.start() + self.addCleanup(context.stop) + native._cache.clear() + native._locks.clear() + native._generations.clear() + self.calls = [] + + def runner(self, services=None, processes='END\n', footprint='FILE\n', manifest=None, + status='status: running', ports='END\n', hestia_panels='END\n'): + def run(argv, deadline): + self.calls.append(argv) + self.assertLessEqual(deadline - time.monotonic(), native.TOTAL_SECONDS) + if argv[1] == 'status': + return status + self.assertEqual(argv[:6], ['pct', 'exec', '101', '--', 'timeout', '-s']) + self.assertEqual(argv[6:9], ['KILL', '2', 'sh']) + script = argv[10] + if script == native.SERVICE_SCRIPT: + return services if services is not None else service() + if script == native.PROCESS_SCRIPT: + return processes + if script == native.PORT_SCRIPT: + return ports + if script == native.HESTIA_PANEL_SCRIPT: + return hestia_panels + if script == native.MANIFEST_FOOTPRINT_SCRIPT: + if manifest is not None: + return manifest + return 'MANIFEST\tpackage.json\n' + footprint.removeprefix('FILE\n') + if script == native.FOOTPRINT_SCRIPT: + return footprint + self.fail('Unexpected probe') + return run + + def test_unknown_version_is_present_with_evidence_and_no_capabilities(self): + result = native.scan(101, self.runner()) + self.assertEqual(result['status'], 'complete') + item, = result['candidates'] + self.assertIsNone(item['installed_version']) + self.assertIsNone(item['tracking_suggestion']) + self.assertEqual(item['default_ports'], []) + self.assertEqual({e['kind'] for e in item['evidence']}, {'installation_file', 'systemd'}) + for field in ('helper_slug', 'repo', 'update_command', 'binary_path', 'upstream_type'): + self.assertNotIn(field, item) + + def test_openrc_node_script_known_version_is_manual_and_secret_free(self): + result = native.scan(101, self.runner( + service(exe='/usr/bin/node', script='/opt/handmade/server.js', manager='openrc'), + footprint='FILE\n' + json.dumps({'version': '1.2.3', 'scripts': {'start': 'SECRET'}, 'env': 'PASSWORD'}))) + item, = result['candidates'] + self.assertEqual(item['installed_version'], '1.2.3') + self.assertEqual(item['tracking_suggestion']['installed_via'], 'manual') + self.assertNotIn('SECRET', json.dumps(result)) + self.assertNotIn('PASSWORD', json.dumps(result)) + self.assertFalse(any('systemctl' in arg for call in self.calls for arg in call)) + + def test_inactive_service_and_packaged_application_are_not_hidden(self): + result = native.scan(101, self.runner(service(name='legit-app', exe='/usr/bin/legit-app', directory=''))) + self.assertEqual(result['candidates'][0]['name'], 'legit-app') + + def test_system_services_and_ambiguous_runtimes_have_zero_footprint_probes(self): + for name, exe in [('ssh', '/usr/sbin/sshd'), ('cron', '/usr/sbin/cron'), + ('postfix', '/usr/lib/postfix/master'), ('rsyslog', '/usr/sbin/rsyslogd'), + ('systemd-networkd', '/usr/lib/systemd/systemd-networkd'), + ('ambiguous', '/usr/bin/python3')]: + self.calls.clear() + result = native.scan(101, self.runner(service(name=name, exe=exe))) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_common_system_daemons_and_vendor_units_have_zero_footprint_probes(self): + cases = [ + ('nginx', '/usr/sbin/nginx', 'openrc', None), + ('apache2', '/usr/sbin/apache2', 'systemd', None), + ('redis-server', '/usr/bin/redis-server', 'systemd', None), + ('mariadb', '/usr/sbin/mariadbd', 'systemd', None), + ('postgresql', '/usr/lib/postgresql/16/bin/postgres', 'systemd', None), + ('php8.3-fpm', '/usr/sbin/php-fpm8.3', 'systemd', None), + ('vendor-daemon', '/usr/bin/vendor-daemon', 'systemd', + '/usr/lib/systemd/system/vendor-daemon.service'), + ] + for name, exe, manager, source in cases: + with self.subTest(name=name): + self.calls.clear() + result = native.scan(101, self.runner( + service(name=name, exe=exe, manager=manager, source=source))) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_alpine_openrc_infrastructure_has_zero_footprint_probes(self): + # Actual Alpine 3.24.1 CT217 init metadata: klogd, user and runsvdir. + # The runsvdir definition can exist without /usr/bin/runsvdir installed; + # rejecting it must precede probing, not turn the scan into partial. + for name, executable in ( + ('klogd', '/sbin/klogd'), + ('user', '/usr/libexec/rc/bin/openrc-user'), + ('runsvdir', '/usr/bin/runsvdir'), + ('renamed-system-service', '/usr/libexec/rc/bin/openrc-user'), + ): + with self.subTest(name=name): + self.calls.clear() + result = native.scan(101, self.runner( + service(name=name, exe=executable, directory='', manager='openrc'))) + self.assertEqual(result['status'], 'complete') + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_helper_services_and_templates_have_zero_footprint_probes(self): + cases = [ + ('netpulse-stable-update', '/bin/bash', '/opt/netpulse', '/opt/netpulse/stable-apply.sh'), + ('rent-tool-backup', '/bin/bash', '/home/app/rent-tool', '/home/app/rent-tool/scripts/backup-db.sh'), + ('keynest-update', '/bin/bash', '/opt/keynest', '/opt/keynest/keynest-update.sh'), + ('cortendesk-bootstrap-worker', '/bin/bash', '/opt/cortendesk', '/opt/cortendesk/bootstrap-worker.sh'), + ('cortendesk-source-worker', '/bin/bash', '/opt/cortendesk', '/opt/cortendesk/source-worker.sh'), + ('cortendesk-managed-password@', '/bin/bash', '/opt/cortendesk', '/opt/cortendesk/password-bridge.sh'), + ('miminails-watch', '/usr/local/bin/miminails-watch', '/usr/local/bin', ''), + ] + for name, exe, directory, script in cases: + with self.subTest(name=name): + self.calls.clear() + result = native.scan(101, self.runner(service( + name=name, exe=exe, directory=directory, script=script))) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_hestia_panel_is_recognized_only_from_its_exact_runtime_and_port(self): + hestia = '/usr/local/hestia/nginx/sbin/hestia-nginx' + panel = f'H\t391\t{hestia}\t/\t8083\nEND\n' + result = native.scan(101, self.runner( + services='END\n', processes='LIMIT\n', ports='END\n', hestia_panels=panel)) + item, = result['candidates'] + self.assertEqual(item['name'], 'Hestia') + self.assertEqual(item['installation_path'], hestia) + self.assertEqual(item['listening_ports'], [8083]) + self.assertEqual(item['default_ports'], [8083]) + self.assertEqual(item['scheme'], 'https') + self.assertIn({'kind': 'working_directory', 'value': '/'}, item['evidence']) + self.assertIn('metadata_limit', result['issues']) + + unrelated = native.candidate(101, { + 'name': 'other-panel', 'path': '/opt/other-panel/server', 'root': '/opt/other-panel', + 'process': '/opt/other-panel/server', 'listening_ports': [8083], + }, 'FILE\n') + self.assertEqual(unrelated['default_ports'], []) + self.assertNotIn('scheme', unrelated) + + self.calls.clear() + result = native.scan(101, self.runner( + services='END\n', processes='LIMIT\n', ports='END\n')) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + with self.assertRaises(native.ProbeError): + native.parse_records(f'H\t391\t{hestia}\t/\t8443\nEND\n', 'H') + + def test_node_and_npm_wrappers_use_only_bounded_manifest_roots(self): + cases = [ + ('keynest', '/usr/bin/node', '/opt/keynest'), + ('rent-tool', '/usr/local/bin/npm', '/home/app/rent-tool'), + ] + for name, exe, directory in cases: + with self.subTest(name=name): + self.calls.clear() + result = native.scan(101, self.runner( + service(name=name, exe=exe, directory=directory), + footprint='FILE\n{"version":"1.2.3"}')) + item, = result['candidates'] + self.assertEqual(item['name'], name) + self.assertEqual(item['installation_path'], directory + '/package.json') + self.assertEqual(item['installed_version'], '1.2.3') + footprint_calls = [call for call in self.calls if native.MANIFEST_FOOTPRINT_SCRIPT in call] + self.assertEqual(len(footprint_calls), 1) + if directory.startswith('/home/'): + parent = '/home/app' + else: + parent = '/opt' + self.assertEqual(footprint_calls[0][-3:], [directory, directory, parent]) + + self.calls.clear() + result = native.scan(101, self.runner(service( + name='ambiguous-home-runtime', exe='/usr/bin/node', directory='/home/app'))) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.MANIFEST_FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_node_release_layout_uses_only_server_manifest_under_shared_root(self): + self.calls.clear() + result = native.scan(101, self.runner( + service(name='keynest', exe='/opt/keynest/node/bin/node', directory='/opt/keynest/current'), + manifest='MANIFEST\tserver/package.json\n{"version":"1.5.63"}')) + item, = result['candidates'] + self.assertEqual(item['name'], 'keynest') + self.assertEqual(item['installation_path'], '/opt/keynest/current/server/package.json') + self.assertEqual(item['installed_version'], '1.5.63') + manifest_calls = [call for call in self.calls if native.MANIFEST_FOOTPRINT_SCRIPT in call] + self.assertEqual(len(manifest_calls), 1) + self.assertEqual(manifest_calls[0][-3:], + ['/opt/keynest/current', '/opt/keynest', '/opt']) + + def test_node_release_layout_rejects_manifest_outside_operator_root(self): + self.calls.clear() + result = native.scan(101, self.runner( + service(name='outside-root', exe='/opt/outside/node/bin/node', directory='/opt/outside/current'), + manifest='OUTSIDE\n')) + self.assertEqual(result['candidates'], []) + self.assertIn('unsafe_manifest_root', result['issues']) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_actual_manifest_probe_accepts_release_and_rejects_symlink_escape(self): + parent = self.root / 'opt' + anchor = parent / 'keynest' + release = anchor / 'releases' / 'v1.5.63' + manifest = release / 'server' / 'package.json' + manifest.parent.mkdir(parents=True) + manifest.write_text('{"version":"1.5.63"}') + current = anchor / 'current' + current.symlink_to(release) + output = native.bounded_run( + ['sh', '-c', native.MANIFEST_FOOTPRINT_SCRIPT, 'fixture', + str(current), str(anchor), str(parent)], time.monotonic() + 3) + self.assertEqual(output, 'MANIFEST\tserver/package.json\n{"version":"1.5.63"}') + + outside = self.root / 'outside' / 'package.json' + outside.parent.mkdir() + outside.write_text('{"version":"not-readable"}') + manifest.unlink() + manifest.symlink_to(outside) + output = native.bounded_run( + ['sh', '-c', native.MANIFEST_FOOTPRINT_SCRIPT, 'fixture', + str(current), str(anchor), str(parent)], time.monotonic() + 3) + self.assertEqual(output, 'OUTSIDE\n') + self.assertNotIn('not-readable', output) + + def test_node_wrapper_with_no_fixed_manifest_is_not_an_error_or_candidate(self): + self.calls.clear() + result = native.scan(101, self.runner( + service(name='missing-manifest', exe='/opt/missing/node/bin/node', directory='/opt/missing/current'), + manifest='MISSING\n')) + self.assertEqual(result['status'], 'complete') + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_helper_process_fallback_has_zero_footprint_probes(self): + self.calls.clear() + result = native.scan(101, self.runner( + services='END\n', processes='P\t123\t/opt/cortendesk/run-worker\t/opt/cortendesk\nEND\n')) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + + def test_missing_remote_qemu_stopped_busy_oci_guards_precede_exec(self): + scenarios = ['missing', 'stopped', 'busy', 'oci', 'invalid-oci', 'unmanaged'] + for scenario in scenarios: + with self.subTest(scenario=scenario): + (self.config / '101.conf').write_text('ostype: debian\n') + if scenario == 'missing': + (self.config / '101.conf').unlink() + if scenario == 'busy': + (self.config / '101.conf').write_text('lock: backup\n') + if scenario == 'unmanaged': + (self.config / '101.conf').write_text('ostype: unmanaged\n') + if 'oci' in scenario: + (self.oci / '101').mkdir(exist_ok=True) + (self.oci / '101' / 'oci-compose.json').write_text('broken') + self.calls.clear() + with self.assertRaises(native.ProbeError): + native.scan(101, self.runner(status='status: stopped' if scenario == 'stopped' else 'status: running')) + self.assertFalse(any(call[1] == 'exec' for call in self.calls)) + if (self.oci / '101').exists(): + (self.oci / '101' / 'oci-compose.json').unlink() + (self.oci / '101').rmdir() + + def test_failure_and_partial_output_are_not_successful_empty_results(self): + for text in ('', 'malformed\nEND\n', 'S\tpartial', 'LIMIT\n'): + result = native.scan(101, self.runner(services=text)) + self.assertEqual(result['status'], 'partial') + run = self.runner() + def failed(argv, deadline): + if argv[1] == 'exec': + raise native.ProbeError('read_failed') + return run(argv, deadline) + self.assertEqual(native.scan(101, failed)['status'], 'partial') + + def test_optional_invalid_version_metadata_keeps_proven_candidate(self): + result = native.scan(101, self.runner(footprint='FILE\n{broken')) + self.assertEqual(result['status'], 'partial') + item, = result['candidates'] + self.assertIsNone(item['installed_version']) + self.assertIsNone(item['tracking_suggestion']) + + def test_working_directory_manifest_does_not_version_an_unrelated_executable(self): + result = native.scan(101, self.runner(service(exe='/opt/other/server'), + footprint='FILE\n{"version":"1.2.3"}')) + self.assertIsNone(result['candidates'][0]['installed_version']) + + def test_path_injection_and_port_only_data_do_not_become_candidates(self): + for text in (service(exe='/opt/app/$(id)'), service(exe='/opt/../server'), + service(name='app;id'), service(exe='/bin/sh')): + self.assertEqual(native.scan(101, self.runner(text))['candidates'], []) + with self.assertRaises(native.ProbeError): + native.parse_records('PORT\t8080\nEND\n', 'P') + + def test_same_process_deduplicates_but_two_apps_using_node_do_not_merge(self): + services = service(exe='/usr/bin/node', script='/opt/handmade/server.js').removesuffix('END\n') + services += service(name='second', exe='/usr/bin/node', script='/opt/second/server.js', directory='/opt/second') + result = native.scan(101, self.runner(services)) + self.assertEqual(len(result['candidates']), 2) + result = native.scan(101, self.runner(processes='P\t123\t/opt/handmade/server\t/opt/handmade\nEND\n')) + self.assertEqual(len(result['candidates']), 1) + other = native.candidate(102, native.parse_records(service(), 'S')[0][0], 'FILE\n') + self.assertNotEqual(other['discovery_id'], result['candidates'][0]['discovery_id']) + + def test_record_and_candidate_caps(self): + too_many = 'P\t123\t/opt/a/app\t/opt/a\n' * 257 + 'END\n' + with self.assertRaises(native.ProbeError): + native.parse_records(too_many, 'P') + services = ''.join(service(name=f'app{i}', exe=f'/opt/app{i}/server', directory=f'/opt/app{i}').removesuffix('END\n') for i in range(51)) + 'END\n' + result = native.scan(101, self.runner(services)) + self.assertEqual(len(result['candidates']), 50) + self.assertIn('candidate_limit', result['issues']) + + def test_cached_ttl_never_runs_scan_and_error_preserves_last_known(self): + complete = native.scan(101, self.runner()) + with patch.object(native, 'scan', return_value=complete) as scan: + native.discover(101) + for age, stale in ((299.9, False), (300, True), (300.1, True)): + with patch.object(native.time, 'time', return_value=complete['scanned_at'] + age): + self.assertEqual(native.cached(101)['stale'], stale) + self.assertEqual(scan.call_count, 1) + with patch.object(native, 'scan', side_effect=native.ProbeError('timeout')): + result = native.discover(101) + self.assertEqual(result['candidates'], complete['candidates']) + self.assertEqual(native.cached(101)['status'], 'error') + self.assertTrue(native.cached(101)['stale']) + partial = {**complete, 'status': 'partial', 'candidates': [], 'issues': ['read_failed']} + with patch.object(native, 'scan', return_value=partial): + self.assertEqual(native.discover(101)['candidates'], complete['candidates']) + with patch.object(native, 'scan', return_value={**complete, 'candidates': []}): + self.assertEqual(native.discover(101)['candidates'], []) + + def test_lifecycle_during_first_or_cached_scan_cannot_publish_fresh_result(self): + complete = native.scan(101, self.runner()) + for prior in (None, complete): + with self.subTest(cached=prior is not None): + native._cache.clear() + if prior: + native._cache[101] = copy.deepcopy(prior) + entered, release = threading.Event(), threading.Event() + results = [] + def delayed(vmid): + entered.set() + if not release.wait(3): + raise AssertionError('scan release timed out') + return copy.deepcopy(complete) + with patch.object(native, 'scan', side_effect=delayed): + worker = threading.Thread(target=lambda: results.append(native.discover(101))) + worker.start() + try: + self.assertTrue(entered.wait(3)) + native.mark_stale(101) + finally: + release.set() + worker.join(3) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(results), 1) + self.assertTrue(results[0]['stale']) + self.assertIn('guest_changed', results[0]['issues']) + self.assertEqual(results[0]['candidates'], prior['candidates'] if prior else []) + self.assertTrue(native.cached(101)['stale']) + with patch.object(native, 'scan', return_value=copy.deepcopy(complete)): + self.assertFalse(native.discover(101)['stale']) + with patch.object(native, 'scan', side_effect=lambda vmid: ( + native.mark_stale(102), copy.deepcopy(complete))[1]): + self.assertFalse(native.discover(101)['stale']) + + def test_scan_excludes_later_updater_and_releases_lock_after_success_or_failure(self): + path = self.root / 'proxmenux-lxc-update-101.lock' + for fail in (False, True): + with self.subTest(fail=fail): + runner = self.runner() + probes = [] + def checked(argv, deadline): + if argv[1] == 'exec': + attempt = subprocess.run(['flock', '-n', str(path), 'true']) + self.assertEqual(attempt.returncode, 1) + probes.append(argv) + if fail: + raise native.ProbeError('timeout') + return runner(argv, deadline) + native.scan(101, checked) + self.assertTrue(probes) + self.assertEqual(subprocess.run(['flock', '-n', str(path), 'true']).returncode, 0) + self.assertEqual(path.read_bytes(), b'') + + def test_pending_control_reservation_blocks_scan_without_touching_it(self): + path = self.root / 'proxmenux-lxc-update-101.lock' + original = b'{"task":"pending-control"}' + path.write_bytes(original) + with self.assertRaisesRegex(native.ProbeError, 'guest_busy'): + native.scan(101, self.runner()) + self.assertFalse(any(call[1] == 'exec' for call in self.calls)) + self.assertEqual(path.read_bytes(), original) + + def test_guest_stop_during_scan_never_returns_complete(self): + runner = self.runner() + statuses = iter(['status: running', 'status: stopped']) + def stopped(argv, deadline): + return next(statuses) if argv[1] == 'status' else runner(argv, deadline) + with self.assertRaisesRegex(native.ProbeError, 'guest_stopped'): + native.scan(101, stopped) + self.assertTrue(all(call[1] == 'exec' for call in self.calls)) + + def test_scan_reservation_closes_race_after_guard_and_releases_after_exception(self): + import lxc_check_job + path = self.root / 'proxmenux-lxc-update-101.lock' + runner = self.runner() + handles = [] + def starts_update(argv, deadline): + if argv[1] == 'status': + handles.append(lxc_check_job.acquire(101)) + return runner(argv, deadline) + with patch.object(lxc_check_job, 'LOCK_DIR', self.root): + try: + with self.assertRaisesRegex(native.ProbeError, 'guest_busy'): + native.scan(101, starts_update) + self.assertFalse(any(call[1] == 'exec' for call in self.calls)) + finally: + for handle in handles: + handle.close() + def crash(argv, deadline): + if argv[1] == 'exec': + with self.assertRaises(BlockingIOError): + lxc_check_job.acquire(101) + # Another CT must remain available. + lxc_check_job.acquire(102).close() + raise RuntimeError('unexpected probe failure') + return runner(argv, deadline) + with self.assertRaisesRegex(RuntimeError, 'unexpected'): + native.scan(101, crash) + lxc_check_job.acquire(101).close() + self.assertEqual(path.read_bytes(), b'') + + def test_failed_scan_releases_discovery_lock_and_keeps_prior_evidence(self): + complete = native.scan(101, self.runner()) + native._cache[101] = copy.deepcopy(complete) + with patch.object(native, 'scan', side_effect=RuntimeError('unexpected')): + with self.assertRaises(RuntimeError): + native.discover(101) + self.assertFalse(native._locks[101].locked()) + self.assertEqual(native.cached(101)['candidates'], complete['candidates']) + + def test_invalid_lock_inode_and_unreadable_config_fail_before_any_probe(self): + import os + path = self.root / 'proxmenux-lxc-update-101.lock' + target = self.root / 'unrelated' + target.write_bytes(b'keep') + for kind in ('symlink', 'fifo', 'directory'): + with self.subTest(kind=kind): + if kind == 'symlink': + path.symlink_to(target) + elif kind == 'fifo': + os.mkfifo(path) + else: + path.mkdir() + try: + with self.assertRaisesRegex(native.ProbeError, 'lock_unavailable'): + native.scan(101, self.runner()) + self.assertEqual(self.calls, []) + self.assertEqual(target.read_bytes(), b'keep') + finally: + path.rmdir() if kind == 'directory' else path.unlink() + with patch.object(Path, 'open', side_effect=PermissionError): + with self.assertRaisesRegex(native.ProbeError, 'config_unavailable'): + native.scan(101, self.runner()) + self.assertEqual(self.calls, []) + + def test_same_ct_busy_and_different_ct_cache_isolation(self): + lock = threading.Lock() + lock.acquire() + native._locks[101] = lock + with patch.object(native, 'scan') as scanner: + self.assertEqual(native.discover(101)['status'], 'busy') + scanner.assert_not_called() + self.assertEqual(native.cached(102)['status'], 'not_scanned') + + def test_filter_registered_curated_and_docker_children_without_mutation(self): + result = native.scan(101, self.runner()) + original = copy.deepcopy(result) + for suggestions, apps in (({}, [{'discovery_id': result['candidates'][0]['discovery_id']}]), + ({'extras': [{'name': 'Handmade'}]}, []), + ({'docker_workloads': [{'name': 'Handmade'}]}, []), + ({'helper_slug': 'handmade', 'name_suggestion': 'Handmade'}, [])): + self.assertEqual(native.filter_candidates(result, suggestions, apps)['candidates'], []) + self.assertEqual(result, original) + + def test_actual_save_first_check_and_recheck_never_probe_or_configure_updater(self): + with patch.object(lxc_apps, '_APPS_DIR', str(self.root / 'apps')), \ + patch.object(lxc_apps, '_pct_exec', side_effect=AssertionError('forbidden guest execution')) as execute, \ + patch.object(lxc_apps, 'fetch_latest_upstream_details', side_effect=AssertionError('forbidden upstream')) as upstream: + for version in (None, '1.2.3'): + item = native.candidate(101, native.parse_records(service(), 'S')[0][0], + 'FILE\n' + (json.dumps({'version': version}) if version else '')) + payload = {'name': item['name'], 'discovery_id': item['discovery_id'], + 'update_method': 'none', **(item['tracking_suggestion'] or {})} + ok, sidecar = lxc_apps.add_app(101, payload) + self.assertTrue(ok, sidecar) + app = sidecar['apps'][-1] + self.assertEqual(app['state']['installed_version'], version) + self.assertIsNone(app['state']['update_available']) + self.assertEqual(app['update_method'], 'none') + for field in ('helper_slug', 'update_command', 'repo', 'upstream_type'): + self.assertNotIn(field, app) + lxc_apps.check_app(101, app['id'], force=True) + self.assertIsNone(lxc_apps.get_schedule(101)) + self.assertIsNone(lxc_apps.get_bulk_update(101)) + execute.assert_not_called() + upstream.assert_not_called() + + def test_failed_save_has_no_first_check(self): + with patch.object(lxc_apps, '_write_sidecar', return_value=False), \ + patch.object(lxc_apps, '_read_sidecar', return_value=None), \ + patch.object(lxc_apps, 'check_app') as check: + self.assertFalse(lxc_apps.add_app(101, {'name': 'handmade'})[0]) + check.assert_not_called() + + def test_active_update_lock_blocks_all_guest_probes_without_writing(self): + path = self.root / 'proxmenux-lxc-update-101.lock' + path.write_bytes(b'unchanged') + with path.open('rb') as handle: + fcntl.flock(handle, fcntl.LOCK_EX) + with self.assertRaisesRegex(native.ProbeError, 'guest_busy'): + native.scan(101, self.runner()) + self.assertEqual(path.read_bytes(), b'unchanged') + self.assertEqual(self.calls, []) + + def test_actual_service_parser_reads_data_never_executes_or_sources_it(self): + for manager in ('systemd', 'openrc'): + directory = self.root / manager + directory.mkdir() + marker = self.root / ('forbidden-' + manager) + unit = directory / ('handmade.service' if manager == 'systemd' else 'handmade') + content = ('[Service]\nExecStart=/usr/bin/node /opt/handmade/server.js --password SECRET\nWorkingDirectory=/opt/handmade\n' + if manager == 'systemd' else 'command="/usr/bin/node"\ncommand_args="/opt/handmade/server.js --password SECRET"\ndirectory="/opt/handmade"\n') + unit.write_text(content + '\ntouch ' + str(marker) + '\n') + script = native.SERVICE_SCRIPT.replace('/etc/systemd/system /usr/lib/systemd/system /lib/systemd/system /etc/init.d', str(directory)) + # Substitute only fixture paths, not the collector/parser behavior. + if manager == 'openrc': + script = script.replace('/etc/init.d:*)', str(directory) + ':*)') + output = native.bounded_run(['sh', '-c', script], time.monotonic() + 3) + record, = native.parse_records(output, 'S')[0] + self.assertEqual(record['path'], '/opt/handmade/server.js') + self.assertNotIn('SECRET', output) + self.assertFalse(marker.exists()) + + def test_merged_usr_units_are_counted_once(self): + usr = self.root / 'usr-units' + usr.mkdir() + (usr / 'handmade.service').write_text('[Service]\nExecStart=/opt/handmade/server\n') + lib = self.root / 'lib-units' + lib.symlink_to(usr) + check = '[ "$dir" = /lib/systemd/system ] && [ /lib/systemd/system -ef /usr/lib/systemd/system ]' + self.assertIn(check, native.SERVICE_SCRIPT) + script = (native.SERVICE_SCRIPT + .replace('/etc/systemd/system /usr/lib/systemd/system /lib/systemd/system /etc/init.d', f'{usr} {lib}') + .replace(check, f'[ "$dir" = {lib} ] && [ {lib} -ef {usr} ]')) + output = native.bounded_run(['sh', '-c', script], time.monotonic() + 3) + self.assertEqual(output.count('handmade.service'), 1) + + def test_a_guest_without_ss_or_netstat_has_no_listener_evidence(self): + bin_dir = self.root / 'no-tools' + bin_dir.mkdir() + (bin_dir / 'awk').symlink_to('/usr/bin/awk') + output = native.bounded_run(['sh', '-c', 'PATH=' + str(bin_dir) + '\n' + native.PORT_SCRIPT, 'fixture', '256'], + time.monotonic() + 3) + self.assertEqual(native.parse_records(output, 'L'), ([], False, 0)) + + def test_actual_process_parser_skips_docker_and_never_reads_environ_cmdline(self): + proc = self.root / 'proc' + proc.mkdir() + for pid, group in ((1, '0::/\n'), (2, '0::/docker/child\n')): + directory = proc / str(pid) + directory.mkdir() + (directory / 'cgroup').write_text(group) + (directory / 'exe').symlink_to('/opt/handmade/server') + (directory / 'cwd').symlink_to('/opt/handmade') + (directory / 'cmdline').write_text('SECRET') + (directory / 'environ').write_text('PASSWORD') + script = native.PROCESS_SCRIPT.replace('/proc/[0-9]*', str(proc) + '/[0-9]*') + output = native.bounded_run(['sh', '-c', script, 'fixture', '256'], time.monotonic() + 3) + self.assertEqual(output, 'P\t1\t/opt/handmade/server\t/opt/handmade\nEND\n') + self.assertNotIn('"$dir/cmdline"', native.PROCESS_SCRIPT) + self.assertNotIn('"$dir/environ"', native.PROCESS_SCRIPT) + self.assertNotIn('SECRET', output) + + def test_ports_require_exact_process_ownership_and_do_not_infer_http(self): + process = 'P\t123\t/opt/handmade/server\t/opt/handmade\nEND\n' + result = native.scan(101, self.runner(processes=process, ports='L\t123\t8080\nL\t999\t9000\nEND\n')) + item, = result['candidates'] + self.assertEqual(item['listening_ports'], [8080]) + self.assertEqual(item['default_ports'], [], 'TCP listener does not prove HTTP') + result = native.scan(101, self.runner(ports='L\t123\t8080\nEND\n')) + self.assertEqual(result['candidates'][0]['listening_ports'], []) + + def test_shared_runtime_ports_are_assigned_only_by_working_directory(self): + services = service(exe='/usr/bin/node', script='/opt/handmade/server.js').removesuffix('END\n') + services += service(name='second', exe='/usr/bin/node', script='/opt/second/server.js', directory='/opt/second') + process = 'P\t123\t/usr/bin/node\t/opt/second\nEND\n' + result = native.scan(101, self.runner(services, processes=process, ports='L\t123\t8080\nEND\n')) + self.assertEqual(result['candidates'][0]['listening_ports'], []) + self.assertEqual(result['candidates'][1]['listening_ports'], [8080]) + + def test_ss_and_alpine_netstat_parser_are_bounded_and_secret_free(self): + for tool, line in (('ss', 'LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("SECRET",pid=123,fd=7))'), + ('netstat', 'tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 123/SECRET')): + bin_dir = self.root / tool + bin_dir.mkdir() + executable = bin_dir / tool + executable.write_text('#!/bin/sh\nprintf \'%s\\n\' \'' + line + '\'\n') + executable.chmod(0o755) + (bin_dir / 'awk').symlink_to('/usr/bin/awk') + (bin_dir / 'timeout').symlink_to('/usr/bin/timeout') + script = 'PATH=' + str(bin_dir) + '\n' + native.PORT_SCRIPT + output = native.bounded_run(['sh', '-c', script, 'fixture', '256'], time.monotonic() + 3) + self.assertEqual(output, 'L\t123\t8080\nEND\n') + self.assertNotIn('SECRET', output) + executable.write_text('#!/bin/sh\nexit 1\n') + output = native.bounded_run(['sh', '-c', script, 'fixture', '256'], time.monotonic() + 3) + with self.assertRaises(native.ProbeError): + native.parse_records(output, 'L') + + +class BoundedRunner(unittest.TestCase): + def test_output_limit_is_enforced_during_read(self): + with self.assertRaisesRegex(native.ProbeError, 'output_limit'): + native.bounded_run([sys.executable, '-c', 'import os; os.write(2,b"x"*1000000)'], time.monotonic() + 3) + + def test_pipe_holding_child_does_not_bypass_timeout(self): + start = time.monotonic() + with self.assertRaisesRegex(native.ProbeError, 'timeout'): + native.bounded_run([sys.executable, '-c', 'import os,time; os.fork(); time.sleep(10)'], start + .15) + self.assertLess(time.monotonic() - start, .8) + + def test_failures_never_reveal_stderr_and_deadline_blocks_spawn(self): + with self.assertRaisesRegex(native.ProbeError, '^read_failed$'): + native.bounded_run([sys.executable, '-c', 'import sys; sys.stderr.write("SECRET"); sys.exit(1)'], time.monotonic() + 3) + with patch.object(native.subprocess, 'Popen') as launch: + with self.assertRaisesRegex(native.ProbeError, 'deadline'): + native.bounded_run(['forbidden'], time.monotonic() - 1) + launch.assert_not_called() + + +class Routes(unittest.TestCase): + """Execute actual Flask route bodies; fixture auth, no server startup jobs.""" + def setUp(self): + from flask import Flask, request, jsonify, g + self.app = Flask('native-fixture') + self.authorized = True + def require_auth(fn): + @wraps(fn) + def wrapped(*args, **kwargs): + if not self.authorized: + return jsonify({'error': 'auth_required'}), 401 + return fn(*args, **kwargs) + return wrapped + self.globals = {'app': self.app, 'request': request, 'jsonify': jsonify, + 'require_auth': require_auth, '_vm_app_suggestions_cache': {}, + '_VM_APP_SUGGESTIONS_TTL': 300, + '_vm_cache_get': lambda cache, vmid, ttl: cache.get(vmid), + '_vm_cache_put': lambda cache, vmid, value: cache.update({vmid: value}), + '_pvesh_cache': {}, '_vm_disk_cache': {}, '_vm_schedule_cache': {}, '_vm_cache_invalidate': Mock(), + '_invalidate_lxc_ip': Mock(), '_schedule_started_guest_refresh': Mock(), + '_publish_guest_modal_cache_revision': Mock()} + tree = ast.parse((SCRIPTS / 'flask_server.py').read_text()) + routes = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in ( + 'api_vm_apps_suggestions', 'api_vm_apps_native_discovery', 'api_vm_apps_add', + '_handle_guest_lifecycle')] + self.assertEqual(len(routes), 4) + exec(compile(ast.Module(body=routes, type_ignores=[]), str(SCRIPTS / 'flask_server.py'), 'exec'), self.globals) + self.client = self.app.test_client() + + def test_explicit_post_only_get_and_existing_post_never_native_scan(self): + result = {'vmid': 101, 'status': 'complete', 'candidates': [], 'stale': False} + with patch.object(native, 'discover', return_value=result) as scan, \ + patch.object(native, 'cached', return_value=result), \ + patch.object(lxc_apps, 'get_suggestions', return_value={'extras': []}) as suggestions, \ + patch.object(lxc_apps, 'load_sidecar', return_value={'apps': []}), \ + patch.object(lxc_apps, '_write_sidecar') as write: + self.assertEqual(self.client.get('/api/vms/101/apps/native-discovery').status_code, 200) + self.client.get('/api/vms/101/apps/suggestions') + # OCI refresh and Find's existing catalog path retain their purpose. + self.client.post('/api/vms/101/apps/suggestions') + with self.app.test_request_context('/api/vms/101/apps/suggestions'): + from flask import g + g._internal_call = True + self.globals['_vm_app_suggestions_cache'].clear() + self.globals['api_vm_apps_suggestions'](101) + scan.assert_not_called() + self.client.post('/api/vms/101/apps/native-discovery') + scan.assert_called_once_with(101) + self.assertEqual(suggestions.call_count, 2) + write.assert_not_called() + + def test_unauthorized_and_wrong_method_have_zero_scans(self): + with patch.object(native, 'discover') as scan, patch.object(lxc_apps, 'load_sidecar') as read: + self.authorized = False + self.assertEqual(self.client.post('/api/vms/101/apps/native-discovery').status_code, 401) + self.assertEqual(self.client.put('/api/vms/101/apps/native-discovery').status_code, 405) + scan.assert_not_called() + read.assert_not_called() + + def test_actual_lifecycle_marks_last_known_stale_without_scanning(self): + native._cache[101] = {'vmid': 101, 'status': 'complete', 'stale': False, + 'candidates': [], 'scanned_at': time.time()} + with patch.object(native, 'discover') as scan, \ + patch.object(lxc_apps, 'clear_schedule_reboot_required') as clear: + for action in ('start', 'reboot', 'stop'): + self.globals['_handle_guest_lifecycle']('101', 'lxc', action) + self.assertTrue(native.cached(101)['stale']) + scan.assert_not_called() + self.assertEqual(clear.call_count, 2) + + def test_actual_repeated_prewarmer_has_zero_native_scans(self): + tree = ast.parse((SCRIPTS / 'flask_server.py').read_text()) + warmer = next(node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name == '_vm_modal_prewarmer_pass') + environment = dict(self.globals) + for name in ('_vm_details_cache', '_vm_backups_cache', '_vm_schedule_cache', '_vm_mounts_cache'): + environment[name] = {} + for name in ('_VM_DETAILS_TTL', '_VM_BACKUPS_TTL', '_VM_SCHEDULE_TTL', '_VM_MOUNTS_TTL'): + environment[name] = 300 + for name in ('get_vm_config', 'api_vm_backups', 'api_vm_apps_schedule', 'api_lxc_mount_points'): + environment[name] = lambda vmid: None + environment.update({'get_cached_pvesh_cluster_resources_vm': lambda: [ + {'vmid': 101, 'type': 'lxc', 'status': 'running'}], 'time': Mock(), 'sys': sys}) + exec(compile(ast.Module(body=[warmer], type_ignores=[]), 'flask_server.py', 'exec'), environment) + with patch.object(native, 'discover') as scan, \ + patch.object(lxc_apps, 'ensure_oci_registration'), \ + patch.object(lxc_apps, 'load_sidecar', return_value={'apps': []}), \ + patch.object(lxc_apps, 'get_suggestions', return_value={'extras': []}): + for _ in range(2): + environment['_vm_app_suggestions_cache'].clear() + self.assertEqual(environment['_vm_modal_prewarmer_pass'](), 1) + scan.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/AppImage/scripts/tests/test_remote_mount_dismissal.py b/AppImage/scripts/tests/test_remote_mount_dismissal.py new file mode 100644 index 00000000..c66ccf17 --- /dev/null +++ b/AppImage/scripts/tests/test_remote_mount_dismissal.py @@ -0,0 +1,67 @@ +"""Remote mount checks carry the key their dismissal is saved under.""" +import sys +import unittest +from pathlib import Path +from types import ModuleType, SimpleNamespace +from unittest.mock import patch + +SCRIPTS_DIR = Path(__file__).resolve().parents[1] +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) + +_health_persistence_module = ModuleType("health_persistence") +_health_persistence_module.health_persistence = SimpleNamespace(cleanup_old_errors=lambda: None) +_health_persistence_module.disk_base_name = lambda name: str(name).replace("/dev/", "") +sys.modules.setdefault("health_persistence", _health_persistence_module) +sys.modules.setdefault("psutil", ModuleType("psutil")) +sys.modules.setdefault("flask_server", SimpleNamespace( + get_proxmox_node_name=lambda: "fixture", + get_cached_pvesh_cluster_resources_vm=lambda: [], + get_cached_vm_disk=lambda _vmid: None, +)) + +import health_monitor # noqa: E402 + + +class _Persistence: + def __init__(self): + self.recorded = [] + + def record_error(self, **kwargs): + self.recorded.append(kwargs) + + def get_active_errors(self, *args, **kwargs): + return [] + + def clear_error(self, key): + pass + + +class RemoteMountDismissal(unittest.TestCase): + def check(self, host, lxc=()): + persistence = _Persistence() + mounts = SimpleNamespace(scan_remote_mounts=lambda force: list(host), + scan_lxc_mounts=lambda force: list(lxc)) + with patch.object(health_monitor, "MOUNT_MONITOR_AVAILABLE", True), \ + patch.object(health_monitor, "mount_monitor", mounts, create=True), \ + patch.object(health_monitor, "health_persistence", persistence): + result = health_monitor.HealthMonitor._check_remote_mounts(SimpleNamespace()) + return result, persistence + + def test_stale_and_read_only_mounts_carry_their_saved_key(self): + result, persistence = self.check( + [{"target": "/mnt/pve/nas", "status": "stale", "fstype": "nfs4"}, + {"target": "/mnt/share", "status": "ok", "fstype": "cifs"}], + [{"target": "/media", "status": "readonly", "fstype": "nfs4", "lxc_id": "120", "lxc_name": "app"}]) + checks = result["checks"] + self.assertEqual(checks["/mnt/pve/nas"]["error_key"], "mount_stale_/mnt/pve/nas") + self.assertEqual(checks["CT 120 (app) /media"]["error_key"], "mount_readonly_ct120:/media") + self.assertTrue(checks["/mnt/pve/nas"]["dismissable"]) + self.assertTrue(checks["CT 120 (app) /media"]["dismissable"]) + self.assertNotIn("error_key", checks["/mnt/share"]) + self.assertEqual({entry["error_key"] for entry in persistence.recorded}, + {"mount_stale_/mnt/pve/nas", "mount_readonly_ct120:/media"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e76a028..6e645e4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -876,8 +876,8 @@ and traceback before changing catalogs or translation-provider settings. #### Qualified offline `tests/` CI lanes `.github/workflows/test-offline-qualified.yml` runs **only** the explicitly listed -nine Python files (including the inert active-notification payload fixture and -the focused health recurrence tests) and eleven +ten Python files (including the native discovery fixtures, the inert active-notification payload fixture and +the focused health recurrence tests) and twelve Node files in `.github/scripts/run_offline_qualified.py`. The Python runner gives each test file a private temporary directory through `TMPDIR`. It removes that directory after the test file finishes. @@ -900,7 +900,7 @@ dangling symlink) rather than overwrite or reuse it. In Bash: ) ``` -Requires Python 3.11, Node 22.14, npm and Bash/coreutils for the bounded shell +Requires Python 3.11, Flask 3.x for the native route fixtures, Node 22.14, npm and Bash/coreutils for the bounded shell fixtures. The separate lockfile installs only TypeScript, React, React DOM, Lucide icons and the locked scheduler dependency; it does not build the Monitor or run npm lifecycle scripts. `--legacy-peer-deps` admits the existing Lucide diff --git a/install_proxmenux.sh b/install_proxmenux.sh index e2aee65a..5d2975cd 100755 --- a/install_proxmenux.sh +++ b/install_proxmenux.sh @@ -21,7 +21,7 @@ # • whiptail (interactive terminal menus) # • curl (downloads and connectivity checks) # • jq (JSON parsing) -# • Python 3 + venv (for translation support) +# - Installs pre-generated JSON language catalogs (no translation runtime). # - Creates the ProxMenux base directories and configuration files: # • $BASE_DIR/config.json # • $BASE_DIR/cache.json @@ -396,7 +396,8 @@ select_language() { "de" "German" \ "it" "Italian" \ "pt" "Portuguese" \ - "sk" "Slovenčina" 3>&1 1>&2 2>&3) + "sk" "Slovenčina" \ + "sv" "Svenska" 3>&1 1>&2 2>&3) if [ -z "$LANGUAGE" ]; then msg_error "No language selected. Exiting." diff --git a/install_proxmenux_beta.sh b/install_proxmenux_beta.sh index 90022cd2..225b3502 100644 --- a/install_proxmenux_beta.sh +++ b/install_proxmenux_beta.sh @@ -594,7 +594,8 @@ select_language() { "de" "German" \ "it" "Italian" \ "pt" "Portuguese" \ - "sk" "Slovenčina" 3>&1 1>&2 2>&3) + "sk" "Slovenčina" \ + "sv" "Svenska" 3>&1 1>&2 2>&3) if [ -z "$LANGUAGE" ]; then msg_error "No language selected. Exiting." diff --git a/lang/de.json b/lang/de.json index 8d643f1a..e81d7622 100644 --- a/lang/de.json +++ b/lang/de.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (Proxmox-Standard-vzdump-Pfad)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (Proxmox-Standard)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = Sticky Bit + RWX für alle. Keine gemeinsame Gruppe erforderlich.", + "====== PBS UPDATE COMPLETED ======": "====== PBS-UPDATE ABGESCHLOSSEN ======", "====== PVE UPDATE COMPLETED ======": "====== PVE-UPDATE ABGESCHLOSSEN ======", ">> Create a new backup job <<": ">> Einen neuen Sicherungsauftrag erstellen <<", ">> Save and return <<": ">> Speichern und zurück <<", @@ -87,6 +88,7 @@ "A multi-container application is not recreated: its containers are updated together.": "Eine Multi-Container-Anwendung wird nicht neu erstellt: Ihre Container werden zusammen aktualisiert.", "A new ProxMenux version is available:": "Eine neue ProxMenux-Version ist verfügbar:", "A new kernel is staged for the next boot:": "Ein neuer Kernel wird für den nächsten Start bereitgestellt:", + "A newer kernel is installed. Reboot before the next backup window to use it.": "Ein neuerer Kernel ist installiert. Starten Sie vor dem nächsten Backup-Fenster einen Neustart, um es zu verwenden.", "A newer version is available:": "Eine neuere Version ist verfügbar:", "A pending operation exists for": "Eine ausstehende operation existiert für", "A pending stack assembly already exists; it is not overwritten": "Eine ausstehende Stapelanordnung existiert bereits; sie wird nicht überschrieben", @@ -169,6 +171,7 @@ "APT language configuration updated": "APT-Sprachkonfiguration aktualisiert", "APT language downloads restored": "APT-Sprachdownloads wiederhergestellt", "APT package lists updated": "APT-Paketlisten aktualisiert", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "APT-Aktualisierung fehlgeschlagen. Überprüfen Sie das PBS-Abonnement und die Repositorys unter Administration > Repositorys. Protokoll:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "APT-Aktualisierung fehlgeschlagen. Prüfen Sie das PBS-Abonnement und die Repositories in der nativen PBS-Oberfläche; es wurden keine Paketquellen geändert.", "APT update failed. Check repository access; NVIDIA installation stopped.": "APT-Update fehlgeschlagen. Überprüfen Sie den Repository-Zugriff. Die Installation von NVIDIA wurde gestoppt.", "Aborted": "Abgebrochen", @@ -443,10 +446,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "Die geprüfte Paketaktualisierung jetzt anwenden? Dienste können neu starten und zur Entfernung aufgeführte Pakete werden entfernt.", "Apply these changes?": "Diese Änderungen anwenden?", "Applying AMD-specific fixes...": "Anwenden von AMD-spezifischen Korrekturen...", + "Applying CPU and RAM changes without rebuilding the container...": "CPU- und RAM-Änderungen werden ohne Neuerstellung des Containers angewendet...", "Applying Changes": "Anwenden von Änderungen", "Applying Controller/NVMe passthrough to VM": "Anwenden von Controller/NVMe-Passthrough auf die VM", "Applying balanced memory optimization settings...": "Ausgewogene Einstellungen zur Speicheroptimierung werden angewendet...", "Applying changes safely...": "Änderungen sicher anwenden...", + "Applying container settings without rebuilding the container...": "Containereinstellungen werden ohne Neuerstellung des Containers angewendet...", "Applying custom restore": "Benutzerdefinierte Wiederherstellung anwenden", "Applying default VM configuration": "Anwenden der Standard-VM-Konfiguration", "Applying host permissions for unprivileged LXC bind-mounts...": "Anwenden von Hostberechtigungen für nicht privilegierte LXC-Bind-Mounts ...", @@ -538,6 +543,7 @@ "Available Borg targets:": "Verfügbare Borg-Ziele:", "Available Disks on Host": "Verfügbare Festplatten auf dem Host", "Available ISO Images": "Verfügbare ISO-Images", + "Available PBS Version": "Verfügbare PBS-Version", "Available PBS repositories:": "Verfügbare PBS-Repositories:", "Available PVE Version": "Verfügbare PVE-Version", "Available Shares:": "Verfügbare Aktien:", @@ -749,6 +755,7 @@ "Cannot verify the reused disk:": "Kann die wiederverwendete Festplatte nicht überprüfen:", "Cannot write the image archive to {storage}. Check storage access and mount status.": "Das Bildarchiv kann nicht in {storage} geschrieben werden. Überprüfen Sie den Speicherzugriff und den Bereitstellungsstatus.", "Capabilities cannot be kept and all dropped at the same time": "Fähigkeiten können nicht gehalten werden und alle gleichzeitig fallen gelassen werden", + "Catalog options": "Katalogoptionen", "Category": "Kategorie", "Caution: Maximum mode generates more heat.": "Achtung: Im Maximalmodus entsteht mehr Wärme.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Ceph-Prüfung durch Benutzerflag übersprungen (--ignore-ceph-check)", @@ -883,6 +890,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Wählen Sie einen Zeitplan (systemd-Kalender-Ereignis-Syntax):", "Choose a script to execute:": "Wählen Sie ein auszuführendes Skript aus:", "Choose a server:": "Wählen Sie einen Server:", + "Choose a setting. Changes are applied only after the final review.": "Wähle eine Einstellung. Änderungen werden erst nach der abschließenden Prüfung angewendet.", "Choose a share to mount:": "Wählen Sie eine Freigabe zum Mounten aus:", "Choose a specific supported GPU or USB node": "Wählen Sie einen bestimmten unterstützten GPU- oder USB-Knoten aus", "Choose action for this controller/NVMe:": "Wählen Sie eine Aktion für diesen Controller/NVMe:", @@ -1520,6 +1528,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "Aktuelles NVIDIA-Inventar behoben: ein Refresh ist required", "Current NVIDIA inventory resolved: no refresh is required": "Aktuelles NVIDIA-Inventar behoben: kein Refresh ist required", "Current Network Configuration": "Aktuelle Netzwerkkonfiguration", + "Current PBS Version": "Aktuelle PBS-Version", "Current PVE Version": "Aktuelle PVE-Version", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Aktuelle ProxMenux-Hostskripte registrieren Remote-Freigaben mithilfe von pvesm als Proxmox-Speicher.", "Current ProxMenux version:": "Aktuelle ProxMenux-Version:", @@ -1634,8 +1643,8 @@ "Default Proxmox logo applied": "Standardmäßiges Proxmox-Logo angewendet", "Default is none (video-only passthrough). Use SPACE to toggle selections.": "Der Standardwert ist „Keine“ (nur Video-Passthrough). Verwenden Sie die LEERTASTE, um die Auswahl umzuschalten.", "Default journald configuration restored": "Standard-Journald-Konfiguration wiederhergestellt", - "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "Der Standardspeicherort ist /mnt/. Die Freigabe wird hier auf dem Host mit offenen Berechtigungen gemountet, sodass ein unprivilegierter LXC sie binden, mounten und darauf schreiben kann. Für den LXC-Zugriff müssen Sie diesen Pfad mit dem LXC Mount Manager binden.", - "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "Der Standardspeicherort ist /mnt/. Die Freigabe wird hier auf dem Host gemountet. Verwenden Sie diesen Pfad in /etc/fstab. Für den LXC-Zugriff müssen Sie diesen Pfad mit dem LXC Mount Manager binden.", + "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "Der Standardspeicherort ist /mnt/. Die Freigabe wird hier auf dem Host mit offenen Berechtigungen eingehängt, sodass ein unprivilegierter LXC sie per Bind-Mount einbinden und darauf schreiben kann. Verwenden Sie dafür den LXC Mount Manager.", + "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "Der Standardspeicherort ist /mnt/. Die Freigabe wird hier auf dem Host eingehängt. Verwenden Sie diesen Pfad in /etc/fstab. Für den Zugriff aus einem LXC binden Sie ihn mit dem LXC Mount Manager als Bind-Mount ein.", "Default login": "Standard-Login", "Default options": "Standardoptionen", "Default options read/write": "Standardoptionen Lesen/Schreiben", @@ -1685,6 +1694,7 @@ "Detected Coral hardware:": "Erkannte Coral-Hardware:", "Detected GPU(s):": "Erkannte GPU(s):", "Detected Intel CPU": "Intel-CPU erkannt", + "Detected Proxmox Backup Server": "Proxmox Backup Server erkannt", "Detected Proxmox VE": "Proxmox VE erkannt", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Proxmox VE 8.x erkannt – Ceph Squid für Debian installieren", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Proxmox VE 9.x erkannt – Ceph Squid für Debian Trixie installieren", @@ -1766,6 +1776,9 @@ "Disabling and stopping rpcbind service and socket...": "Rpcbind-Dienst und Socket deaktivieren und stoppen ...", "Disabling non-free firmware warnings...": "Warnungen zu nicht kostenloser Firmware werden deaktiviert...", "Disabling portmapper/rpcbind for security...": "Portmapper/rpcbind wird aus Sicherheitsgründen deaktiviert...", + "Discard changes": "Änderungen verwerfen", + "Discard unapplied changes?": "Noch nicht angewendete Änderungen verwerfen?", + "Discover NFS exports on a server": "NFS-Exporte eines Servers ermitteln", "Discover targets on portal:": "Entdecken Sie Ziele auf dem Portal:", "Discovering iSCSI targets...": "iSCSI-Ziele werden ermittelt...", "Discovery successful": "Entdeckung erfolgreich", @@ -1845,6 +1858,7 @@ "Do you want to proceed with the system update?": "Möchten Sie mit der Systemaktualisierung fortfahren?", "Do you want to proceed?": "Möchten Sie fortfahren?", "Do you want to reboot now?": "Möchten Sie jetzt neu starten?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "Möchten Sie das Proxmox-Abonnementbanner von der Proxmox Backup Server-Weboberfläche entfernen?", "Do you want to remove the stale entry from vfio.conf and continue?": "Möchten Sie den veralteten Eintrag aus vfio.conf entfernen und fortfahren?", "Do you want to remove the stale entry from vfio.conf?": "Möchten Sie den veralteten Eintrag aus vfio.conf entfernen?", "Do you want to restart the network service now to apply changes?": "Möchten Sie den Netzwerkdienst jetzt neu starten, um die Änderungen zu übernehmen?", @@ -2022,7 +2036,7 @@ "Enter VM ID:": "Geben Sie die VM-ID ein:", "Enter VM/CT ID:": "Geben Sie die VM/CT-ID ein:", "Enter ZFS pool name for the selected disk:": "Geben Sie den ZFS-Poolnamen für die ausgewählte Festplatte ein:", - "Enter a name for the mount point (used as /mnt/):": "Geben Sie einen Namen für den Mount-Punkt ein (verwendet als /mnt/):", + "Enter a name for the mount point (used as /mnt/):": "Geben Sie einen Namen für den Mountpoint ein (wird als /mnt/ verwendet):", "Enter a name for the new virtual machine:": "Geben Sie einen Namen für die neue virtuelle Maschine ein:", "Enter a non-empty password or access token on a single line.": "Geben Sie in einer einzelnen Zeile ein nicht leeres Passwort oder Zugriffstoken ein.", "Enter a number, or write or paste a command:": "Geben Sie eine Zahl ein oder schreiben oder fügen Sie einen Befehl ein:", @@ -2170,7 +2184,7 @@ "Example: pct stop 110": "Beispiel: pct stop 110", "Example: pct stop 114": "Beispiel: pct stop 114", "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup": "Beispiel: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup", - "Example: pvesm path local-lvm:vm-114-disk-0": "Beispiel: pvesm-Pfad local-lvm:vm-114-disk-0", + "Example: pvesm path local-lvm:vm-114-disk-0": "Beispiel: pvesm path local-lvm:vm-114-disk-0", "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf": "Beispiel: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf", "Examples:": "Beispiele:", "Execute destructive rollback?": "Destruktives Rollback ausführen?", @@ -2270,6 +2284,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "Das Klonen des log2ram-Repositorys ist fehlgeschlagen. Überprüfen Sie /tmp/log2ram_install.log", "Failed to configure EFI disk": "Die EFI-Festplatte konnte nicht konfiguriert werden", "Failed to configure IOMMU automatically.": "IOMMU konnte nicht automatisch konfiguriert werden.", + "Failed to configure Proxmox Backup Server repositories": "Proxmox Backup Server-Repositorys konnten nicht konfiguriert werden", "Failed to configure Proxmox repositories": "Proxmox-Repositorys konnten nicht konfiguriert werden", "Failed to configure TPM device in VM": "Das TPM-Gerät konnte in der VM nicht konfiguriert werden", "Failed to configure repositories.": "Die Repositorys konnten nicht konfiguriert werden.", @@ -2481,7 +2496,7 @@ "For a consistent export, the VM should be stopped.": "Für einen konsistenten Export sollte die VM gestoppt werden.", "For any question, issue or bug report, please use the official repository": "Für Fragen, Probleme oder Fehlerberichte nutzen Sie bitte das offizielle Repository", "For dedicated GPUs, FLR is required by this policy to reduce VM": "Für dedizierte GPUs ist gemäß dieser Richtlinie FLR erforderlich, um VM zu reduzieren", - "For example: pct enter 101": "Beispiel: pct Geben Sie 101 ein", + "For example: pct enter 101": "Zum Beispiel: pct enter 101", "For guest shares add: --options guest": "Für Gastfreigaben hinzufügen: --options Guest", "For security reasons, the system cannot format it.": "Aus Sicherheitsgründen kann das System es nicht formatieren.", "For unprivileged containers, use instead:": "Verwenden Sie für nichtprivilegierte Container stattdessen:", @@ -2893,8 +2908,8 @@ "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.": "Wenn Benachrichtigungen aktiviert sind (Telegram/Discord/ntfy/...), erhalten Sie die Meldung „Host-Wiederherstellung abgeschlossen“, wenn alle Hintergrundaufgaben abgeschlossen sind.", "If passthrough fails on Windows: install RadeonResetBugFix.": "Wenn Passthrough unter Windows fehlschlägt: Installieren Sie RadeonResetBugFix.", "If path not accessible (ZFS/BTRFS):": "Wenn der Pfad nicht zugänglich ist (ZFS/BTRFS):", - "If pvesm path returned a DEVICE (LVM):": "Wenn der pvesm-Pfad ein GERÄT (LVM) zurückgegeben hat:", - "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Wenn der pvesm-Pfad ein VERZEICHNIS (ZFS/BTRFS) zurückgegeben hat:", + "If pvesm path returned a DEVICE (LVM):": "Wenn pvesm path ein GERÄT (LVM) zurückgegeben hat:", + "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Wenn pvesm path ein VERZEICHNIS (ZFS/BTRFS) zurückgegeben hat:", "If repositories error:": "Bei Repository-Fehler:", "If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Wenn der Container bereits geändert wurde, wird sein vorheriges natives Backup wiederhergestellt. Freigegebene Hostverzeichnisse werden nicht zurückgesetzt. Die Festplatten des fehlgeschlagenen Versuchs werden entfernt, wenn die Bereinigung erfolgreich ist.", "If the host freezes, remove hostpci entries from": "Wenn der Host einfriert, entfernen Sie die Hostpci-Einträge aus", @@ -2977,6 +2992,7 @@ "Inaccessible device during VM startup": "Während des VM-Starts ist kein Zugriff auf das Gerät möglich", "Inactive": "Inaktiv", "Inactive (entry in fstab, not currently mounted)": "Inaktiv (Eintrag in fstab, derzeit nicht gemountet)", + "Include these changes in oci-compose?": "Diese Änderungen in oci-compose einschließen?", "Include these resources in oci-compose?": "Diese Ressourcen in oci-compose einschließen?", "Includes /etc/network (may drop SSH immediately)": "Enthält /etc/network (kann SSH sofort löschen)", "Includes /etc/zfs": "Enthält /etc/zfs", @@ -3210,9 +3226,11 @@ "Interrupted operation:": "Unterbrochene operation:", "Interrupted stack operation found": "Unterbrochener Stapel operation gefunden", "Invalid 'proxmox-ve' candidate (not 9.x or none). Please verify your repository configuration and network, then retry.": "Ungültiger „proxmox-ve“-Kandidat (nicht 9.x oder keiner). Bitte überprüfen Sie Ihre Repository-Konfiguration und Ihr Netzwerk und versuchen Sie es dann erneut.", - "Invalid ALLOWED_HOSTS value": "Wert für ungültig ALLOWED HOSTs", + "Invalid ALLOWED_HOSTS value": "Ungültiger Wert für ALLOWED_HOSTS", "Invalid Home Assistant OS check timeout": "Invalid Home Assistant OS Check Timeout", "Invalid ID": "Ungültige ID", + "Invalid IPv4 address": "Ungültige IPv4-Adresse", + "Invalid IPv4 gateway": "Ungültiges IPv4-Gateway", "Invalid Intel render path": "Ungültiger Intel-Renderpfad", "Invalid Jellyfin path:": "Ungültige Jellyfin-Strecke:", "Invalid MAC address:": "Ungültige MAC-Adresse:", @@ -3245,6 +3263,7 @@ "Invalid administrator email": "E-Mail des ungültigen Administrators", "Invalid administrator user name": "Ungültiger Administrator Benutzername", "Invalid base VMID": "Ungültige Basis VMID", + "Invalid bridge": "Ungültige Bridge", "Invalid check package:": "Ungültiges Kontrollpaket:", "Invalid configuration path:": "Ungültiger Konfigurationspfad:", "Invalid consume/export volumes": "Ungültige Verbrauchs-/Ausfuhrmengen", @@ -3308,6 +3327,7 @@ "Invalid remote name": "Name der ungültigen Fernbedienung", "Invalid remote path": "Ungültiger Fernpfad", "Invalid repair version:": "Ungültige Reparaturversion:", + "Invalid resource value": "Ungültiger Ressourcenwert", "Invalid resources": "Ungültige Mittel", "Invalid restored volume path": "Ungültiger wiederhergestellter Volumenpfad", "Invalid running state": "Invaliditätsstatus", @@ -3326,6 +3346,7 @@ "Invalid stack members": "Ungültige Stapelmitglieder", "Invalid stack name": "Name des ungültigen Stacks", "Invalid stack operation": "Ungültiger Stapel operation", + "Invalid storage": "Ungültiger Speicher", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "Ungültige Speicher-ID. Verwenden Sie nur Buchstaben, Zahlen, Bindestriche und Unterstriche.", "Invalid storage name": "Ungültiger Speichername", "Invalid suite application": "Anwendung ungültiger Suiten", @@ -3440,7 +3461,7 @@ "Keyfile removed.": "Schlüsseldatei entfernt.", "Keyrings method failed; trying apt-key fallback": "Keyrings-Methode fehlgeschlagen; Ich versuche einen Apt-Key-Fallback", "KiCad - A Cross Platform and Open Source Electronics Design Automation Suite.": "KiCad - Eine Cross-Plattform und Open Source Electronics Design Automation Suite.", - "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai hat kein Standardkonto. Geben Sie den Container ein mit: pct enter {main vmid}", + "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai hat kein Standardkonto. Öffnen Sie eine Shell im Container mit: pct enter {main_vmid}", "Kimai is a professional grade time-tracking application, free and open-source.": "Kimai ist eine professionelle Time-Tracking-Anwendung, kostenlos und Open-Source.", "Kometa is a powerful tool designed to give you complete control over your media libraries. With Kometa, you can take your customization to the next level, with granular control over metadata, collections, overlays, and much more.": "Kometa ist ein leistungsstarkes Tool, das Ihnen die vollständige Kontrolle über Ihre Medienbibliotheken gibt. Mit Kometa können Sie Ihre Anpassung auf die nächste Ebene bringen, mit granularer Kontrolle über Metadaten, Sammlungen, Overlays und vielem mehr.", "Kometa reads its configuration from /config/config.yml and the container only ships /config/config.yml.template. Copy the template to config.yml, fill in the required Plex and TMDb connections, then restart the container.": "Kometa liest seine Konfiguration aus /config/config.yml und der Container liefert nur /config/config.yml.template. Kopieren Sie die Vorlage auf config.yml, füllen Sie die required Plex und TMDb-Verbindungen aus und starten Sie dann den Container neu.", @@ -3511,7 +3532,7 @@ "Limiting size and optimizing journald": "Begrenzung der Größe und Optimierung des Journals", "Limiting size and optimizing journald...": "Größe begrenzen und Journal optimieren...", "Limnoria A robust, full-featured, and user/programmer-friendly Python IRC bot, with many existing plugins. Successor of the well-known Supybot.": "Limnoria Ein robuster, voll ausgestatteter und benutzerfreundlicher Python IRC-Bot mit vielen vorhandenen Plugins. Nachfolger des bekannten Supybot.", - "Limnoria joins no IRC network until its configuration file exists. Create it with the setup wizard from the Proxmox host: pct exec -- bash -c 'cd /config && limnoria-wizard'": "Limnoria schließt sich keinem IRC-Netzwerk an, bis seine Konfigurationsdatei existiert. Erstellen Sie es mit dem Setup-Assistenten aus dem Proxmox-Host: pct exec - bash -c 'cd /config & & limnoria-Wizard'", + "Limnoria joins no IRC network until its configuration file exists. Create it with the setup wizard from the Proxmox host: pct exec -- bash -c 'cd /config && limnoria-wizard'": "Limnoria verbindet sich erst mit einem IRC-Netzwerk, wenn die Konfigurationsdatei existiert. Erstellen Sie sie mit dem Einrichtungsassistenten auf dem Proxmox-Host: pct exec -- bash -c 'cd /config && limnoria-wizard'", "Line to paste (single line, including \"command=...\" prefix):": "Einzufügende Zeile (einzelne Zeile, einschließlich „command=…“-Präfix):", "Linux Installation Options": "Linux-Installationsoptionen", "Linux/Mac path:": "Linux/Mac-Pfad:", @@ -4157,6 +4178,7 @@ "No active Proxmox storage accepts OCI template archives.": "Kein aktiver Proxmox-Speicher akzeptiert OCI-Vorlagenarchive.", "No active exports or showmount not available": "Keine aktiven Exporte oder Showmount nicht verfügbar", "No active session": "Keine aktive Sitzung", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "Kein aktives Abonnement und kein verwendbares PBS-Repository. Im nicht interaktiven Modus können APT-Quellen nicht geändert werden. Konfigurieren Sie sie unter Administration > Repositorys.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "Kein aktives Abonnement und kein nutzbares PVE-Repository. Im nicht interaktiven Modus können APT-Quellen nicht geändert werden. Konfigurieren Sie sie unter Knoten > Updates > Repositorys.", "No additional GPU can be added.": "Es kann keine zusätzliche GPU hinzugefügt werden.", "No additional device needs to be added.": "Es muss kein zusätzliches Gerät hinzugefügt werden.", @@ -4242,7 +4264,7 @@ "No jobs": "Keine Jobs", "No language selected.": "Keine Sprache ausgewählt.", "No local disk storage configured.": "Kein lokaler Festplattenspeicher konfiguriert.", - "No main sources.list present (skipped)": "Keine Hauptquellenliste vorhanden (übersprungen)", + "No main sources.list present (skipped)": "Keine zentrale sources.list vorhanden (übersprungen)", "No matches for:": "Keine Treffer für:", "No matching PVE vzdump jobs were found for the PBS backend.": "Für das PBS-Backend wurden keine passenden PVE-vzdump-Jobs gefunden.", "No matching PVE vzdump jobs were found for the local archive backend.": "Für das lokale Archiv-Backend wurden keine passenden PVE-vzdump-Jobs gefunden.", @@ -4261,6 +4283,7 @@ "No password provided.": "Kein Passwort angegeben.", "No paths available": "Keine Pfade verfügbar", "No paths selected. Select at least one path.": "Keine Pfade ausgewählt. Wählen Sie mindestens einen Pfad aus.", + "No pending changes": "Keine ausstehenden Änderungen", "No pending paths to schedule for reboot.": "Keine ausstehenden Pfade zum Planen eines Neustarts.", "No permanent NFS mounts configured.": "Keine permanenten NFS-Mounts konfiguriert.", "No permanent Samba mounts configured.": "Keine permanenten Samba-Mounts konfiguriert.", @@ -4402,6 +4425,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM entfernt (Marionettenartefakte müssen möglicherweise manuell bereinigt werden)", "OVH server detected": "OVH-Server erkannt", "OVH server detection and RTM installation process completed": "OVH-Servererkennung und RTM-Installationsprozess abgeschlossen", + "Object detector": "Objekterkennung", "Object detector for Frigate": "Objektdetektor für Frigate", "Observer is not responding on port 4357": "Beobachter reagiert nicht auf Port 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian ist eine Notiz-App, mit der Sie Ihre Notizen auf Ihrem Gerät erstellen, verknüpfen und organisieren können, mit Hunderten von Plugins und Themes, um Ihren Workflow anzupassen. Sie können Ihre Notizen auch online veröffentlichen, offline darauf zugreifen und sie sicher mit einer End-to-End-Verschlüsselung synchronisieren.", @@ -4874,12 +4898,17 @@ "Proxmology logo applied": "Proxmology-Logo angewendet", "Proxmox 9 system update allready": "Proxmox 9 Systemupdate bereits", "Proxmox APT repositories configured": "Proxmox APT-Repositorys konfiguriert", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Proxmox Backup Server (PBS)-Ziele", + "Proxmox Backup Server APT repositories configured": "Proxmox Backup Server APT-Repositorys konfiguriert", + "Proxmox Backup Server Update": "Aktualisierung von Proxmox Backup Server", + "Proxmox Backup Server update completed.": "Aktualisierung von Proxmox Backup Server abgeschlossen.", "Proxmox CIFS Storage Status:": "Proxmox CIFS-Speicherstatus:", "Proxmox NFS Storage Status:": "Proxmox NFS-Speicherstatus:", "Proxmox System Update": "Proxmox-Systemupdate", "Proxmox Update": "Proxmox-Update", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "Die Paketaktualisierung von Proxmox VE 8 ist fehlgeschlagen. Prüfen Sie die APT-Ausgabe, bevor Sie es erneut versuchen.", "Proxmox VE 8 system update completed successfully": "Proxmox VE 8 Systemupdate erfolgreich abgeschlossen", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Manuelles Upgrade-Handbuch für Proxmox VE 8 auf 9", "Proxmox VE 9.x no-subscription repository created": "Proxmox VE 9.x No-Subscription-Repository erstellt", @@ -4887,6 +4916,7 @@ "Proxmox VE configuration completed.": "Proxmox VE-Konfiguration abgeschlossen.", "Proxmox VE safe update completed": "Sicheres Proxmox VE-Update abgeschlossen", "Proxmox auth logger service created and started": "Proxmox-Auth-Logger-Dienst erstellt und gestartet", + "Proxmox could not apply the container settings": "Proxmox konnte die Containereinstellungen nicht anwenden", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Das Proxmox-Enterprise-Repository fehlt möglicherweise oder ist nicht zugänglich. Ich versuche, auf kein Abonnement umzustellen...", "Proxmox filter configured": "Proxmox-Filter konfiguriert", "Proxmox iSCSI Storage Status:": "Proxmox iSCSI-Speicherstatus:", @@ -5153,6 +5183,8 @@ "Remove saved credentials": "Gespeicherte Anmeldeinformationen entfernen", "Remove subscription banner": "Abonnementbanner entfernen", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Anwendung entfernen? Seine Containerfestplatten werden gelöscht und können nur durch ein Backup wiederhergestellt werden.", + "Remove the lock left on a VM by an interrupted task": "Die Sperre entfernen, die eine unterbrochene Aufgabe auf einer VM hinterlassen hat", + "Remove the lock left on a container by an interrupted task": "Die Sperre entfernen, die eine unterbrochene Aufgabe auf einem Container hinterlassen hat", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Die gespeicherten Anmeldeinformationen für {registry} entfernen?\nLaufende Apps funktionieren weiterhin.", "Remove the unprivileged flag from configuration:": "Entfernen Sie das Unprivileged-Flag aus der Konfiguration:", "Remove unused packages and their config": "Entfernen Sie nicht verwendete Pakete und deren Konfiguration", @@ -5234,6 +5266,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "Repository-Helfer nicht verfügbar. Die Installation von NVIDIA wurde gestoppt.", "Repository policy returned an unexpected result.": "Die Repository-Richtlinie hat ein unerwartetes Ergebnis zurückgegeben.", "Repository ready.": "Repository bereit.", + "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.": "Repository-Quellen geändert, aber die Aktualisierung der APT-Paketliste ist fehlgeschlagen. Betrieb gestoppt;Quellen wurden nicht zurückgesetzt. Überprüfen Sie „Administration > Repositorys“ und wiederholen Sie apt-get update, bevor Sie fortfahren.", "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.": "Die Repository-Quellen wurden geändert, aber die Aktualisierung der APT-Paketliste ist fehlgeschlagen. Vorgang gestoppt; die Quellen wurden nicht zurückgesetzt. Überprüfen Sie Knoten > Updates > Repositorys und wiederholen Sie apt-get update, bevor Sie fortfahren.", "Repository switch declined; no APT source changed.": "Repository-Wechsel abgelehnt; es wurde keine APT-Quelle geändert.", "Repository:": "Repository:", @@ -5328,6 +5361,7 @@ "Reverting ZFS ARC tuning...": "ZFS ARC-Tuning wird zurückgesetzt...", "Reverting pigz wrapper...": "Pigz-Wrapper wird zurückgesetzt...", "Reverting vzdump speed tuning...": "vzdump-Geschwindigkeitsoptimierung wird zurückgesetzt...", + "Review and apply": "Prüfen und anwenden", "Review external OCI changes": "Überprüfen Sie externe OCI-Änderungen", "Review package upgrade": "Paketaktualisierung prüfen", "Review passthrough config files": "Überprüfen Sie die Passthrough-Konfigurationsdateien", @@ -5787,6 +5821,7 @@ "ShareCoveX panel": "ShareCoveX-Panel", "Shared Directory Ready:": "Bereit für das freigegebene Verzeichnis:", "Shared Group": "Geteilte Gruppe", + "Shared directories": "Freigegebene Verzeichnisse", "Shared directory created:": "Gemeinsames Verzeichnis erstellt:", "Shared directory for consume and export": "Gemeinsames Verzeichnis für Konsum und Export", "Shared directory for copy/sync operations": "Gemeinsames Verzeichnis für Copy/Sync operations", @@ -5816,6 +5851,7 @@ "Should show pve-manager/9.x.x": "Sollte pve-manager/9.x.x anzeigen", "Shoutrrr addresses for the notifications (optional)": "Shoutrrr-Adressen für die Benachrichtigungen (optional)", "Show All Scripts": "Alle Skripte anzeigen", + "Show CT users and their UID/GID on the host": "CT-Benutzer und UID/GID auf dem Host anzeigen", "Show CT users in table format": "CT-Benutzer im Tabellenformat anzeigen", "Show Container Privilege Status": "Container-Berechtigungsstatus anzeigen", "Show DNS resolution status": "DNS-Auflösungsstatus anzeigen", @@ -5977,6 +6013,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Starten Sie die VM, um die Windows-Installation von der gemounteten ISO aus zu starten.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Die Anwendungen starten, sobald sie registriert sind? Antworten Sie mit „Nein“, wenn die ursprünglichen Container noch auf einem anderen Host laufen: Beide würden die gleichen Adressen verwenden.", "Start the converted container:": "Starten Sie den konvertierten Container:", + "Start the interactive Proxmox package upgrade": "Interaktive Paketaktualisierung von Proxmox starten", "Start the main system upgrade:": "Starten Sie das Hauptsystem-Upgrade:", "Start the stack with Proxmox": "Starten Sie den Stack mit Proxmox", "Start uploading to PBS — sets a recovery passphrase": "Hochladen auf PBS starten – legt eine Wiederherstellungspassphrase fest", @@ -6256,6 +6293,7 @@ "The Paperless persistent volumes need at least 8 GB": "Die papierlosen persistenten Volumes benötigen mindestens 8 GB", "The PostgreSQL volume needs at least 4 GB": "Das PostgreSQL-Volume benötigt mindestens 4 GB", "The PostgreSQL volume needs at least 8 GB": "Das PostgreSQL-Volume benötigt mindestens 8 GB", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "Die Aktualisierung von Proxmox VE 8 wurde nicht abgeschlossen. Das Upgrade auf Proxmox VE 9 wurde nicht gestartet.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "Der Archivschlüsselbund Proxmox fehlt;Die Ceph-Installation kann nicht sicher fortgesetzt werden", "The Proxmox inventory and the local configurations differ": "Das Proxmox-Inventar und die lokalen Konfigurationen unterscheiden sich", "The ROCm image has no support for the AMD GPU of this host:": "Das ROCm-Image unterstützt die AMD-GPU dieses Hosts nicht:", @@ -6326,6 +6364,7 @@ "The build could not be checked beforehand; continuing without that check.": "Der Build konnte vorher nicht überprüft werden;ohne diese Prüfung weitermachen.", "The cached image does not match the current digest": "Das zwischengespeicherte Bild entspricht nicht dem aktuellen Digest", "The cached image is damaged; it will be downloaded again.": "Das zwischengespeicherte Bild ist beschädigt; es wird erneut heruntergeladen.", + "The changes need the application installer": "Die Änderungen erfordern das Installationsprogramm der Anwendung", "The character device does not exist:": "Das Zeichengerät existiert nicht:", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Der Befehl fragt nach einem Passwort, das nicht wiederholt wird. Nach dem Erstellen der Benutzer fragt die Weboberfläche nach einem Benutzernamen und einem Passwort.", "The command does not name an image": "Der Befehl benennt kein Bild", @@ -6445,8 +6484,10 @@ "The host bind source does not exist:": "Die Host-Bind-Quelle existiert nicht:", "The host bind source is not a regular file or directory:": "Die Host-Bind-Quelle ist keine reguläre Datei oder ein Verzeichnis:", "The host directory changed before it was mounted": "Das Host-Verzeichnis wurde geändert, bevor es gemountet wurde", + "The host directory does not exist": "Das Verzeichnis auf dem Host ist nicht vorhanden", "The host directory may not be accessible from an unprivileged container.": "Auf das Hostverzeichnis kann von einem unprivilegierten Container aus möglicherweise nicht zugegriffen werden.", "The host has no IPv4 address on the selected bridge": "Der Host hat keine IPv4-Adresse auf der ausgewählten Brücke", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "Der Host verliert den Zugriff darauf. Wenn die Gruppe einen Netzwerk- oder Speichercontroller enthält, den der Host benötigt, brechen Sie in der Zusammenfassung ab.", "The host monitor does not see the real host memory": "Der Host-Monitor sieht den echten Host-Speicher nicht", "The host monitor does not share this host namespace:": "Der Host-Monitor teilt diesen Host-Namespace nicht:", "The host monitor include is unknown or differs from the saved record": "Das Host-Monitor-Include ist unbekannt oder weicht vom gespeicherten Datensatz ab", @@ -6636,6 +6677,7 @@ "The repair must preserve the image dependencies:": "Die Reparatur muss die Bildabhängigkeiten bewahren:", "The requested VMID block is already in use": "Der angeforderte VMID-Block wird bereits verwendet", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "Das angeforderte Machine Learning-GPU-Profil funktioniert nicht; es wird nicht durch CPU ersetzt", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "Die Ressourcenänderung konnte nicht vollständig rückgängig gemacht werden. Öffne die Verwaltung erneut, um die aktuellen Proxmox-Einstellungen zu prüfen und zu übernehmen.", "The restored service did not pass its health check": "Der wiederhergestellte Service bestand seinen Gesundheitscheck nicht", "The restored service stopped; the recovery is not confirmed": "Der wiederhergestellte Dienst wurde gestoppt; die Wiederherstellung wird nicht bestätigt", "The reviewed Tandoor stack does not require a privileged LXC.": "Der überprüfte Tandoor-Stack requi ist kein privilegierter LXC.", @@ -6681,6 +6723,7 @@ "The selected VM": "Die ausgewählte VM", "The selected VM is running.": "Die ausgewählte VM läuft.", "The selected base folder does not exist and could not be created:": "Der ausgewählte Basisordner existiert nicht und konnte nicht erstellt werden:", + "The selected bridge does not exist": "Die ausgewählte Bridge ist nicht vorhanden", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "Die ausgewählte Bridge verfügt über kein IPv4-Subnetz für die Host-Monitor-Firewall", "The selected configuration needs to start the LXC during the installation": "Die ausgewählte Konfiguration muss den LXC während der Installation starten", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Der ausgewählte Container ist nicht privilegiert. Für den direkten Geräte-Passthrough ist ein privilegierter Container erforderlich.", @@ -6703,6 +6746,7 @@ "The service builds its own image; only a published image can be installed": "Der Dienst erstellt ein eigenes Image; nur ein veröffentlichtes Image kann installiert werden", "The service declares no image:": "Der Dienst erklärt kein Bild:", "The setting has no final value:": "Die Einstellung hat keinen Endwert:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Die Einstellungen konnten nicht vollständig zurückgesetzt werden. Prüfe den Container und die gespeicherte Änderung, bevor du fortfährst.", "The share already exists in smb.conf:": "Die Freigabe existiert bereits in smb.conf:", "The shared destination is not a directory": "Das gemeinsame Ziel ist kein Verzeichnis", "The shared directory points to a protected host path": "Das Shared Directory zeigt auf einen geschützten Hostpfad", @@ -6770,19 +6814,24 @@ "These Proxmox resources were added outside ProxMenux:": "Diese Proxmox-Ressourcen wurden außerhalb von ProxMenux hinzugefügt:", "These VMIDs are not free:": "Diese VMIDs sind nicht kostenlos:", "These are the changes that will be made": "Dies sind die Änderungen, die vorgenommen werden", + "These changes were made in Proxmox outside ProxMenux:": "Diese Änderungen wurden in Proxmox außerhalb von ProxMenux vorgenommen:", "These container mount paths have an extension-like suffix:": "Diese Einhängepfade im Container haben eine Endung, die einer Dateierweiterung ähnelt:", "These containers were restored from a backup and this host has no record of their OCI application:": "Diese Container wurden aus einem Backup wiederhergestellt und dieser Host hat keinen Datensatz ihrer OCI-Anwendung:", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Diese Geräte teilen sich die IOMMU-Gruppe der ausgewählten GPU und werden zusammen mit ihr der VM zugewiesen:", "These host backups run with this job and expect its storage:": "Diese Host-Backups werden mit diesem Job ausgeführt und erwarten dessen Speicherung:", "These host backups run with this job and will stop running:": "Diese Host-Backups werden mit diesem Job ausgeführt und werden nicht mehr ausgeführt:", "These interface configurations will be removed": "Diese Schnittstellenkonfigurationen werden entfernt", "These manual changes cannot be adopted safely:": "Diese manuellen Änderungen können nicht sicher übernommen werden:", "These paths will not be restored live and will be extracted for manual recovery.": "Diese Pfade werden nicht live wiederhergestellt und zur manuellen Wiederherstellung extrahiert.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Diese Einstellungen erfordern eine anwendungsspezifische Anpassung. Das Image wurde nicht heruntergeladen und der Container nicht neu erstellt.", "These values are asked during the installation:": "Diese Werte werden während der Installation abgefragt:", "They are described in the image documentation:": "Sie sind in der Dokumentation des Images beschrieben:", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Sie werden zu oci-compose hinzugefügt, bevor Sie fortfahren. Nicht unterstützte oder geänderte Ressourcen werden nicht importiert.", + "They will be saved in oci-compose before continuing.": "Sie werden in oci-compose gespeichert, bevor Sie fortfahren.", "This CIFS share is mounted with restrictive permissions.": "Diese CIFS-Freigabe wird mit restriktiven Berechtigungen gemountet.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Diese GPU gilt als inkompatibel mit GPU-Passthrough zu einer VM in ProxMenux.", "This NFS share is fully restricted — even the host root cannot write to it.": "Diese NFS-Freigabe ist vollständig eingeschränkt – selbst der Host-Root kann nicht darauf schreiben.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Dieser Proxmox Backup Server hat kein aktives Abonnement. Zum Repository ohne Abonnement wechseln? Die nicht erreichbare Enterprise-PBS-Quelle wird deaktiviert.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Für diese VM sind zusätzliche Installationsschritte erforderlich. Weitere Informationen finden Sie im Installationshandbuch unter:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Dieses Konto ist für {registry}. Geben Sie ein Bild aus dieser Registrierung ein.", "This action will:": "Diese Aktion wird:", @@ -6866,6 +6915,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Dieses Skript kann nicht über das Proxmox-Webterminal ausgeführt werden.", "This script must be run as root.": "Dieses Skript muss als Root ausgeführt werden.", "This script must be run on a Proxmox host.": "Dieses Skript muss auf einem Proxmox-Host ausgeführt werden.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Dieses Skript wendet die folgenden Optimierungen und erweiterten Anpassungen auf Ihren Proxmox Backup Server an", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Dieses Skript wendet die folgenden Optimierungen und erweiterten Anpassungen auf Ihren Proxmox VE-Server an", "This script will update your Proxmox VE system with advanced options:": "Dieses Skript aktualisiert Ihr Proxmox VE-System mit erweiterten Optionen:", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Diese Sitzung wird im Monitor-Terminal ausgeführt. Wenn Sie es von hier aus ausführen, wird die Verbindung während der Installation unterbrochen und der Switch bleibt in einem defekten Zustand.", @@ -6952,7 +7002,7 @@ "Transaction log:": "Transaktionsprotokoll:", "Translation files:": "Übersetzungsdateien:", "Transmission is designed for easy, powerful use. Transmission has the features you want from a BitTorrent client: encryption, a web interface, peer exchange, magnet links, DHT, µTP, UPnP and NAT-PMP port forwarding, webseed support, watch directories, tracker editing, global and per-torrent speed limits, and more.": "Transmission wurde für eine einfache und leistungsstarke Nutzung entwickelt. Transmission verfügt über die Funktionen, die Sie von einem BitTorrent-Client erwarten: Verschlüsselung, eine Web-Schnittstelle, Peer-Exchange, Magnet-Links, DHT, μTP, UPnP und NAT-PMP Port-Weiterleitung, Webseed-Unterstützung, Watch-Verzeichnisse, Tracker-Bearbeitung, globale und per-Torrent-Geschwindigkeitsbeschränkungen und mehr.", - "Tried pvesm path and manual detection methods": "Versuchte den Pvesm-Pfad und manuelle Erkennungsmethoden", + "Tried pvesm path and manual detection methods": "pvesm path und manuelle Erkennungsmethoden wurden versucht", "Trust this certificate and save it for scheduled backups?": "Diesem Zertifikat vertrauen und es für geplante Sicherungen speichern?", "Try Again": "Versuchen Sie es erneut", "Try a different search term.": "Versuchen Sie es mit einem anderen Suchbegriff.", @@ -6997,6 +7047,7 @@ "Unable to identify the root filesystem. Aborting.": "Das Root-Dateisystem konnte nicht identifiziert werden. Abbruch.", "Unable to resolve system ZFS pool disks. Aborting.": "System-Pool-Festplatten ZFS können nicht aufgelöst werden. Abbruch.", "Unable to resolve system disk topology. Aborting.": "Die Topologie der Systemfestplatte kann nicht aufgelöst werden. Abbruch.", + "Unapplied changes": "Noch nicht angewendete Änderungen", "Understand the security implications of privileged containers": "Verstehen Sie die Sicherheitsauswirkungen privilegierter Container", "Unexpected Proxmox inventory; recovery blocked": "Unerwartetes Proxmox-Inventar; Erholung blockiert", "Unexpected formatting directory in the new Valkey volume": "Unerwartetes Formatierungsverzeichnis im neuen Valkey-Volume", @@ -7112,6 +7163,7 @@ "Unsupported mount type": "Typ der nicht unterstützten Halterung", "Unsupported mount type:": "Typ der nicht unterstützten Halterung:", "Unsupported native device type:": "Nicht unterstützter nativer Gerätetyp:", + "Unsupported network settings": "Nicht unterstützte Netzwerkeinstellungen", "Unsupported operation": "Nicht unterstützte operation", "Unsupported or unknown Proxmox version; no repository changed.": "Nicht unterstützte oder unbekannte Proxmox-Version; es wurde kein Repository geändert.", "Unsupported output format:": "Nicht unterstütztes Ausgabeformat:", @@ -7192,6 +7244,7 @@ "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Upgrade-Assistent: Proxmox VE 8 → 9 (Trixie)", "Upgrade canceled by user": "Upgrade vom Benutzer abgebrochen", "Upgrade one node at a time": "Aktualisieren Sie jeweils einen Knoten", + "Upgrade the system, offering the": "Aktualisieren Sie das System und bieten Sie das an", "Upgrade to Proxmox VE 9": "Upgrade auf Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "NVIDIA-Dienstprogramme werden aktualisiert (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "Eine verschlüsselte Kopie des Schlüssels auf PBS hochladen, damit Sie ihn auf einem neu installierten Host mit nur einer Passphrase wiederherstellen können?", @@ -7205,7 +7258,7 @@ "Usage:": "Verwendung:", "Use \"Check test progress\" to see results.": "Verwenden Sie „Testfortschritt prüfen“, um die Ergebnisse anzuzeigen.", "Use 'Export to file' to save it and inspect manually.": "Verwenden Sie „In Datei exportieren“, um es zu speichern und manuell zu überprüfen.", - "Use 'pct restore' / 'qmrestore' to recover their disks from your VM backups.": "Verwenden Sie „pct restart“ / „qmrestore“, um ihre Festplatten aus Ihren VM-Backups wiederherzustellen.", + "Use 'pct restore' / 'qmrestore' to recover their disks from your VM backups.": "Verwenden Sie 'pct restore' / 'qmrestore', um die Datenträger aus den VM-Sicherungen wiederherzustellen.", "Use Custom backup and uncheck the conflicting path from the list": "Verwenden Sie die benutzerdefinierte Sicherung und deaktivieren Sie den in Konflikt stehenden Pfad aus der Liste", "Use Default Settings?": "Standardeinstellungen verwenden?", "Use Download first if you want to save a copy of the current key. Continue?": "Verwenden Sie zuerst „Herunterladen“, wenn Sie eine Kopie des aktuellen Schlüssels speichern möchten. Weitermachen?", @@ -7629,6 +7682,7 @@ "active VF(s)": "aktive VF(s)", "active VFs": "aktive VFs", "active Virtual Functions. Changing its driver binding would destroy every VF.": "aktive virtuelle Funktionen. Eine Änderung der Treiberbindung würde jedes VF zerstören.", + "added in Proxmox": "in Proxmox hinzugefügt", "after copy; source tree is incomplete.": "nach Kopie; Der Quellbaum ist unvollständig.", "after update.": "nach dem Update.", "all guests": "alle Gäste", @@ -7949,8 +8003,8 @@ "partition(s). Partition table preserved.": "Partition(en). Partitionstabelle bleibt erhalten.", "pasted Compose file": "Einfügen der Compose-Datei", "paths for next boot (/etc/pve, guests, drivers, ...)": "Pfade für den nächsten Start (/etc/pve, Gäste, Treiber, ...)", - "pct exec authorization failed": "PCT-Exec-Autorisierung fehlgeschlagen", - "pct push failed. Check log:": "PCT-Push fehlgeschlagen. Protokoll prüfen:", + "pct exec authorization failed": "Autorisierung für pct exec fehlgeschlagen", + "pct push failed. Check log:": "pct push fehlgeschlagen. Protokoll prüfen:", "pending (reboot required to enumerate full group)": "ausstehend (Neustart erforderlich, um die vollständige Gruppe aufzulisten)", "phpMyAdmin is installed with arbitrary server connections enabled: the login page has a Server field where the address of the MySQL or MariaDB server is entered, together with its user and password.": "phpMyAdmin wird mit aktivierten beliebigen Serververbindungen installiert: Die Anmeldeseite hat ein Serverfeld, in dem die Adresse des MySQL- oder MariaDB-Servers zusammen mit seinem Benutzer und seinem Passwort eingegeben wird.", "pigz configuration completed": "Pigz-Konfiguration abgeschlossen", @@ -7973,8 +8027,8 @@ "pvesm command not found. This should not happen on Proxmox.": "pvesm-Befehl nicht gefunden. Dies sollte auf Proxmox nicht passieren.", "pvesm not available.": "pvesm nicht verfügbar.", "pvesm not found.": "pvesm nicht gefunden.", - "pvesm path failed, trying manual detection...": "pvesm-Pfad fehlgeschlagen, manuelle Erkennung versucht ...", - "pvesm status failed": "pvesm-Status fehlgeschlagen", + "pvesm path failed, trying manual detection...": "pvesm path fehlgeschlagen, manuelle Erkennung wird versucht...", + "pvesm status failed": "pvesm status fehlgeschlagen", "pyLoad is a Free and Open Source download manager written in Python and designed to be extremely lightweight, easily extensible and fully manageable via web.": "pyLoad ist ein Free- und Open Source-Download-Manager, der in Python geschrieben ist und extrem leicht, leicht erweiterbar und vollständig über das Web zu verwalten ist.", "pyLoad web interface": "pyLoad Web Interface", "qBittorrent WebUI password (user: admin)": "qBittorrent WebUI Passwort (Benutzer: admin)", @@ -7996,6 +8050,7 @@ "remapped users": "Benutzer neu zugeordnet", "remove the (now-empty) directory if possible": "Entfernen Sie nach Möglichkeit das (jetzt leere) Verzeichnis", "removed from Proxmox": "aus Proxmox entfernt", + "removed in Proxmox": "in Proxmox entfernt", "removed successfully from Proxmox.": "erfolgreich aus Proxmox entfernt.", "requires running the official installer, which restarts the Monitor service.": "erfordert die Ausführung des offiziellen Installationsprogramms, das den Monitor-Dienst neu startet.", "requires the package": "erfordert das Paket", @@ -8032,8 +8087,8 @@ "skipped (already exist)": "übersprungen (existiert bereits)", "smbclient command is not working properly.": "Der smbclient-Befehl funktioniert nicht ordnungsgemäß.", "smbclient command not found after installation.": "Der Befehl smbclient wurde nach der Installation nicht gefunden.", - "sources.list update skipped (no change)": "Aktualisierung der Quellenliste übersprungen (keine Änderung)", - "sources.list updated to Trixie": "Quellen. Liste auf Trixie aktualisiert", + "sources.list update skipped (no change)": "Aktualisierung von sources.list übersprungen (keine Änderung)", + "sources.list updated to Trixie": "sources.list auf Trixie aktualisiert", "ssh-keygen failed. Cannot create a new SSH key.": "ssh-keygen ist fehlgeschlagen. Es kann kein neuer SSH-Schlüssel erstellt werden.", "stale entry/entries for interfaces no longer present": "veralteter Eintrag/Einträge für Schnittstellen, die nicht mehr vorhanden sind", "standard performance": "Standardleistung", @@ -8052,7 +8107,7 @@ "switch_gpu_mode.sh was not found.": "switch_gpu_mode.sh wurde nicht gefunden.", "sysfs ROM dump failed — trying ACPI VFCT table...": "sysfs-ROM-Dump fehlgeschlagen – Versuch der ACPI-VFCT-Tabelle ...", "syslog-ng allows you to flexibly collect, parse, classify, rewrite and correlate logs from across your infrastructure and store or route them to log analysis tools.": "syslog-ng ermöglicht es Ihnen, Protokolle aus Ihrer gesamten Infrastruktur flexibel zu sammeln, zu analysieren, zu klassifizieren, neu zu schreiben und zu korrelieren und sie zu Protokollanalyse-Tools zu speichern oder zu leiten.", - "systemctl restart networking failed:": "systemctl-Neustart des Netzwerks fehlgeschlagen:", + "systemctl restart networking failed:": "systemctl restart networking fehlgeschlagen:", "systemd OnCalendar expression": "systemd OnCalendar-Ausdruck", "the API key was not generated on the first start": "Der API-Schlüssel wurde beim ersten Start nicht generiert", "the NVIDIA hook of the container is not available:": "der NVIDIA-Hook des Containers ist nicht verfügbar:", @@ -8099,18 +8154,20 @@ "using": "verwenden", "using direct path (not guaranteed to survive reboots).": "Verwenden des direkten Pfads (keine Garantie, dass Neustarts überlebt werden).", "vCPUs:": "vCPUs:", + "value changed in Proxmox": "Wert in Proxmox geändert", "vfio-pci IDs configured": "vfio-pci-IDs konfiguriert", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "vfio-pci-IDs in /etc/modprobe.d/vfio.conf", "video": "Video", "video + recognition": "Video + Erkennung", "video only": "nur Video", "vzdump backup speed optimization completed": "vzdump-Backup-Geschwindigkeitsoptimierung abgeschlossen", - "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag baut seine Links aus der während der Installation angegebenen Adresse auf. Wenn es nicht mit der Adresse des Containers übereinstimmt, bearbeiten Sie lxc.environment. Laufzeit: SYMFONY ENV DOMAIN NAME in /etc/pve/lxc/.conf mit angehaltenem Container und erneut starten.", + "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag erzeugt seine Links aus der bei der Installation angegebenen Adresse. Passt diese nicht zur Container-Adresse, stoppen Sie den Container, bearbeiten Sie lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf und starten Sie ihn erneut.", "wallabag listens on port 80 of the container and stores its data in SQLite.": "wallabag hört auf Port 80 des Containers und speichert seine Daten in SQLite.", "wallabag web interface": "wallabag Web-Schnittstelle", "was": "war", "was installed, but the kernel reports:": "wurde installiert, aber der Kernel meldet:", "when finished": "wenn fertig", + "when the server has no subscription (you will be asked)": "Wenn der Server kein Abonnement hat (Sie werden gefragt)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "Bindet die GPU beim nächsten Neustart erneut an vfio-pci, wodurch der zu installierende Treiber beschädigt wird.", "wipefs failed on": "Wipefs sind fehlgeschlagen", "with": "mit", diff --git a/lang/es.json b/lang/es.json index de4d581e..1434b077 100644 --- a/lang/es.json +++ b/lang/es.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (ruta por defecto de vzdump)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (por defecto en Proxmox)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = sticky bit + rwx para todos. No se necesita un grupo compartido.", + "====== PBS UPDATE COMPLETED ======": "====== ACTUALIZACIÓN PBS COMPLETADA ======", "====== PVE UPDATE COMPLETED ======": "====== ACTUALIZACIÓN PVE COMPLETADA ======", ">> Create a new backup job <<": ">> Crear un trabajo de backup nuevo <<", ">> Save and return <<": ">> Guardar y volver <<", @@ -86,6 +87,7 @@ "A modern wiki and knowledge base for teams": "Una wiki y base de conocimiento moderna para equipos", "A new ProxMenux version is available:": "Una nueva versión de ProxMenux está disponible:", "A new kernel is staged for the next boot:": "Se prepara un nuevo kernel para el siguiente arranque:", + "A newer kernel is installed. Reboot before the next backup window to use it.": "Hay un kernel más reciente instalado. Reinicie antes de la próxima ventana de backups para usarlo.", "A newer version is available:": "Hay una versión más nueva disponible:", "A pending operation exists for": "Existe una operación pendiente", "A pending stack assembly already exists; it is not overwritten": "Ya existe un ensamblado de pila pendiente; no se sobrescribe", @@ -168,6 +170,7 @@ "APT language configuration updated": "Configuración del idioma APT actualizada", "APT language downloads restored": "Descargas del idioma APT restauradas", "APT package lists updated": "Listas de paquetes APT actualizadas", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "La actualización de APT ha fallado. Revise la suscripción y los repositorios de PBS en Administration > Repositories. Log:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "No se pudieron actualizar los índices de APT. Comprueba la suscripción y los repositorios de PBS en su interfaz nativa; no se ha cambiado ninguna fuente.", "APT update failed. Check repository access; NVIDIA installation stopped.": "La actualización de APT ha fallado. Verifica el acceso a los repositorios; la instalación de NVIDIA se ha detenido.", "Aborted": "Abortado", @@ -256,7 +259,7 @@ "Add new PVE 9 enterprise repository (deb822 format) (Only if using enterprise):": "Agregue un nuevo repositorio empresarial PVE 9 (formato deb822) (solo si usa Enterprise):", "Add or change a device": "Agregar o cambiar un dispositivo", "Add or remove extra paths and devices": "Añadir o eliminar rutas y dispositivos extra", - "Add physical disk to VM via": "Agregue el disco físico a la VM mediante", + "Add physical disk to VM via": "Añadir un disco físico a la VM mediante", "Add share block in /etc/samba/smb.conf:": "Agregue el bloque para compartir en /etc/samba/smb.conf:", "Add unprivileged flag to container configuration:": "Agregar la opción de contenedor sin privilegios a su configuración:", "Add virtual disk": "Agregar disco virtual", @@ -317,7 +320,7 @@ "After confirming, you will be asked to choose the NVIDIA driver version to install.": "Después de confirmar, se le pedirá que elija la versión del controlador NVIDIA para instalar.", "After detailed analysis, no changes are needed.": "Después de un análisis detallado, no se necesitan cambios.", "After detailed analysis, no cleanup is needed.": "Después de un análisis detallado, no es necesaria ninguna limpieza.", - "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Después de iniciar sesión, ejecute: ip a para obtener la dirección IP.\nLuego, ingrese esa dirección IP en su navegador web de esta manera:\n http://DIRECCIÓN_IP\n\nEsto abrirá el panel de Umbral OS.", + "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Después de iniciar sesión, ejecuta ip a para obtener la dirección IP.\nIntroduce esa dirección en el navegador de esta forma:\n http://IP_ADDRESS\n\nSe abrirá el panel de Umbral OS.", "After pasting on the server, ensure the file is chmod 600 and owned by": "Después de pegarlo en el servidor, asegúrese de que el archivo sea chmod 600 y sea propiedad de", "After reboot the system will be fully accessible (SSH, web UI, login), but the following components will be reinstalled in BACKGROUND — until they finish, commands like nvidia-smi may not yet be available:": "Después de reiniciar, el sistema será completamente accesible (SSH, interfaz web e inicio de sesión), pero los siguientes componentes se reinstalarán EN SEGUNDO PLANO; hasta que finalicen, es posible que comandos como nvidia-smi aún no estén disponibles:", "After reboot, these components will reinstall in background:": "Después de reiniciar, estos componentes se reinstalarán en segundo plano:", @@ -440,10 +443,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "¿Aplicar ahora la actualización de paquetes revisada? Los servicios podrían reiniciarse y se eliminarán los paquetes indicados para su eliminación.", "Apply these changes?": "¿Aplicar estos cambios?", "Applying AMD-specific fixes...": "Aplicando correcciones específicas de AMD...", + "Applying CPU and RAM changes without rebuilding the container...": "Aplicando los cambios de CPU y RAM sin recrear el contenedor...", "Applying Changes": "Aplicando cambios", "Applying Controller/NVMe passthrough to VM": "Aplicando el passthrough de controlador/NVMe a la VM", "Applying balanced memory optimization settings...": "Aplicando configuraciones equilibradas de optimización de memoria...", "Applying changes safely...": "Aplicando cambios de forma segura...", + "Applying container settings without rebuilding the container...": "Aplicando los ajustes sin recrear el contenedor...", "Applying custom restore": "Aplicar restauración personalizada", "Applying default VM configuration": "Aplicar la configuración predeterminada de VM", "Applying host permissions for unprivileged LXC bind-mounts...": "Aplicando permisos de host para montajes de enlace LXC sin privilegios...", @@ -536,6 +541,7 @@ "Available Borg targets:": "Destinos Borg disponibles:", "Available Disks on Host": "Discos disponibles en el host", "Available ISO Images": "Imágenes ISO disponibles", + "Available PBS Version": "Versión PBS disponible", "Available PBS repositories:": "Repositorios de PBS disponibles:", "Available PVE Version": "Versión PVE disponible", "Available Shares:": "Recursos compartidos disponibles:", @@ -563,7 +569,7 @@ "Babybuddy is a buddy for babies! Helps caregivers track sleep, feedings, diaper changes, tummy time and more to learn about and predict baby's needs without (as much) guess work.": "¡Babybuddy es un compañero para bebés! Ayuda a quienes los cuidan a registrar el sueño, las tomas, los cambios de pañal, el tiempo boca abajo y más, para conocer y prever las necesidades del bebé sin (tantas) conjeturas.", "Back": "Volver", "Back to job list": "Volver a la lista de trabajos", - "Back to previous menu or Esc + Enter": "Volver al menú anterior o Esc + Enter", + "Back to previous menu or Esc + Enter": "Volver al menú anterior o Esc + Intro", "Backed up and cleared": "Guardado en backup y vaciado", "Backend": "Backend", "Backend:": "Parte trasera:", @@ -748,6 +754,7 @@ "Cannot verify the reused disk:": "No se puede verificar el disco reutilizado:", "Cannot write the image archive to {storage}. Check storage access and mount status.": "No se puede escribir el archivo de la imagen en {storage}. Comprueba el acceso al almacenamiento y que esté montado.", "Capabilities cannot be kept and all dropped at the same time": "No se pueden mantener las capacidades y todas se disminuyen al mismo tiempo", + "Catalog options": "Opciones del catálogo", "Category": "Categoría", "Caution: Maximum mode generates more heat.": "Precaución: el modo máximo genera más calor.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Comprobación de Ceph omitida por el indicador del usuario (--ignore-ceph-check)", @@ -779,7 +786,7 @@ "Certificate provider: empty for Let's Encrypt, zerossl for ZeroSSL": "Proveedor de certificados: vacío para Let's Encrypt, zerossl para ZeroSSL", "Change GPU acceleration?": "¿Cambiar la aceleración de GPU?", "Change Language": "Cambiar idioma", - "Change Release Channel": "Cambiar canal de release", + "Change Release Channel": "Cambiar canal de versiones", "Change it after the first login.": "Cámbialo después del primer inicio de sesión.", "Change or add an environment variable?": "¿Cambiar o añadir una variable de entorno?", "Change the access network?": "¿Cambiar la red de acceso?", @@ -791,7 +798,7 @@ "Changes have been applied to the configuration file.": "Se han aplicado cambios al archivo de configuración.", "Changes prepared": "Cambios preparados", "Changes will apply after reboot.": "Los cambios se aplicarán después del reinicio.", - "Changing Release Channel": "Cambio de canal de release", + "Changing Release Channel": "Cambio de canal de versiones", "Changing between a privileged and an unprivileged container, or the host monitor mode, requires installing the application again": "Cambiar entre contenedor privilegiado y sin privilegios, o el modo de monitor del host, requiere instalar la aplicación de nuevo", "Changing the machine type on an existing installed VM is not safe: it changes the chipset and PCI slot layout, which typically prevents the guest OS from booting.": "Cambiar el tipo de máquina de una VM ya instalada no es seguro: cambia el chipset y la disposición de las ranuras PCI, lo que normalmente impide que arranque el sistema operativo de la VM.", "Changing the rootfs or its storage requires a separate migration": "Cambiar los rootfs o su almacenamiento requiere una migración separada", @@ -800,20 +807,20 @@ "Check": "Controlar", "Check BIOS/UEFI in Hardware > BIOS — must match what the original VM used": "Verifique BIOS/UEFI en Hardware > BIOS: debe coincidir con lo que usó la VM original", "Check Coral USB/M.2 detection": "Comprobar la detección de Coral USB/M.2", - "Check LXC GPU/TPU mapping": "Verifique el mapeo LXC GPU/TPU", + "Check LXC GPU/TPU mapping": "Comprobar el mapeo de GPU/TPU de los LXC", "Check NFS Status": "Verificar el estado de NFS", - "Check NVIDIA driver and devices": "Verifique el controlador y los dispositivos NVIDIA", - "Check Proxmox Web UI status": "Verifique el estado de la interfaz de usuario web de Proxmox", + "Check NVIDIA driver and devices": "Comprobar el controlador y los dispositivos NVIDIA", + "Check Proxmox Web UI status": "Comprobar el estado de la interfaz web de Proxmox", "Check Proxmox firewall status": "Comprobar el estado del cortafuegos de Proxmox", "Check Samba Status": "Verificar el estado de Samba", "Check VM disk/PCI slots": "Verifique el disco VM/ranuras PCI", - "Check VM passthrough settings": "Revise la configuración de passthrough de la VM", + "Check VM passthrough settings": "Revisar la configuración de passthrough de la VM", "Check access to an image": "Comprobar el acceso a una imagen", "Check connectivity with another host": "Verificar la conectividad con otro host", "Check container mount points": "Verifique los puntos de montaje del contenedor", "Check if container is unprivileged:": "Compruebe si el contenedor no tiene privilegios:", - "Check kernel params (IOMMU flags)": "Verifique los parámetros del kernel (indicadores IOMMU)", - "Check loaded GPU/TPU modules": "Verifique los módulos GPU/TPU cargados", + "Check kernel params (IOMMU flags)": "Comprobar los parámetros del kernel (opciones IOMMU)", + "Check loaded GPU/TPU modules": "Comprobar los módulos de GPU/TPU cargados", "Check log:": "Revise el log:", "Check resulting config:": "Verifique la configuración resultante:", "Check script not found": "Comprobar script no encontrado", @@ -882,6 +889,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Elija una programación (sintaxis de eventos de calendario de systemd):", "Choose a script to execute:": "Elige un script para ejecutar:", "Choose a server:": "Elige un servidor:", + "Choose a setting. Changes are applied only after the final review.": "Elige qué configurar. Los cambios solo se aplican tras la revisión final.", "Choose a share to mount:": "Elige un recurso compartido para montar:", "Choose a specific supported GPU or USB node": "Elija un nodo concreto de GPU o USB compatible", "Choose action for this controller/NVMe:": "Elige la acción para este controlador/NVMe:", @@ -911,7 +919,7 @@ "Choose storage for the downloaded image.": "Elige el almacenamiento para la imagen descargada.", "Choose the driver version for Coral M.2:": "Elige la versión del controlador para Coral M.2:", "Choose the filesystem for the new partition:": "Elige el sistema de archivos para la nueva partición:", - "Choose the release channel to use:": "Elige el canal de release que quieres usar:", + "Choose the release channel to use:": "Elige el canal de versiones que quieres usar:", "Choose the storage for the EFI disk (4MB):": "Elija el almacenamiento para el disco EFI (4 MB):", "Choose the storage for the TPM disk (4MB):": "Elija el almacenamiento para el disco TPM (4 MB):", "Choose the storage for the loader disk:": "Elija el almacenamiento para el disco del cargador:", @@ -1518,6 +1526,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "Actual inventario NVIDIA resuelto: un refresco es necesario", "Current NVIDIA inventory resolved: no refresh is required": "Inventario NVIDIA actual resuelto: ningún refresco es necesario", "Current Network Configuration": "Configuración de red actual", + "Current PBS Version": "Versión PBS actual", "Current PVE Version": "Versión PVE actual", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Los scripts de host ProxMenux actuales registran recursos compartidos remotos como almacenamientos en Proxmox utilizando pvesm.", "Current ProxMenux version:": "Versión actual de ProxMenux:", @@ -1632,8 +1641,8 @@ "Default Proxmox logo applied": "Logotipo predeterminado de Proxmox aplicado", "Default is none (video-only passthrough). Use SPACE to toggle selections.": "Por defecto, ninguno (passthrough solo de vídeo). Use ESPACIO para marcar o desmarcar.", "Default journald configuration restored": "Configuración de diario predeterminada restaurada", - "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "La ubicación predeterminada es /mnt/. El recurso compartido se montará aquí en el host con permisos abiertos para que un LXC sin privilegios pueda vincularlo, montarlo y escribir en él. Para acceder a LXC, vincule esta ruta con LXC Mount Manager.", - "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "La ubicación predeterminada es /mnt/. El recurso compartido se montará aquí en el host. Utilice esta ruta en /etc/fstab. Para acceder a LXC, vincule esta ruta con LXC Mount Manager.", + "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "La ubicación predeterminada es /mnt/. El recurso compartido se montará aquí, en el host, con permisos abiertos para que un LXC sin privilegios pueda montarlo mediante un bind mount y escribir en él. Usa LXC Mount Manager para crear ese montaje en el LXC.", + "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "La ubicación predeterminada es /mnt/. El recurso compartido se montará aquí, en el host. Usa esta ruta en /etc/fstab. Para acceder desde un LXC, crea un bind mount de esta ruta con LXC Mount Manager.", "Default login": "Acceso predeterminado", "Default options": "Opciones predeterminadas", "Default options read/write": "Opciones predeterminadas lectura/escritura", @@ -1645,8 +1654,8 @@ "Delete Borg target": "Eliminar destino Borg", "Delete Export": "Eliminar Exportar", "Delete Share": "Eliminar Compartir", - "Delete a CT (irreversible). Use the correct ": "Eliminar un CT (irreversible). Utilice el correcto", - "Delete a VM (irreversible). Use the correct ": "Eliminar una VM (irreversible). Utilice el correcto", + "Delete a CT (irreversible). Use the correct ": "Eliminar un CT (irreversible). Usa el correcto", + "Delete a VM (irreversible). Use the correct ": "Eliminar una VM (irreversible). Usa el correcto", "Delete archive": "Eliminar archivo", "Delete backup job": "¿Eliminar el trabajo de backup", "Delete it?": "¿Eliminarla?", @@ -1683,6 +1692,7 @@ "Detected Coral hardware:": "Hardware Coral detectado:", "Detected GPU(s):": "GPU detectadas:", "Detected Intel CPU": "CPU Intel detectada", + "Detected Proxmox Backup Server": "Proxmox Backup Server detectado", "Detected Proxmox VE": "Proxmox VE detectado", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Proxmox VE 8.x detectado: instalación de Ceph Squid para Debian", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Detectado Proxmox VE 9.x - Instalación de Ceph Squid para Debian Trixie", @@ -1764,6 +1774,9 @@ "Disabling and stopping rpcbind service and socket...": "Deshabilitar y detener el servicio y el socket rpcbind...", "Disabling non-free firmware warnings...": "Deshabilitando las advertencias de firmware no libre...", "Disabling portmapper/rpcbind for security...": "Deshabilitando portmapper/rpcbind por seguridad...", + "Discard changes": "Descartar cambios", + "Discard unapplied changes?": "¿Descartar los cambios sin aplicar?", + "Discover NFS exports on a server": "Descubrir los exports NFS de un servidor", "Discover targets on portal:": "Detectar destinos en el portal:", "Discovering iSCSI targets...": "Descubriendo destinos iSCSI...", "Discovery successful": "Detección correcta", @@ -1843,6 +1856,7 @@ "Do you want to proceed with the system update?": "¿Quieres continuar con la actualización del sistema?", "Do you want to proceed?": "¿Quieres continuar?", "Do you want to reboot now?": "¿Quieres reiniciar ahora?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "¿Desea eliminar el banner de suscripción de Proxmox de la interfaz web de Proxmox Backup Server?", "Do you want to remove the stale entry from vfio.conf and continue?": "¿Quiere eliminar la entrada obsoleta de vfio.conf y continuar?", "Do you want to remove the stale entry from vfio.conf?": "¿Quiere eliminar la entrada obsoleta de vfio.conf?", "Do you want to restart the network service now to apply changes?": "¿Quieres reiniciar el servicio de red ahora para aplicar los cambios?", @@ -1957,7 +1971,7 @@ "Enable": "Habilitar", "Enable / disable job timer": "Activar/desactivar el temporizador de copias", "Enable High Availability services": "Activar servicios de alta disponibilidad", - "Enable IOMMU in GRUB or ZFS boot": "Habilite IOMMU en el arranque GRUB o ZFS", + "Enable IOMMU in GRUB or ZFS boot": "Activar IOMMU en el arranque GRUB o ZFS", "Enable IOMMU support if not enabled": "Habilite la compatibilidad con IOMMU si no está habilitado", "Enable IOMMU, reboot the host, and try again.": "Habilite IOMMU, reinicie el host y vuelva a intentarlo.", "Enable Intel/AMD VA-API video acceleration": "Activar la aceleración de vídeo VA-API de Intel/AMD", @@ -2006,30 +2020,30 @@ "Ensuring proxmox.sources (PVE 9, no-subscription, deb822) is present...": "Garantizar que proxmox.sources (PVE 9, sin suscripción, deb822) esté presente...", "Ensuring pve-enterprise.sources (PVE 9, deb822) is present...": "Garantizar que pve-enterprise.sources (PVE 9, deb822) esté presente...", "Enter": "Ingresar", - "Enter CT ID:": "Ingrese el ID de CT:", - "Enter MSR register (e.g. 0x10):": "Ingrese el registro MSR (por ejemplo, 0x10):", + "Enter CT ID:": "Introduce el ID del CT:", + "Enter MSR register (e.g. 0x10):": "Introduce el registro MSR (por ejemplo, 0x10):", "Enter NFS export path (e.g., /mnt/shared):": "Ingrese la ruta de exportación NFS (por ejemplo, /mnt/shared):", - "Enter NFS server IP or hostname:": "Ingrese la IP o el nombre de host del servidor NFS:", - "Enter NVMe device (e.g., nvme0):": "Ingrese el dispositivo NVMe (por ejemplo, nvme0):", - "Enter PCI BDF (e.g. 0000:01:00.0):": "Ingrese PCI BDF (por ejemplo, 0000:01:00.0):", + "Enter NFS server IP or hostname:": "Introduce la IP o el nombre de host del servidor NFS:", + "Enter NVMe device (e.g., nvme0):": "Introduce el dispositivo NVMe (por ejemplo, nvme0):", + "Enter PCI BDF (e.g. 0000:01:00.0):": "Introduce el BDF PCI (por ejemplo, 0000:01:00.0):", "Enter PCI BDF (e.g. 0000:04:00.0):": "Ingrese PCI BDF (por ejemplo, 0000:04:00.0):", "Enter Path": "Ingresar ruta", "Enter Samba server IP:": "Ingrese la IP del servidor Samba:", "Enter Samba share name:": "Ingrese el nombre del recurso compartido de Samba:", "Enter Tailscale Auth Key": "Ingrese la clave de autenticación de Tailscale", - "Enter VM ID:": "Introduzca el ID de la máquina virtual:", - "Enter VM/CT ID:": "Introduzca el ID de VM/CT:", + "Enter VM ID:": "Introduce el ID de la VM:", + "Enter VM/CT ID:": "Introduce el ID de la VM o del CT:", "Enter ZFS pool name for the selected disk:": "Ingrese el nombre del grupo ZFS para el disco seleccionado:", - "Enter a name for the mount point (used as /mnt/):": "Ingrese un nombre para el punto de montaje (usado como /mnt/):", + "Enter a name for the mount point (used as /mnt/):": "Introduce un nombre para el punto de montaje (se usará como /mnt/):", "Enter a name for the new virtual machine:": "Ingrese un nombre para la nueva máquina virtual:", "Enter a non-empty password or access token on a single line.": "Escribe una contraseña o un token de acceso, en una sola línea.", - "Enter a number, or write or paste a command:": "Ingrese un número, o escriba o pegue un comando:", + "Enter a number, or write or paste a command:": "Introduce un número, o escribe o pega un comando:", "Enter a systemd calendar event, e.g.:": "Introduzca un evento de calendario de systemd, por ejemplo:", "Enter a usable IPv4 address with its prefix, for example": "Introduzca una dirección IPv4 usable con su prefijo, por ejemplo", "Enter a valid registry username.": "Escribe un usuario válido para el registro.", - "Enter backup file (.zst):": "Ingrese el archivo de backup (.zst):", - "Enter backup path (.tar.zst):": "Ingrese la ruta de backup (.tar.zst):", - "Enter backup path (.vma.zst):": "Ingrese la ruta de backup (.vma.zst):", + "Enter backup file (.zst):": "Introduce el archivo de backup (.zst):", + "Enter backup path (.tar.zst):": "Introduce la ruta del backup (.tar.zst):", + "Enter backup path (.vma.zst):": "Introduce la ruta del backup (.vma.zst):", "Enter base folder for temporary files and converter (default:": "Ingrese la carpeta base para archivos temporales y convertidor (predeterminado:", "Enter boot target (e.g. scsi0, sata0, ide0):": "Ingrese el destino de inicio (por ejemplo, scsi0, sata0, ide0):", "Enter content types (comma-separated):": "Introduzca los tipos de contenido (separados por comas):", @@ -2042,30 +2056,30 @@ "Enter custom pathr": "Ingrese una ruta personalizada", "Enter destination directory for exported file(s):": "Ingrese el directorio de destino para los archivos exportados:", "Enter destination path for ISO file": "Ingrese la ruta de destino para el archivo ISO", - "Enter destination path:": "Introduzca la ruta de destino:", - "Enter device (e.g., sda or nvme0):": "Ingrese el dispositivo (por ejemplo, sda o nvme0):", + "Enter destination path:": "Introduce la ruta de destino:", + "Enter device (e.g., sda or nvme0):": "Introduce el dispositivo (por ejemplo, sda o nvme0):", "Enter directory containing OVA/OVF files:": "Ingrese al directorio que contiene los archivos OVA/OVF:", "Enter directory path:": "Introduzca la ruta del directorio:", - "Enter disk image path:": "Ingrese la ruta de la imagen del disco:", + "Enter disk image path:": "Introduce la ruta de la imagen de disco:", "Enter disk or partition path (prefer /dev/disk/by-id/...):": "Ingrese la ruta del disco o la partición (prefiera /dev/disk/by-id/...):", - "Enter domain name:": "Introduzca el nombre de dominio:", - "Enter domain:": "Introduzca el dominio:", + "Enter domain name:": "Introduce el nombre de dominio:", + "Enter domain:": "Introduce el dominio:", "Enter filesystem (ext4/xfs/btrfs):": "Ingrese al sistema de archivos (ext4/xfs/btrfs):", "Enter folder name for /mnt:": "Ingrese el nombre de la carpeta para /mnt:", - "Enter full disk path as shown above (starting with /dev/disk/by-id/xxx...):": "Ingrese la ruta completa del disco como se muestra arriba (comenzando con /dev/disk/by-id/xxx...):", + "Enter full disk path as shown above (starting with /dev/disk/by-id/xxx...):": "Introduce la ruta completa del disco tal como aparece arriba (empieza por /dev/disk/by-id/xxx...):", "Enter full path for mount point:": "Ingrese la ruta completa para el punto de montaje:", - "Enter full path to the disk image (e.g., /var/lib/vz/images/xyz.img):": "Ingrese la ruta completa a la imagen del disco (por ejemplo, /var/lib/vz/images/xyz.img):", - "Enter full path to the input image (e.g., /var/lib/vz/images/disk.vmdk):": "Ingrese la ruta completa a la imagen de entrada (por ejemplo, /var/lib/vz/images/disk.vmdk):", + "Enter full path to the disk image (e.g., /var/lib/vz/images/xyz.img):": "Introduce la ruta completa de la imagen de disco (por ejemplo, /var/lib/vz/images/xyz.img):", + "Enter full path to the input image (e.g., /var/lib/vz/images/disk.vmdk):": "Introduce la ruta completa de la imagen de entrada (por ejemplo, /var/lib/vz/images/disk.vmdk):", "Enter full path:": "Ingrese la ruta completa:", "Enter host IP (e.g., 192.168.0.100):": "Ingrese la IP del host (por ejemplo, 192.168.0.100):", - "Enter host or IP:": "Ingrese el host o IP:", - "Enter hostpci slot (e.g. 0):": "Ingrese la ranura hostpci (por ejemplo, 0):", + "Enter host or IP:": "Introduce el host o la IP:", + "Enter hostpci slot (e.g. 0):": "Introduce el slot hostpci (por ejemplo, 0):", "Enter hostpci slot number (e.g. 0):": "Ingrese el número de ranura hostpci (por ejemplo, 0):", "Enter iSCSI target portal IP or hostname:": "Ingrese la IP o el nombre de host del portal de destino iSCSI:", "Enter image full path:": "Ingrese la ruta completa de la imagen:", "Enter imported disk reference (e.g. local-lvm:vm-100-disk-0):": "Ingrese la referencia del disco importado (por ejemplo, local-lvm:vm-100-disk-0):", "Enter interface (sata/scsi/virtio/ide):": "Ingrese a la interfaz (sata/scsi/virtio/ide):", - "Enter interface name (e.g. eth0):": "Ingrese el nombre de la interfaz (por ejemplo, eth0):", + "Enter interface name (e.g. eth0):": "Introduce el nombre de la interfaz (por ejemplo, eth0):", "Enter mount path on host:": "Ingrese la ruta de montaje en el host:", "Enter mount point in CT (e.g. /mnt/data):": "Ingrese el punto de montaje en CT (por ejemplo, /mnt/data):", "Enter mp slot number (e.g. 0):": "Ingrese el número de ranura mp (por ejemplo, 0):", @@ -2077,26 +2091,26 @@ "Enter password for": "Introduzca la contraseña para", "Enter password:": "Introduzca la contraseña:", "Enter path manually": "Ingrese la ruta manualmente", - "Enter path to analyze:": "Ingrese la ruta a analizar:", + "Enter path to analyze:": "Introduce la ruta que quieres analizar:", "Enter path:": "Introduzca la ruta:", - "Enter pool name to clear errors:": "Ingrese el nombre del grupo para borrar errores:", - "Enter pool name to export:": "Ingrese el nombre del grupo para exportar:", - "Enter pool name to import:": "Ingrese el nombre del grupo para importar:", - "Enter pool name:": "Introduzca el nombre del grupo:", - "Enter pool/dataset name:": "Ingrese el nombre del grupo/conjunto de datos:", - "Enter pool/dataset to destroy:": "Ingrese el grupo/conjunto de datos para destruir:", - "Enter pool/dataset to mount:": "Ingrese el grupo/conjunto de datos para montar:", + "Enter pool name to clear errors:": "Introduce el nombre del pool cuyos errores quieres borrar:", + "Enter pool name to export:": "Introduce el nombre del pool que quieres exportar:", + "Enter pool name to import:": "Introduce el nombre del pool que quieres importar:", + "Enter pool name:": "Introduce el nombre del pool:", + "Enter pool/dataset name:": "Introduce el nombre del pool o dataset:", + "Enter pool/dataset to destroy:": "Introduce el pool o dataset que quieres destruir:", + "Enter pool/dataset to mount:": "Introduce el pool o dataset que quieres montar:", "Enter search term (leave empty to show all scripts):": "Ingrese el término de búsqueda (déjelo vacío para mostrar todos los scripts):", "Enter server IP": "Ingrese la IP del servidor", "Enter server IP/hostname manually": "Ingrese la IP/nombre de host del servidor manualmente", - "Enter server IP:": "Ingrese la IP del servidor:", + "Enter server IP:": "Introduce la IP del servidor:", "Enter slot number (e.g. 0):": "Ingrese el número de ranura (por ejemplo, 0):", "Enter storage ID for Proxmox:": "Ingrese el ID de almacenamiento para Proxmox:", - "Enter storage ID:": "Ingrese el ID de almacenamiento:", - "Enter storage name:": "Introduzca el nombre del almacenamiento:", + "Enter storage ID:": "Introduce el ID del almacenamiento:", + "Enter storage name:": "Introduce el nombre del almacenamiento:", "Enter subnet (e.g., 192.168.0.0/24):": "Ingrese la subred (por ejemplo, 192.168.0.0/24):", "Enter target disk (e.g. /dev/sdX):": "Ingrese el disco de destino (por ejemplo, /dev/sdX):", - "Enter target storage name (e.g., local-lvm):": "Ingrese el nombre del almacenamiento de destino (por ejemplo, local-lvm):", + "Enter target storage name (e.g., local-lvm):": "Introduce el nombre del almacenamiento de destino (por ejemplo, local-lvm):", "Enter target storage:": "Ingrese el almacenamiento de destino:", "Enter the BORG REPOKEY passphrase for target:": "Ingrese la frase de contraseña de BORG REPOKEY para el destino:", "Enter the Samba username:": "Ingrese el nombre de usuario de Samba:", @@ -2105,14 +2119,14 @@ "Enter the full path to the directory containing disk images:": "Ingrese la ruta completa al directorio que contiene las imágenes del disco:", "Enter the full path to the host directory:": "Ingrese la ruta completa al directorio del host:", "Enter the full path to the host folder:": "Ingrese la ruta completa a la carpeta del host:", - "Enter the full path to the output image (e.g., /var/lib/vz/images/output.qcow2):": "Ingrese la ruta completa a la imagen de salida (por ejemplo, /var/lib/vz/images/output.qcow2):", + "Enter the full path to the output image (e.g., /var/lib/vz/images/output.qcow2):": "Introduce la ruta completa de la imagen de salida (por ejemplo, /var/lib/vz/images/output.qcow2):", "Enter the host mount path:": "Ingrese la ruta de montaje del host:", "Enter the iperf3 server IP or hostname:": "Ingrese la IP o el nombre de host del servidor iperf3:", "Enter the maximum size (in MB) to allocate for /var/log in RAM (e.g. 128, 256, 512):": "Ingrese el tamaño máximo (en MB) para asignar para /var/log en la RAM (por ejemplo, 128, 256, 512):", "Enter the mount point for the NFS export (e.g., /mnt/mynfs):": "Ingrese el punto de montaje para la exportación NFS (por ejemplo, /mnt/mynfs):", "Enter the mount point for the shared folder (e.g., /mnt/myshare):": "Ingrese el punto de montaje para la carpeta compartida (por ejemplo, /mnt/myshare):", "Enter the mount point inside the CT for": "Introduzca el punto de montaje dentro del CT para", - "Enter the number or type the interface name:": "Ingrese el número o escriba el nombre de la interfaz:", + "Enter the number or type the interface name:": "Introduce el número o escribe el nombre de la interfaz:", "Enter the password for Samba user:": "Ingrese la contraseña para el usuario de Samba:", "Enter the recovery passphrase set when the keyfile was created:": "Ingrese la frase de contraseña de recuperación establecida cuando se creó el archivo de claves:", "Enter the size in whole GB, for example": "Introduzca el tamaño en GB enteros, por ejemplo", @@ -2163,11 +2177,11 @@ "Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Ejemplo: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak", "Example: daily or Mon..Fri 03:00": "Ejemplo: diario o lunes a viernes 03:00", "Example: mkdir -p /tmp/lxc_convert_114": "Ejemplo: mkdir -p /tmp/lxc_convert_114", - "Example: pct config 110 | grep unprivileged": "Ejemplo: configuración pct 110 | grep sin privilegios", - "Example: pct stop 110": "Ejemplo: parada pct 110", - "Example: pct stop 114": "Ejemplo: parada pct 114", + "Example: pct config 110 | grep unprivileged": "Ejemplo: pct config 110 | grep unprivileged", + "Example: pct stop 110": "Ejemplo: pct stop 110", + "Example: pct stop 114": "Ejemplo: pct stop 114", "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup": "Ejemplo: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup", - "Example: pvesm path local-lvm:vm-114-disk-0": "Ejemplo: ruta pvesm local-lvm:vm-114-disk-0", + "Example: pvesm path local-lvm:vm-114-disk-0": "Ejemplo: pvesm path local-lvm:vm-114-disk-0", "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf": "Ejemplo: sed -i '/^sin privilegios: 1/d' /etc/pve/lxc/110.conf", "Examples:": "Ejemplos:", "Execute destructive rollback?": "¿Ejecutar el rollback destructivo?", @@ -2267,6 +2281,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "No se pudo clonar el repositorio log2ram. Compruebe /tmp/log2ram_install.log", "Failed to configure EFI disk": "No se pudo configurar el disco EFI", "Failed to configure IOMMU automatically.": "No se pudo configurar IOMMU automáticamente.", + "Failed to configure Proxmox Backup Server repositories": "No se han podido configurar los repositorios de Proxmox Backup Server", "Failed to configure Proxmox repositories": "No se pudieron configurar los repositorios de Proxmox", "Failed to configure TPM device in VM": "No se pudo configurar el dispositivo TPM en VM", "Failed to configure repositories.": "No se pudieron configurar los repositorios.", @@ -2413,7 +2428,7 @@ "Fastfetch installed successfully": "Fastfetch se instaló correctamente", "Fastfetch is not installed correctly.": "Fastfetch no está instalado correctamente.", "Fastfetch is not installed.": "Fastfetch no está instalado.", - "Fastfetch is not installed. Run: apt install fastfetch": "Fastfetch no está instalado. Ejecute: apt install fastfetch", + "Fastfetch is not installed. Run: apt install fastfetch": "Fastfetch no está instalado. Ejecuta: apt install fastfetch", "Fastfetch now displays: System optimised by: ProxMenux": "Fastfetch ahora muestra: Sistema optimizado por: ProxMenux", "Fastfetch removed from system": "Fastfetch eliminado del sistema", "Fastfetch will start automatically in the console": "Fastfetch se iniciará automáticamente en la consola", @@ -2478,7 +2493,7 @@ "For a consistent export, the VM should be stopped.": "Para una exportación consistente, se debe detener la VM.", "For any question, issue or bug report, please use the official repository": "Para cualquier pregunta, problema o informe de error, utilice el repositorio oficial", "For dedicated GPUs, FLR is required by this policy to reduce VM": "Para GPU dedicadas, esta política requiere FLR para reducir la VM", - "For example: pct enter 101": "Por ejemplo: pct introduzca 101", + "For example: pct enter 101": "Por ejemplo: pct enter 101", "For guest shares add: --options guest": "Para recursos compartidos de invitado añada: --options guest", "For security reasons, the system cannot format it.": "Por razones de seguridad, el sistema no puede formatearlo.", "For unprivileged containers, use instead:": "Para contenedores sin privilegios, utilice en su lugar:", @@ -2487,8 +2502,8 @@ "Force NFSv3:": "Forzar NFSv3:", "Force NFSv4:": "Forzar NFSv4:", "Force stop VM now?": "¿Forzar la detención de VM ahora?", - "Force stop a container. Use the correct ": "Forzar la parada de un contenedor. Utilice el correcto", - "Force stop a virtual machine. Use the correct ": "Forzar la detención de una máquina virtual. Utilice el correcto", + "Force stop a container. Use the correct ": "Forzar la parada de un contenedor. Usa el correcto", + "Force stop a virtual machine. Use the correct ": "Forzar la parada de una VM. Usa el correcto", "Forcing removal...": "Forzando la eliminación...", "Format / Wipe Physical Disk (Safe)": "Formatear/borrar disco físico (seguro)", "Format Mode": "Modo de formato", @@ -2974,6 +2989,7 @@ "Inaccessible device during VM startup": "Dispositivo inaccesible durante el inicio de la VM", "Inactive": "Inactivo", "Inactive (entry in fstab, not currently mounted)": "Inactivo (entrada en fstab, no montado actualmente)", + "Include these changes in oci-compose?": "¿Incluir estos cambios en el oci-compose?", "Include these resources in oci-compose?": "¿Incorporar estos recursos al oci-compose?", "Includes /etc/network (may drop SSH immediately)": "Incluye /etc/network (puede eliminar SSH inmediatamente)", "Includes /etc/zfs": "Incluye /etc/zfs", @@ -3210,6 +3226,8 @@ "Invalid ALLOWED_HOSTS value": "Valor inválido ALLOWED_HOSTS", "Invalid Home Assistant OS check timeout": "Inválido Home Assistant OS check timeout", "Invalid ID": "ID no válida", + "Invalid IPv4 address": "Dirección IPv4 no válida", + "Invalid IPv4 gateway": "Puerta de enlace IPv4 no válida", "Invalid Intel render path": "Ruta de renderización Intel no válida", "Invalid Jellyfin path:": "Camino inválido Jellyfin:", "Invalid MAC address:": "Dirección MAC inválida:", @@ -3242,6 +3260,7 @@ "Invalid administrator email": "Correo electrónico del administrador inválido", "Invalid administrator user name": "Nombre del usuario del administrador inválido", "Invalid base VMID": "Base inválida VMID", + "Invalid bridge": "Puente no válido", "Invalid check package:": "Paquete de comprobación inválido:", "Invalid configuration path:": "Vía de configuración inválida:", "Invalid consume/export volumes": "Volumen de consumo/exportación inválidos", @@ -3305,6 +3324,7 @@ "Invalid remote name": "Nombre remoto inválido", "Invalid remote path": "Camino remoto inválido", "Invalid repair version:": "Versión de reparación inválida:", + "Invalid resource value": "Valor de recursos no válido", "Invalid resources": "Recursos inválidos", "Invalid restored volume path": "Camino de volumen restaurado inválido", "Invalid running state": "Estado de funcionamiento inválido", @@ -3323,6 +3343,7 @@ "Invalid stack members": "Miembros de pila no válidos", "Invalid stack name": "Nombre de pila no válido", "Invalid stack operation": "Operación de pila no válida", + "Invalid storage": "Almacenamiento no válido", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID de almacenamiento no válido. Utilice únicamente letras, números, guiones y guiones bajos.", "Invalid storage name": "Nombre de almacenamiento no válido", "Invalid suite application": "Aplicación de suite inválida", @@ -3594,7 +3615,7 @@ "Lynis is a security auditing tool that performs comprehensive system scans including:": "Lynis es una herramienta de auditoría de seguridad que realiza análisis completos del sistema que incluyen:", "Lynis is currently installed.": "Lynis está instalado actualmente.", "Lynis is not installed on this system.": "Lynis no está instalado en este sistema.", - "Lynis is not installed. Run: apt install lynis": "Lynis no está instalado. Ejecute: apt install lynis", + "Lynis is not installed. Run: apt install lynis": "Lynis no está instalado. Ejecuta: apt install lynis", "Lynis is ready to use": "Lynis está listo para usar", "Lynis updated to version:": "Lynis actualizado a la versión:", "Lynis version:": "Versión Lynis:", @@ -4152,6 +4173,7 @@ "No active Proxmox storage accepts OCI template archives.": "Ningún almacenamiento activo de Proxmox acepta archivos de plantilla OCI.", "No active exports or showmount not available": "No hay exportaciones activas o showmount no disponible", "No active session": "Ninguna sesión activa", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "No hay suscripción activa ni un repositorio de PBS utilizable. El modo no interactivo no puede cambiar las fuentes de APT; configúrelas en Administration > Repositories.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "No hay suscripción activa ni un repositorio de PVE utilizable. El modo no interactivo no puede cambiar las fuentes de APT; configúralas en Nodo > Actualizaciones > Repositorios.", "No additional GPU can be added.": "No se puede agregar ninguna GPU adicional.", "No additional device needs to be added.": "No es necesario agregar ningún dispositivo adicional.", @@ -4256,6 +4278,7 @@ "No password provided.": "No se proporcionó contraseña.", "No paths available": "No hay rutas disponibles", "No paths selected. Select at least one path.": "No hay rutas seleccionadas. Seleccione al menos una ruta.", + "No pending changes": "No hay cambios pendientes", "No pending paths to schedule for reboot.": "No hay rutas pendientes para programar el reinicio.", "No permanent NFS mounts configured.": "No se han configurado montajes NFS permanentes.", "No permanent Samba mounts configured.": "No hay montajes Samba permanentes configurados.", @@ -4394,6 +4417,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM eliminado (los artefactos de las marionetas pueden necesitar una limpieza manual)", "OVH server detected": "Servidor OVH detectado", "OVH server detection and RTM installation process completed": "Finalizado el proceso de detección del servidor OVH e instalación de RTM", + "Object detector": "Detector de objetos", "Object detector for Frigate": "Detector de objetos para Frigate", "Observer is not responding on port 4357": "El observador no responde en el puerto 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian es una aplicación de notas que te permite crear, enlazar y organizar tus notas en tu dispositivo, con cientos de plugins y temas para personalizar tu flujo de trabajo. También puedes publicar tus notas en línea, acceder a ellas sin conexión y sincronizarlas de forma segura con cifrado de extremo a extremo.", @@ -4525,7 +4549,7 @@ "PBS extraction failed": "Error en la extracción de PBS", "PBS host or IP address:": "Host de PBS o dirección IP:", "PBS package update": "Actualización de paquetes de PBS", - "PBS package upgrade completed. Check whether a reboot is required before your next backup window.": "Actualización de paquetes de PBS completada. Comprueba si es necesario reiniciar antes del próximo período de copias de seguridad.", + "PBS package upgrade completed. Check whether a reboot is required before your next backup window.": "Actualización de paquetes de PBS completada. Comprueba si hace falta reiniciar antes de la próxima ventana de backups.", "PBS package upgrade failed. Review the APT output before retrying.": "La actualización de paquetes de PBS ha fallado. Revisa la salida de APT antes de volver a intentarlo.", "PBS upload": "Subida a PBS", "PBS version and packages": "Versión y paquetes de PBS", @@ -4763,9 +4787,9 @@ "Preserving logs to /var/log.hdd before unmounting...": "Preservando logs en /var/log.hdd antes de desmontar...", "Press 'q' to exit": "Presione 'q' para salir", "Press Ctrl+C to stop the server and return to menu.": "Presione Ctrl+C para detener el servidor y regresar al menú.", - "Press ENTER to continue...": "Presione ENTER para continuar...", - "Press ENTER to return to the menu...": "Presione ENTER para regresar al menú...", - "Press ENTER to return...": "Presione ENTER para regresar...", + "Press ENTER to continue...": "Pulsa Intro para continuar...", + "Press ENTER to return to the menu...": "Pulsa Intro para volver al menú...", + "Press ENTER to return...": "Pulsa Intro para volver...", "Press Enter to close...": "Presione Entrar para cerrar...", "Press Enter to continue": "Presione Entrar para continuar", "Press Enter to continue...": "Presione Entrar para continuar...", @@ -4865,12 +4889,17 @@ "Proxmology logo applied": "Logotipo de Proxmology aplicado", "Proxmox 9 system update allready": "Actualización del sistema Proxmox 9 ya", "Proxmox APT repositories configured": "Repositorios Proxmox APT configurados", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Destinos de Proxmox Backup Server (PBS)", + "Proxmox Backup Server APT repositories configured": "Repositorios APT de Proxmox Backup Server configurados", + "Proxmox Backup Server Update": "Actualización de Proxmox Backup Server", + "Proxmox Backup Server update completed.": "Actualización de Proxmox Backup Server completada.", "Proxmox CIFS Storage Status:": "Estado de almacenamiento CIFS de Proxmox:", "Proxmox NFS Storage Status:": "Estado de almacenamiento NFS de Proxmox:", "Proxmox System Update": "Actualización del sistema Proxmox", "Proxmox Update": "Actualización de Proxmox", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "La actualización de paquetes de Proxmox VE 8 ha fallado. Revisa la salida de APT antes de volver a intentarlo.", "Proxmox VE 8 system update completed successfully": "La actualización del sistema Proxmox VE 8 se completó con éxito", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Guía de actualización manual de Proxmox VE 8 a 9", "Proxmox VE 9.x no-subscription repository created": "Se creó el repositorio sin suscripción Proxmox VE 9.x", @@ -4878,6 +4907,7 @@ "Proxmox VE configuration completed.": "Configuración de Proxmox VE completada.", "Proxmox VE safe update completed": "Actualización segura de Proxmox VE completada", "Proxmox auth logger service created and started": "Servicio de registro de autenticación Proxmox creado e iniciado", + "Proxmox could not apply the container settings": "Proxmox no ha podido aplicar los ajustes del contenedor", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Es posible que falte el repositorio empresarial de Proxmox o que no se pueda acceder a él. Intentando cambiar a sin suscripción...", "Proxmox filter configured": "Filtro Proxmox configurado", "Proxmox iSCSI Storage Status:": "Estado de almacenamiento Proxmox iSCSI:", @@ -5073,10 +5103,10 @@ "Reinstalling": "Reinstalando", "Reinstalling core Proxmox packages...": "Reinstalando los paquetes principales de Proxmox...", "Relative CPU priority (cpuunits)": "Prioridad relativa de la CPU (cpuunits)", - "Release Channel": "Canal de release", - "Release channel set to Beta.": "Canal de release establecido en Beta.", - "Release channel set to Stable.": "Canal de release establecido en Estable.", - "Release channel:": "Canal de release:", + "Release Channel": "Canal de versiones", + "Release channel set to Beta.": "Canal de versiones establecido en Beta.", + "Release channel set to Stable.": "Canal de versiones establecido en Estable.", + "Release channel:": "Canal de versiones:", "Reload Proxmox firewall rules": "Recargar las reglas del cortafuegos de Proxmox", "Reload network configuration (ifupdown2)": "Recargar la configuración de red (ifupdown2)", "Remapped Users:": "Usuarios reasignados:", @@ -5137,6 +5167,8 @@ "Remove saved credentials": "Eliminar las credenciales guardadas", "Remove subscription banner": "Eliminar banner de suscripción", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "¿Eliminar la aplicación? Los discos de sus contenedores se eliminan y solo un backup puede recuperarlos.", + "Remove the lock left on a VM by an interrupted task": "Quitar el bloqueo que deja en una VM una tarea interrumpida", + "Remove the lock left on a container by an interrupted task": "Quitar el bloqueo que deja en un contenedor una tarea interrumpida", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "¿Eliminar las credenciales guardadas de {registry}?\nLas aplicaciones en marcha seguirán funcionando.", "Remove the unprivileged flag from configuration:": "Elimine el flag unprivileged de la configuración:", "Remove unused packages and their config": "Eliminar paquetes no utilizados y su configuración", @@ -5206,7 +5238,7 @@ "Replace": "Reemplazar", "Replace with the actual ID.": "Reemplace con el ID real.", "Replace with your actual container ID": "Reemplace con su ID de contenedor real", - "Replace with the value from step 4": "Reemplace con el valor del paso 4", + "Replace with the value from step 4": "Sustituye por el valor del paso 4", "Replace invalid port(s)": "Reemplazar puerto(s) no válido(s)", "Replace it?": "¿Reemplazarlo?", "Replace paths with your actual values from step 5": "Reemplace las rutas con sus valores reales del paso 5", @@ -5218,6 +5250,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "El asistente de repositorios no está disponible. La instalación de NVIDIA se ha detenido.", "Repository policy returned an unexpected result.": "La política de repositorios ha devuelto un resultado inesperado.", "Repository ready.": "Repositorio listo.", + "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.": "Las fuentes de repositorio han cambiado, pero la actualización de la lista de paquetes de APT ha fallado. Operación detenida; las fuentes no se han revertido. Revise Administration > Repositories y vuelva a ejecutar apt-get update antes de continuar.", "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.": "Las fuentes de los repositorios han cambiado, pero la actualización de la lista de paquetes de APT ha fallado. La operación se ha detenido; las fuentes no se han revertido. Revisa Nodo > Actualizaciones > Repositorios y vuelve a ejecutar apt-get update antes de continuar.", "Repository switch declined; no APT source changed.": "Cambio de repositorio rechazado; no se ha modificado ninguna fuente de APT.", "Repository:": "Repositorio:", @@ -5244,7 +5277,7 @@ "Resources": "Recursos", "Restart Network": "Reiniciar la red", "Restart Network Service": "Reiniciar el servicio de red", - "Restart Web UI proxy": "Reinicie el proxy de la interfaz de usuario web", + "Restart Web UI proxy": "Reiniciar el proxy de la interfaz web", "Restart aMule automatically on a schedule (true or false)": "Reiniciar aMule automáticamente según una programación (true o false)", "Restart and enable Samba:": "Reinicie y habilite Samba:", "Restart container to activate mount?": "¿Reiniciar el contenedor para activar el montaje?", @@ -5312,6 +5345,7 @@ "Reverting ZFS ARC tuning...": "Revirtiendo el ajuste ZFS ARC...", "Reverting pigz wrapper...": "Revertir la envoltura de pigz...", "Reverting vzdump speed tuning...": "Revertir el ajuste de velocidad de vzdump...", + "Review and apply": "Revisar y aplicar", "Review external OCI changes": "Revisar cambios externos de OCI", "Review package upgrade": "Revisar la actualización de paquetes", "Review passthrough config files": "Revisar los archivos de configuración de passthrough", @@ -5336,9 +5370,9 @@ "Run GGUF LLMs locally with GPU acceleration": "Ejecuta LLM en formato GGUF en local con aceleración por GPU", "Run Now": "Ejecutar ahora", "Run PVE 8 to 9": "Ejecuta PVE 8 a 9", - "Run PVE 8 to 9 check": "Ejecute la verificación PVE 8 a 9", + "Run PVE 8 to 9 check": "Ejecutar la comprobación de PVE 8 a 9", "Run \\\"Install NVIDIA Drivers on Host\\\" first so the installer is cached.": "Primero ejecute \\\"Instalar controladores NVIDIA en el host\\\" para que el instalador se almacene en caché.", - "Run a full security audit": "Ejecute una auditoría de seguridad completa", + "Run a full security audit": "Ejecutar una auditoría de seguridad completa", "Run a job now": "Ejecute un trabajo ahora", "Run apt-get install -f to complete any pending package configurations": "Ejecutar apt-get install -f para completar las configuraciones de paquetes pendientes", "Run as root on the Proxmox node; the registry contains private data": "Ejecutar como root en el nodo Proxmox; el registro contiene datos privados", @@ -5354,7 +5388,7 @@ "Run now": "Ejecutar ahora", "Run security audit now": "Ejecute la auditoría de seguridad ahora", "Run the following inside the VM:": "Ejecute lo siguiente dentro de la VM:", - "Run the release-channel switch from an SSH session or the Proxmox host console with:": "Ejecute el cambio de canal de release desde una sesión SSH o la consola del host Proxmox con:", + "Run the release-channel switch from an SSH session or the Proxmox host console with:": "Ejecuta el cambio de canal de versiones desde una sesión SSH o la consola del host Proxmox con:", "Run the update from an SSH session or the Proxmox host console with:": "Ejecute la actualización desde una sesión SSH o la consola del host Proxmox con:", "Run this operation as root.": "Ejecuta esta operación como root.", "Run upgrade checklist script:": "Ejecute el script de la lista de verificación de actualización:", @@ -5549,7 +5583,7 @@ "Select Virtual Machine": "Seleccionar máquina virtual", "Select a Custom Logo": "Seleccione un logotipo personalizado", "Select a VirtIO ISO to use:": "Seleccione una ISO de VirtIO para usar:", - "Select a category of useful commands:": "Seleccione una categoría de comandos útiles:", + "Select a category of useful commands:": "Elige una categoría de comandos útiles:", "Select a category or search for applications:": "Seleccione una categoría o busque aplicaciones:", "Select a category or search for scripts:": "Seleccione una categoría o busque scripts:", "Select a custom ISO to use:": "Seleccione una ISO personalizada para usar:", @@ -5591,7 +5625,7 @@ "Select conversion guide:": "Seleccione la guía de conversión:", "Select conversion option:": "Seleccione la opción de conversión:", "Select destination": "Seleccionar destino", - "Select disk interface type:": "Seleccione el tipo de interfaz de disco:", + "Select disk interface type:": "Elige el tipo de interfaz del disco:", "Select driver version": "Seleccione la versión del controlador", "Select export format:": "Seleccione el formato de exportación:", "Select export permissions:": "Seleccione permisos de exportación:", @@ -5771,6 +5805,7 @@ "ShareCoveX panel": "Panel de ShareCoveX", "Shared Directory Ready:": "Directorio compartido listo:", "Shared Group": "Grupo compartido", + "Shared directories": "Directorios compartidos", "Shared directory created:": "Directorio compartido creado:", "Shared directory for consume and export": "Directorio compartido para consumo y exportación", "Shared directory for copy/sync operations": "Directorio compartido para operaciones de copia/sincronización", @@ -5800,6 +5835,7 @@ "Should show pve-manager/9.x.x": "Debería mostrar pve-manager/9.x.x", "Shoutrrr addresses for the notifications (optional)": "Direcciones de Shoutrrr para las notificaciones (opcional)", "Show All Scripts": "Mostrar todos los scripts", + "Show CT users and their UID/GID on the host": "Ver usuarios del CT y su UID/GID en el host", "Show CT users in table format": "Mostrar usuarios de CT en formato de tabla", "Show Container Privilege Status": "Mostrar estado de privilegios de los contenedores", "Show DNS resolution status": "Mostrar estado de resolución DNS", @@ -5813,14 +5849,14 @@ "Show Proxmox version": "Mostrar versión de Proxmox", "Show Routing Table": "Mostrar tabla de enrutamiento", "Show UUID and filesystem type of block devices": "Mostrar UUID y tipo de sistema de archivos de dispositivos de bloque", - "Show VM configuration. Use the correct ": "Mostrar la configuración de la máquina virtual. Utilice el correcto", + "Show VM configuration. Use the correct ": "Mostrar la configuración de la VM. Usa el correcto", "Show Version Information": "Mostrar información de versión", "Show ZFS pool status": "Mostrar el estado del grupo ZFS", "Show active firewall rules (iptables)": "Mostrar las reglas activas del cortafuegos (iptables)", "Show all active mounts from the kernel": "Mostrar todos los montajes activos del kernel", "Show available Proxmox upgrades": "Mostrar actualizaciones de Proxmox disponibles", "Show configured network bridges": "Mostrar puentes de red configurados", - "Show container configuration. Use the correct ": "Mostrar la configuración del contenedor. Utilice el correcto", + "Show container configuration. Use the correct ": "Mostrar la configuración del contenedor. Usa el correcto", "Show detailed keyfile info": "Mostrar información detallada del archivo de claves", "Show disk usage by mount point": "Mostrar uso del disco por punto de montaje", "Show jobs and last run status": "Mostrar tareas y estado de la última ejecución", @@ -5942,8 +5978,8 @@ "Start VM": "Iniciar máquina virtual", "Start VM after creation": "Iniciar VM después de la creación", "Start VM after creation?": "¿Iniciar VM después de la creación?", - "Start a container. Use the correct ": "Inicie un contenedor. Utilice el correcto", - "Start a virtual machine. Use the correct ": "Inicie una máquina virtual. Utilice el correcto", + "Start a container. Use the correct ": "Iniciar un contenedor. Usa el correcto", + "Start a virtual machine. Use the correct ": "Iniciar una VM. Usa el correcto", "Start each LXC with Proxmox (no coordinated startup)": "Iniciar cada LXC con Proxmox (sin arranque coordinado)", "Start long self-test (hours)": "Iniciar autotest largo (horas)", "Start long test now?": "¿Iniciar una prueba larga ahora?", @@ -5962,6 +5998,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Inicie la VM para comenzar la instalación de Windows desde la ISO montada.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "¿Iniciar las aplicaciones una vez registradas? Responde No si los contenedores originales siguen en marcha en otro host: los dos usarían las mismas direcciones.", "Start the converted container:": "Inicie el contenedor convertido:", + "Start the interactive Proxmox package upgrade": "Iniciar la actualización interactiva de paquetes de Proxmox", "Start the main system upgrade:": "Inicie la actualización principal del sistema:", "Start the stack with Proxmox": "Iniciar la pila con Proxmox", "Start uploading to PBS — sets a recovery passphrase": "Empezar a subir a PBS — establece una frase de contraseña de recuperación", @@ -6241,6 +6278,7 @@ "The Paperless persistent volumes need at least 8 GB": "Los volúmenes persistentes de Paperless necesitan al menos 8 GB", "The PostgreSQL volume needs at least 4 GB": "El volumen de PostgreSQL necesita al menos 4 GB", "The PostgreSQL volume needs at least 8 GB": "El volumen de PostgreSQL necesita al menos 8 GB", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "La actualización de Proxmox VE 8 no ha terminado. No se ha iniciado la actualización a Proxmox VE 9.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "Falta el conjunto de claves de archivo Proxmox; La instalación de Ceph no puede continuar de forma segura", "The Proxmox inventory and the local configurations differ": "El inventario de Proxmox y las configuraciones locales difieren", "The ROCm image has no support for the AMD GPU of this host:": "La imagen de ROCm no tiene soporte para la GPU AMD de este host:", @@ -6309,6 +6347,7 @@ "The build could not be checked beforehand; continuing without that check.": "La compilación no se pudo comprobar de antemano; se continúa sin esa comprobación.", "The cached image does not match the current digest": "La imagen en caché no coincide con el digest actual", "The cached image is damaged; it will be downloaded again.": "La imagen en caché está dañada; se descargará de nuevo.", + "The changes need the application installer": "Los cambios requieren el instalador de la aplicación", "The character device does not exist:": "El dispositivo de carácter no existe:", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "El comando pide una contraseña que no se muestra al escribirla. Tras crear los usuarios, la interfaz web pide un nombre de usuario y una contraseña.", "The command does not name an image": "El comando no nombra ninguna imagen", @@ -6427,8 +6466,10 @@ "The host bind source does not exist:": "La fuente de host bind no existe:", "The host bind source is not a regular file or directory:": "La fuente host bind no es un archivo o directorio regular:", "The host directory changed before it was mounted": "El directorio host cambió antes de montarlo", + "The host directory does not exist": "El directorio del host no existe", "The host directory may not be accessible from an unprivileged container.": "Es posible que no se pueda acceder al directorio del host desde un contenedor sin privilegios.", "The host has no IPv4 address on the selected bridge": "El host no tiene dirección IPv4 en el puente seleccionado", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "El host pierde el acceso a ellos. Si el grupo incluye un controlador de red o de almacenamiento que el host necesita, cancela en el resumen.", "The host monitor does not see the real host memory": "El monitor host no ve la memoria real del host", "The host monitor does not share this host namespace:": "El monitor host no comparte este espacio de nombres de host:", "The host monitor include is unknown or differs from the saved record": "El include del monitor del host es desconocido o difiere del registro guardado", @@ -6612,6 +6653,7 @@ "The repair must preserve the image dependencies:": "La reparación debe preservar las dependencias de la imagen:", "The requested VMID block is already in use": "El bloque VMID solicitado ya está en uso", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "El perfil de GPU solicitado para Machine learning no funciona; no se sustituye por CPU", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "El cambio de recursos no se ha podido revertir por completo. Vuelve al menú de gestión para revisar e incorporar la configuración actual de Proxmox.", "The restored service did not pass its health check": "El servicio restaurado no superó su comprobación de estado", "The restored service stopped; the recovery is not confirmed": "El servicio restaurado se detuvo; la recuperación no se confirma", "The reviewed Tandoor stack does not require a privileged LXC.": "La pila de Tandoor revisada no necesita un LXC privilegiado.", @@ -6657,6 +6699,7 @@ "The selected VM": "La máquina virtual seleccionada", "The selected VM is running.": "La VM seleccionada se está ejecutando.", "The selected base folder does not exist and could not be created:": "La carpeta base seleccionada no existe y no se pudo crear:", + "The selected bridge does not exist": "El puente seleccionado no existe", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "El bridge seleccionado no tiene subred IPv4 para el cortafuegos del monitor del host", "The selected configuration needs to start the LXC during the installation": "La configuración seleccionada debe iniciar el LXC durante la instalación", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "El contenedor seleccionado no tiene privilegios. Se requiere un contenedor privilegiado para el passthrough directo de dispositivos.", @@ -6679,6 +6722,7 @@ "The service builds its own image; only a published image can be installed": "El servicio construye su propia imagen; solo se puede instalar una imagen publicada", "The service declares no image:": "El servicio no declara imagen:", "The setting has no final value:": "El ajuste no tiene valor final:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "No se han podido revertir todos los ajustes. Revisa el contenedor y la modificación guardada antes de continuar.", "The share already exists in smb.conf:": "El recurso compartido ya existe en smb.conf:", "The shared destination is not a directory": "El destino compartido no es un directorio", "The shared directory points to a protected host path": "El directorio compartido apunta a una ruta de host protegida", @@ -6746,18 +6790,23 @@ "These Proxmox resources were added outside ProxMenux:": "Se añadieron estos recursos en Proxmox fuera de ProxMenux:", "These VMIDs are not free:": "Estos VMID no están libres:", "These are the changes that will be made": "Estos son los cambios que se harán", + "These changes were made in Proxmox outside ProxMenux:": "Estos cambios se hicieron en Proxmox fuera de ProxMenux:", "These container mount paths have an extension-like suffix:": "Estas rutas de montaje del contenedor tienen un sufijo parecido a una extensión:", "These containers were restored from a backup and this host has no record of their OCI application:": "Estos contenedores se restauraron desde un backup y este host no tiene el registro de su aplicación OCI:", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Estos dispositivos comparten el grupo IOMMU de la GPU seleccionada y se asignarán a la VM junto con ella:", "These host backups run with this job and expect its storage:": "Estos backups del host se ejecutan con este trabajo y esperan su almacenamiento:", "These interface configurations will be removed": "Estas configuraciones de interfaz serán eliminadas", "These manual changes cannot be adopted safely:": "Estos cambios manuales no se pueden incorporar con seguridad:", "These paths will not be restored live and will be extracted for manual recovery.": "Estas rutas no se restaurarán en vivo y se extraerán para su recuperación manual.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Estos ajustes requieren una adaptación específica de la aplicación. No se ha descargado la imagen ni se ha recreado el contenedor.", "These values are asked during the installation:": "Estos valores se preguntan durante la instalación:", "They are described in the image documentation:": "Están descritos en la documentación de la imagen:", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Se añadirán al oci-compose antes de continuar. No se importarán recursos incompatibles o modificados.", + "They will be saved in oci-compose before continuing.": "Se guardarán en el oci-compose antes de continuar.", "This CIFS share is mounted with restrictive permissions.": "Este recurso compartido CIFS está montado con permisos restrictivos.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Esta GPU se considera incompatible con el passthrough de GPU a una VM en ProxMenux.", "This NFS share is fully restricted — even the host root cannot write to it.": "Este recurso compartido NFS está completamente restringido: ni siquiera la raíz del host puede escribir en él.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Este Proxmox Backup Server no tiene una suscripción activa, ¿cambiar al repositorio sin suscripción? Se deshabilitará la fuente Enterprise de PBS, que no es accesible.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Esta máquina virtual requiere pasos de instalación adicionales; consulte la guía de instalación en:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Esta cuenta es de {registry}. Escribe una imagen de ese registro.", "This action will:": "Esta acción:", @@ -6833,7 +6882,7 @@ "This process changes file ownership inside the container": "Este proceso cambia la propiedad del archivo dentro del contenedor", "This profile only supports directory bind mounts": "Este perfil solo admite bind mounts de directorio", "This profile requires a privileged LXC, which reduces isolation from the host.": "Este perfil requiere un LXC privilegiado, lo que reduce el aislamiento respecto al host.", - "This release channel is already active.": "Este canal de release ya está activo.", + "This release channel is already active.": "Este canal de versiones ya está activo.", "This removes the 'unprivileged: 1' line from the config": "Esto elimina la línea 'sin privilegios: 1' de la configuración", "This removes the storage from Proxmox. The iSCSI target is not affected.": "Esto elimina el almacenamiento de Proxmox. El destino iSCSI no se ve afectado.", "This replaces the currently installed keyfile.": "Esto reemplaza el archivo de claves instalado actualmente.", @@ -6841,6 +6890,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Este script no se puede ejecutar desde la terminal web de Proxmox.", "This script must be run as root.": "Este script debe ejecutarse como root.", "This script must be run on a Proxmox host.": "Este script debe ejecutarse en un host Proxmox.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Este script aplicará las siguientes optimizaciones y ajustes avanzados a su Proxmox Backup Server", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Este script aplicará las siguientes optimizaciones y ajustes avanzados a su servidor Proxmox VE", "This script will update your Proxmox VE system with advanced options:": "Este script actualizará su sistema Proxmox VE con opciones avanzadas:", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Esta sesión se ejecuta en el terminal del Monitor. Ejecutarlo desde aquí cortaría la conexión a mitad de la instalación y dejaría el cambio en un estado roto.", @@ -6972,6 +7022,7 @@ "Unable to identify the root filesystem. Aborting.": "No se pudo identificar el sistema de archivos raíz. Se cancela.", "Unable to resolve system ZFS pool disks. Aborting.": "No se pudieron resolver los discos del pool ZFS del sistema. Se cancela.", "Unable to resolve system disk topology. Aborting.": "No se pudo resolver la topología de discos del sistema. Se cancela.", + "Unapplied changes": "Cambios sin aplicar", "Understand the security implications of privileged containers": "Comprender las implicaciones de seguridad de los contenedores privilegiados", "Unexpected Proxmox inventory; recovery blocked": "Inventario Proxmox inesperado; recuperación bloqueada", "Unexpected formatting directory in the new Valkey volume": "Directorio de formato inesperado en el nuevo volumen Valkey", @@ -7087,6 +7138,7 @@ "Unsupported mount type": "Tipo de montaje no compatible", "Unsupported mount type:": "Tipo de montaje no compatible:", "Unsupported native device type:": "Tipo de dispositivo nativo sin soporte:", + "Unsupported network settings": "Ajustes de red no compatibles", "Unsupported operation": "Operación sin soporte", "Unsupported or unknown Proxmox version; no repository changed.": "Versión de Proxmox no compatible o desconocida; no se ha modificado ningún repositorio.", "Unsupported output format:": "Formato de salida no admitido:", @@ -7158,13 +7210,14 @@ "Updating kernel panic configuration...": "Actualizando la configuración de pánico del kernel...", "Updating package lists...": "Actualizando listas de paquetes...", "Updating pve-enterprise.list (if present) to Trixie...": "Actualizando pve-enterprise.list (si está presente) a Trixie...", - "Updating release channel configuration...": "Actualizando la configuración del canal de release...", + "Updating release channel configuration...": "Actualizando la configuración del canal de versiones...", "Updating the copy of the record...": "Actualizando la copia del registro...", "Upgrade Ceph to compatible version": "Actualice Ceph a una versión compatible", "Upgrade PVE 8 to PVE 9": "Actualizar PVE 8 a PVE 9", "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Asistente de actualización: Proxmox VE 8 → 9 (Trixie)", "Upgrade canceled by user": "Actualización cancelada por el usuario", "Upgrade one node at a time": "Actualice un nodo a la vez", + "Upgrade the system, offering the": "Actualizar el sistema, ofreciendo el", "Upgrade to Proxmox VE 9": "Actualice a Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "Actualizando las utilidades de NVIDIA (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "¿Cargar una copia cifrada de la clave a PBS para poder recuperarla en un host reinstalado con solo una frase de contraseña?", @@ -7603,6 +7656,7 @@ "active VF(s)": "FV activas", "active VFs": "FV activas", "active Virtual Functions. Changing its driver binding would destroy every VF.": "Funciones virtuales activas. Cambiar la vinculación del controlador destruiría todos los VF.", + "added in Proxmox": "añadida en Proxmox", "after copy; source tree is incomplete.": "después de la copia; El árbol fuente está incompleto.", "after update.": "después de la actualización.", "all guests": "todas las VM y CT", @@ -7922,7 +7976,7 @@ "partition(s). Partition table preserved.": "partición(es). Se conserva la tabla de particiones.", "pasted Compose file": "archivo Compose pegado", "paths for next boot (/etc/pve, guests, drivers, ...)": "rutas para el próximo arranque (/etc/pve, VM y CT, controladores, ...)", - "pct exec authorization failed": "error en la autorización ejecutiva de PCT", + "pct exec authorization failed": "Ha fallado la autorización de pct exec", "pct push failed. Check log:": "pct push falló. Revise el log:", "pending (reboot required to enumerate full group)": "pendiente (es necesario reiniciar para enumerar el grupo completo)", "phpMyAdmin is installed with arbitrary server connections enabled: the login page has a Server field where the address of the MySQL or MariaDB server is entered, together with its user and password.": "phpMyAdmin se instala con conexiones de servidor arbitrarias habilitadas: la página de inicio de sesión tiene un campo Server donde se introduce la dirección del servidor MySQL o MariaDB, junto con su usuario y contraseña.", @@ -7946,8 +8000,8 @@ "pvesm command not found. This should not happen on Proxmox.": "Comando pvesm no encontrado. Esto no debería suceder en Proxmox.", "pvesm not available.": "pvesm no disponible.", "pvesm not found.": "pvesm no encontrado.", - "pvesm path failed, trying manual detection...": "La ruta pvesm falló, intentando la detección manual...", - "pvesm status failed": "el estado de pvesm falló", + "pvesm path failed, trying manual detection...": "Ha fallado pvesm path; intentando la detección manual...", + "pvesm status failed": "Ha fallado pvesm status", "pyLoad is a Free and Open Source download manager written in Python and designed to be extremely lightweight, easily extensible and fully manageable via web.": "pyLoad es un gestor de descargas libre y de código abierto escrito en Python, diseñado para ser muy ligero, fácil de ampliar y totalmente gestionable por web.", "pyLoad web interface": "Interfaz web de pyLoad", "qBittorrent WebUI password (user: admin)": "contraseña de qBittorrent WebUI (usuario: admin)", @@ -7969,6 +8023,7 @@ "remapped users": "usuarios reasignados", "remove the (now-empty) directory if possible": "elimine el directorio (ahora vacío) si es posible", "removed from Proxmox": "eliminado de Proxmox", + "removed in Proxmox": "eliminada en Proxmox", "removed successfully from Proxmox.": "eliminado correctamente de Proxmox.", "requires running the official installer, which restarts the Monitor service.": "requiere ejecutar el instalador oficial, que reinicia el servicio del Monitor.", "requires the package": "requiere el paquete", @@ -8006,7 +8061,7 @@ "smbclient command is not working properly.": "El comando smbclient no funciona correctamente.", "smbclient command not found after installation.": "El comando smbclient no se encuentra después de la instalación.", "sources.list update skipped (no change)": "Actualización de sources.list omitida (sin cambios)", - "sources.list updated to Trixie": "fuentes.lista actualizada a Trixie", + "sources.list updated to Trixie": "sources.list actualizado a Trixie", "ssh-keygen failed. Cannot create a new SSH key.": "ssh-keygen falló. No se puede crear una nueva clave SSH.", "stale entry/entries for interfaces no longer present": "entrada(s) obsoleta(s) de interfaces que ya no existen", "standard performance": "rendimiento estándar", @@ -8072,6 +8127,7 @@ "using": "usando", "using direct path (not guaranteed to survive reboots).": "utilizando una ruta directa (no se garantiza que sobreviva a los reinicios).", "vCPUs:": "vCPU:", + "value changed in Proxmox": "valor cambiado en Proxmox", "vfio-pci IDs configured": "ID de vfio-pci configurados", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "ID de vfio-pci en /etc/modprobe.d/vfio.conf", "video": "vídeo", @@ -8084,6 +8140,7 @@ "was": "era", "was installed, but the kernel reports:": "se instaló, pero el kernel informa:", "when finished": "al terminar", + "when the server has no subscription (you will be asked)": "cuando el servidor no tiene suscripción (se le preguntará)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "volverá a vincular la GPU a vfio-pci en el próximo reinicio, rompiendo el controlador que está a punto de instalarse.", "wipefs failed on": "los borrados fallaron", "with": "con", diff --git a/lang/fr.json b/lang/fr.json index 2d3be751..40682cd3 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (chemin vzdump par défaut de Proxmox)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (Proxmox par défaut)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = bit collant + rwx pour tous. Aucun groupe partagé n’est nécessaire.", + "====== PBS UPDATE COMPLETED ======": "====== MISE À JOUR PBS TERMINÉE ======", "====== PVE UPDATE COMPLETED ======": "====== MISE À JOUR PVE TERMINÉE ======", ">> Create a new backup job <<": ">> Créer une nouvelle tâche de sauvegarde <<", ">> Save and return <<": ">> Enregistrer et retourner <<", @@ -87,6 +88,7 @@ "A multi-container application is not recreated: its containers are updated together.": "Une application multi-conteneurs n'est pas recréée : ses conteneurs sont mis à jour ensemble.", "A new ProxMenux version is available:": "Une nouvelle version de ProxMenux est disponible :", "A new kernel is staged for the next boot:": "Un nouveau noyau est préparé pour le prochain démarrage :", + "A newer kernel is installed. Reboot before the next backup window to use it.": "Un noyau plus récent est installé. Redémarrez avant la prochaine fenêtre de sauvegarde pour l'utiliser.", "A newer version is available:": "Une version plus récente est disponible :", "A pending operation exists for": "Un opera en attente existe pour", "A pending stack assembly already exists; it is not overwritten": "Une pile en attente existe déjà; elle n'est pas écrasée", @@ -169,6 +171,7 @@ "APT language configuration updated": "Configuration du langage APT mise à jour", "APT language downloads restored": "Téléchargements de langues APT restaurés", "APT package lists updated": "Listes de packages APT mises à jour", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "L’actualisation APT a échoué. Vérifiez l'abonnement PBS et les référentiels dans Administration > Référentiels. Enregistrer:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "Échec de l'actualisation APT. Vérifiez l'abonnement et les dépôts de PBS dans son interface native ; aucune source n'a été modifiée.", "APT update failed. Check repository access; NVIDIA installation stopped.": "La mise à jour APT a échoué. Vérifiez l'accès au référentiel ; l'installation de NVIDIA s'est arrêtée.", "Aborted": "Avorté", @@ -298,7 +301,7 @@ "Address of the database server (mysql and pgsql only)": "Adresse du serveur de base de données (mysql et pgsql uniquement)", "Address of the oci-compose file": "Adresse du fichier oci-compose", "Address to reach Pydio Cells (https://domain or https://IP:8080)": "Adresse pour atteindre Pydio Cells (https://domaine ou https://IP:8080)", - "Address used to reach wallabag (http://IP or https://wallabag.example.com)": "Adresse utilisée pour atteindre wallabag (http://IP ou https://wallabag.exemple.com)", + "Address used to reach wallabag (http://IP or https://wallabag.example.com)": "Adresse d’accès à wallabag (http://IP ou https://wallabag.example.com)", "Addresses to update: ipv4, ipv6 or both (uses an external service)": "Adresses à mettre à jour: ipv4, ipv6 ou les deux (utilise un service externe)", "Adguardhome Sync web interface": "Interface web Adguardhome Sync", "Adguardhome-sync is a tool to synchronize AdGuardHome config to replica instances.": "Adguardhome-sync est un outil pour synchroniser la configuration AdGuardHome pour reproduire des instances.", @@ -319,7 +322,7 @@ "After confirming, you will be asked to choose the NVIDIA driver version to install.": "Après confirmation, il vous sera demandé de choisir la version du pilote NVIDIA à installer.", "After detailed analysis, no changes are needed.": "Après une analyse détaillée, aucune modification n’est nécessaire.", "After detailed analysis, no cleanup is needed.": "Après une analyse détaillée, aucun nettoyage n’est nécessaire.", - "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Après vous être connecté, exécutez : ip a pour obtenir l'adresse IP.\nEnsuite, saisissez cette adresse IP dans votre navigateur Web comme ceci :\n http://ADRESSE_IP\n\nCela ouvrira le tableau de bord Umbral OS.", + "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Après la connexion, exécutez ip a pour obtenir l’adresse IP.\nSaisissez ensuite cette adresse dans votre navigateur sous cette forme :\n http://IP_ADDRESS\n\nLe tableau de bord Umbral OS s’ouvrira.", "After pasting on the server, ensure the file is chmod 600 and owned by": "Après avoir collé sur le serveur, assurez-vous que le fichier est chmod 600 et appartient à", "After reboot the system will be fully accessible (SSH, web UI, login), but the following components will be reinstalled in BACKGROUND — until they finish, commands like nvidia-smi may not yet be available:": "Après le redémarrage, le système sera entièrement accessible (SSH, interface utilisateur Web, connexion), mais les composants suivants seront réinstallés en ARRIÈRE-PLAN — jusqu'à ce qu'ils soient terminés, des commandes comme nvidia-smi peuvent ne pas encore être disponibles :", "After reboot, these components will reinstall in background:": "Après le redémarrage, ces composants seront réinstallés en arrière-plan :", @@ -443,10 +446,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "Appliquer maintenant la mise à niveau des paquets examinée ? Des services peuvent redémarrer et les paquets indiqués pour suppression seront supprimés.", "Apply these changes?": "Appliquer ces modifications ?", "Applying AMD-specific fixes...": "Application de correctifs spécifiques à AMD...", + "Applying CPU and RAM changes without rebuilding the container...": "Application des modifications de CPU et de RAM sans recréer le conteneur...", "Applying Changes": "Application des modifications", "Applying Controller/NVMe passthrough to VM": "Application du relais Controller/NVMe à la VM", "Applying balanced memory optimization settings...": "Application des paramètres d'optimisation de la mémoire équilibrée...", "Applying changes safely...": "Appliquer les modifications en toute sécurité...", + "Applying container settings without rebuilding the container...": "Application des paramètres sans recréer le conteneur...", "Applying custom restore": "Application d'une restauration personnalisée", "Applying default VM configuration": "Application de la configuration de VM par défaut", "Applying host permissions for unprivileged LXC bind-mounts...": "Application des autorisations d'hôte pour les montages de liaison LXC non privilégiés...", @@ -538,6 +543,7 @@ "Available Borg targets:": "Cibles Borg disponibles :", "Available Disks on Host": "Disques disponibles sur l'hôte", "Available ISO Images": "Images ISO disponibles", + "Available PBS Version": "version PBS disponible", "Available PBS repositories:": "Dépôts PBS disponibles :", "Available PVE Version": "Version PVE disponible", "Available Shares:": "Actions disponibles :", @@ -749,6 +755,7 @@ "Cannot verify the reused disk:": "Impossible de vérifier le disque réutilisé :", "Cannot write the image archive to {storage}. Check storage access and mount status.": "Impossible d'écrire l'archive d'images sur {storage}. Vérifiez l'accès au stockage et l'état du montage.", "Capabilities cannot be kept and all dropped at the same time": "Les capacités ne peuvent être conservées et toutes abandonnées en même temps", + "Catalog options": "Options du catalogue", "Category": "Catégorie", "Caution: Maximum mode generates more heat.": "Attention : le mode maximum génère plus de chaleur.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Vérification Ceph ignorée par l'indicateur utilisateur (--ignore-ceph-check)", @@ -883,6 +890,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Choisissez un calendrier (syntaxe d'événement de calendrier systemd) :", "Choose a script to execute:": "Choisissez un script à exécuter :", "Choose a server:": "Choisissez un serveur :", + "Choose a setting. Changes are applied only after the final review.": "Choisissez un réglage. Les modifications ne sont appliquées qu’après la vérification finale.", "Choose a share to mount:": "Choisissez un partage à monter :", "Choose a specific supported GPU or USB node": "Choisissez un GPU ou un nœud USB pris en charge spécifique", "Choose action for this controller/NVMe:": "Choisissez une action pour ce contrôleur/NVMe :", @@ -1520,6 +1528,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "L'inventaire actuel de NVIDIA résolu: un rafraîchissement est required", "Current NVIDIA inventory resolved: no refresh is required": "L'inventaire actuel de NVIDIA résolu: aucun rafraîchissement n'est required", "Current Network Configuration": "Configuration réseau actuelle", + "Current PBS Version": "version actuelle de PBS", "Current PVE Version": "Version PVE actuelle", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Les scripts hôtes ProxMenux actuels enregistrent les partages distants en tant que stockages Proxmox à l'aide de pvesm.", "Current ProxMenux version:": "Version actuelle de ProxMenux :", @@ -1634,8 +1643,8 @@ "Default Proxmox logo applied": "Logo Proxmox par défaut appliqué", "Default is none (video-only passthrough). Use SPACE to toggle selections.": "La valeur par défaut est aucune (passthrough vidéo uniquement). Utilisez ESPACE pour basculer les sélections.", "Default journald configuration restored": "Configuration journald par défaut restaurée", - "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "L'emplacement par défaut est /mnt/. Le partage sera monté ici sur l'hôte avec des autorisations ouvertes afin qu'un LXC non privilégié puisse y effectuer un montage en liaison et y écrire. Pour l'accès LXC, montez en liaison ce chemin avec le gestionnaire de montage LXC.", - "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "L'emplacement par défaut est /mnt/. Le partage sera monté ici sur l'hôte. Utilisez ce chemin dans /etc/fstab. Pour l'accès LXC, montez en liaison ce chemin avec le gestionnaire de montage LXC.", + "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "L’emplacement par défaut est /mnt/. Le partage sera monté ici sur l’hôte avec des permissions ouvertes, afin qu’un LXC non privilégié puisse le monter par liaison et y écrire. Utilisez LXC Mount Manager pour créer ce montage dans le LXC.", + "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "L’emplacement par défaut est /mnt/. Le partage sera monté ici sur l’hôte. Utilisez ce chemin dans /etc/fstab. Pour y accéder depuis un LXC, créez un montage lié de ce chemin avec LXC Mount Manager.", "Default login": "Connexion par défaut", "Default options": "Options par défaut", "Default options read/write": "Options par défaut lecture/écriture", @@ -1685,6 +1694,7 @@ "Detected Coral hardware:": "Matériel Coral détecté :", "Detected GPU(s):": "GPU(s) détecté(s) :", "Detected Intel CPU": "Processeur Intel détecté", + "Detected Proxmox Backup Server": "Proxmox Backup Server détecté", "Detected Proxmox VE": "Proxmox VE détecté", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Proxmox VE 8.x détecté - Installation de Ceph Squid pour Debian", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Proxmox VE 9.x détecté - Installation de Ceph Squid pour Debian Trixie", @@ -1766,6 +1776,9 @@ "Disabling and stopping rpcbind service and socket...": "Désactivation et arrêt du service et du socket rpcbind...", "Disabling non-free firmware warnings...": "Désactivation des avertissements du firmware non libre...", "Disabling portmapper/rpcbind for security...": "Désactivation de portmapper/rpcbind pour la sécurité...", + "Discard changes": "Abandonner les modifications", + "Discard unapplied changes?": "Abandonner les modifications non appliquées ?", + "Discover NFS exports on a server": "Découvrir les exports NFS d’un serveur", "Discover targets on portal:": "Découvrez les cibles sur le portail :", "Discovering iSCSI targets...": "Découverte des cibles iSCSI...", "Discovery successful": "Découverte réussie", @@ -1845,6 +1858,7 @@ "Do you want to proceed with the system update?": "Voulez-vous procéder à la mise à jour du système ?", "Do you want to proceed?": "Voulez-vous continuer ?", "Do you want to reboot now?": "Voulez-vous redémarrer maintenant ?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "Voulez-vous supprimer la bannière d'abonnement Proxmox de l'interface web de Proxmox Backup Server ?", "Do you want to remove the stale entry from vfio.conf and continue?": "Voulez-vous supprimer l'entrée obsolète de vfio.conf et continuer ?", "Do you want to remove the stale entry from vfio.conf?": "Voulez-vous supprimer l'entrée obsolète de vfio.conf ?", "Do you want to restart the network service now to apply changes?": "Voulez-vous redémarrer le service réseau maintenant pour appliquer les modifications ?", @@ -1907,7 +1921,7 @@ "Dry run completed; the container and the mounts were not changed.": "Le parcours à sec est terminé; le conteneur et les supports n'ont pas été changés.", "DuckDNS subdomain without .duckdns.org (comma separated for several)": "DuckDNS sous-domaine sans .duckdns.org (comma séparé pour plusieurs)", "DuckDNS token from your account at duckdns.org": "Jeton DuckDNS de votre compte sur duckdns.org", - "DuckDNS updates the subdomain every 5 minutes. Without UPDATE_IP, DuckDNS itself detects the public IPv4 address of the request.": "DuckDNS met à jour le sous-domaine toutes les 5 minutes. Sans mise à jour, DuckDNS détecte lui-même l'adresse IPv4 publique de la requête.", + "DuckDNS updates the subdomain every 5 minutes. Without UPDATE_IP, DuckDNS itself detects the public IPv4 address of the request.": "DuckDNS met à jour le sous-domaine toutes les 5 minutes. Sans UPDATE_IP, DuckDNS détecte lui-même l’adresse IPv4 publique de la requête.", "DuckStation is a PS1 Emulator aiming for the best accuracy and game support.": "DuckStation est un émulateur PS1 visant le meilleur accuracy et le meilleur support de jeu.", "Duckdns is a free service which will point a DNS (sub domains of duckdns.org) to an IP of your choice. The service is completely free, and doesn't require reactivation or forum posts to maintain its existence.": "Duckdns est un service gratuit qui dirigera un DNS (sous-domaines de duckdns.org) vers une IP de votre choix. Le service est entièrement gratuit, et ne require réactivation ou forum posts pour maintenir son existence.", "Dumping AMD GPU ROM BIOS via sysfs...": "Vidage du BIOS ROM GPU AMD via sysfs...", @@ -2166,11 +2180,11 @@ "Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Exemple : cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak", "Example: daily or Mon..Fri 03:00": "Exemple : tous les jours ou du lundi au vendredi à 03h00", "Example: mkdir -p /tmp/lxc_convert_114": "Exemple : mkdir -p /tmp/lxc_convert_114", - "Example: pct config 110 | grep unprivileged": "Exemple : configuration PCT 110 | grep sans privilège", - "Example: pct stop 110": "Exemple : PCT stop 110", - "Example: pct stop 114": "Exemple : PCT arrêt 114", + "Example: pct config 110 | grep unprivileged": "Exemple : pct config 110 | grep unprivileged", + "Example: pct stop 110": "Exemple : pct stop 110", + "Example: pct stop 114": "Exemple : pct stop 114", "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup": "Exemple : pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup", - "Example: pvesm path local-lvm:vm-114-disk-0": "Exemple : chemin pvesm local-lvm:vm-114-disk-0", + "Example: pvesm path local-lvm:vm-114-disk-0": "Exemple : pvesm path local-lvm:vm-114-disk-0", "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf": "Exemple : sed -i '/^unprivileged : 1/d' /etc/pve/lxc/110.conf", "Examples:": "Exemples :", "Execute destructive rollback?": "Exécuter une restauration destructrice ?", @@ -2270,6 +2284,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "Échec du clonage du référentiel log2ram. Vérifiez /tmp/log2ram_install.log", "Failed to configure EFI disk": "Échec de la configuration du disque EFI", "Failed to configure IOMMU automatically.": "Échec de la configuration automatique d'IOMMU.", + "Failed to configure Proxmox Backup Server repositories": "Échec de la configuration des dépôts de Proxmox Backup Server", "Failed to configure Proxmox repositories": "Échec de la configuration des référentiels Proxmox", "Failed to configure TPM device in VM": "Échec de la configuration du périphérique TPM dans la VM", "Failed to configure repositories.": "Échec de la configuration des référentiels.", @@ -2481,7 +2496,7 @@ "For a consistent export, the VM should be stopped.": "Pour une exportation cohérente, la VM doit être arrêtée.", "For any question, issue or bug report, please use the official repository": "Pour toute question, problème ou rapport de bug, veuillez utiliser le référentiel officiel", "For dedicated GPUs, FLR is required by this policy to reduce VM": "Pour les GPU dédiés, FLR est requis par cette stratégie pour réduire les VM", - "For example: pct enter 101": "Par exemple : PCT, entrez 101", + "For example: pct enter 101": "Par exemple : pct enter 101", "For guest shares add: --options guest": "Pour les partages invités, ajoutez : --options invité", "For security reasons, the system cannot format it.": "Pour des raisons de sécurité, le système ne peut pas le formater.", "For unprivileged containers, use instead:": "Pour les conteneurs non privilégiés, utilisez plutôt :", @@ -2893,8 +2908,8 @@ "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.": "Si les notifications sont activées (Telegram/Discord/ntfy/...), vous recevrez un message « Restauration de l'hôte terminée » lorsque toutes les tâches en arrière-plan seront terminées.", "If passthrough fails on Windows: install RadeonResetBugFix.": "Si le relais échoue sous Windows : installez RadeonResetBugFix.", "If path not accessible (ZFS/BTRFS):": "Si chemin non accessible (ZFS/BTRFS) :", - "If pvesm path returned a DEVICE (LVM):": "Si le chemin pvesm a renvoyé un DEVICE (LVM) :", - "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Si le chemin pvesm renvoie un RÉPERTOIRE (ZFS/BTRFS) :", + "If pvesm path returned a DEVICE (LVM):": "Si pvesm path a renvoyé un PÉRIPHÉRIQUE (LVM) :", + "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Si pvesm path a renvoyé un RÉPERTOIRE (ZFS/BTRFS) :", "If repositories error:": "Si erreur de référentiels :", "If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Si le conteneur a déjà été modifié, sa sauvegarde native précédente est restaurée. Les répertoires hôtes partagés ne sont pas rétablis. Les disques de la tentative ayant échoué sont supprimés une fois le nettoyage réussi.", "If the host freezes, remove hostpci entries from": "Si l'hôte se bloque, supprimez les entrées hostpci de", @@ -2977,6 +2992,7 @@ "Inaccessible device during VM startup": "Périphérique inaccessible lors du démarrage de la VM", "Inactive": "Inactif", "Inactive (entry in fstab, not currently mounted)": "Inactif (entrée dans fstab, non monté actuellement)", + "Include these changes in oci-compose?": "Inclure ces modifications dans oci-compose ?", "Include these resources in oci-compose?": "inclure ces ressources dans oci-compose ?", "Includes /etc/network (may drop SSH immediately)": "Inclut /etc/network (peut supprimer SSH immédiatement)", "Includes /etc/zfs": "Inclut /etc/zfs", @@ -3033,7 +3049,7 @@ "Initiator IQN is authorised on the target": "L'IQN initiateur est autorisé sur la cible", "Initiator IQN:": "IQN initiateur :", "Inkscape is professional quality vector graphics software which runs on Linux, Mac OS X and Windows desktop computers.": "Inkscape est un logiciel graphique vectoriel de qualité professionnelle qui fonctionne sur les ordinateurs de bureau Linux, Mac OS X et Windows.", - "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "À l'intérieur du LXC, créez l'administrateur: console kimai:utilisateur:créez VOTRE USERNAME VOTRE ROLE_SUPER_ADMIN", + "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "Dans le LXC, créez l’administrateur : console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN", "Inspect disks before any action": "Inspecter les disques avant toute action", "Inspect host device nodes": "Inspecter les nœuds du périphérique hôte", "Inspect passthrough/kernel events": "Inspecter les événements de relais/noyau", @@ -3210,9 +3226,11 @@ "Interrupted operation:": "Interruption du opera:", "Interrupted stack operation found": "Pile interrompue operation trouvée", "Invalid 'proxmox-ve' candidate (not 9.x or none). Please verify your repository configuration and network, then retry.": "Candidat 'proxmox-ve' invalide (pas 9.x ou aucun). Veuillez vérifier la configuration et le réseau de votre référentiel, puis réessayez.", - "Invalid ALLOWED_HOSTS value": "Valeur HÔTES INDÉSIRABLES", + "Invalid ALLOWED_HOSTS value": "Valeur ALLOWED_HOSTS invalide", "Invalid Home Assistant OS check timeout": "Non valide Home Assistant OS check timeout", "Invalid ID": "Pièce d'identité invalide", + "Invalid IPv4 address": "Adresse IPv4 non valide", + "Invalid IPv4 gateway": "Passerelle IPv4 non valide", "Invalid Intel render path": "Chemin de rendu Intel incorrect", "Invalid Jellyfin path:": "Chemin Jellyfin non valide:", "Invalid MAC address:": "Adresse MAC non valide:", @@ -3245,6 +3263,7 @@ "Invalid administrator email": "Courriel administrateur non valide", "Invalid administrator user name": "Nom d'utilisateur non valide", "Invalid base VMID": "VMID de base non valide", + "Invalid bridge": "Pont réseau non valide", "Invalid check package:": "Dossier de contrôle non valide:", "Invalid configuration path:": "Chemin de configuration non valide & #160;:", "Invalid consume/export volumes": "Volumes de consommation/d'exportation non valides", @@ -3308,6 +3327,7 @@ "Invalid remote name": "Nom distant non valide", "Invalid remote path": "Voie distante non valide", "Invalid repair version:": "Version de réparation non valide:", + "Invalid resource value": "Valeur de ressource non valide", "Invalid resources": "Ressources non valides", "Invalid restored volume path": "Voie de volume restaurée non valide", "Invalid running state": "État de fonctionnement non valide", @@ -3326,6 +3346,7 @@ "Invalid stack members": "Membres de pile non valides", "Invalid stack name": "Nom de la pile non valide", "Invalid stack operation": "Pile non valide operation", + "Invalid storage": "Stockage non valide", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID de stockage invalide. Utilisez uniquement des lettres, des chiffres, des traits d'union et des traits de soulignement.", "Invalid storage name": "Nom de stockage invalide", "Invalid suite application": "Demande de suite non valide", @@ -3440,7 +3461,7 @@ "Keyfile removed.": "fichier clé supprimé.", "Keyrings method failed; trying apt-key fallback": "La méthode des porte-clés a échoué ; essayer la solution de secours apt-key", "KiCad - A Cross Platform and Open Source Electronics Design Automation Suite.": "KiCad - Une plateforme croisée et une suite d'automatisation de conception électronique ouverte.", - "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai n'a pas de compte par défaut. Saisissez le conteneur avec : pct entre {main vmid}", + "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai n’a pas de compte par défaut. Ouvrez un shell dans le conteneur avec : pct enter {main_vmid}", "Kimai is a professional grade time-tracking application, free and open-source.": "Kimai est une application professionnelle de suivi du temps, libre et open-source.", "Kometa is a powerful tool designed to give you complete control over your media libraries. With Kometa, you can take your customization to the next level, with granular control over metadata, collections, overlays, and much more.": "Kometa est un outil puissant conçu pour vous donner un contrôle complet sur vos bibliothèques multimédias. Avec Kometa, vous pouvez porter votre personnalisation au niveau suivant, avec un contrôle granulaire sur les métadonnées, les collections, les superpositions et bien plus encore.", "Kometa reads its configuration from /config/config.yml and the container only ships /config/config.yml.template. Copy the template to config.yml, fill in the required Plex and TMDb connections, then restart the container.": "Kometa lit sa configuration à partir de /config/config.yml et le conteneur ne livre que /config/config.yml.template. Copiez le modèle pour config.yml, remplissez les connexions required Plex et TMDb, puis redémarrez le conteneur.", @@ -3492,7 +3513,7 @@ "Leave empty to keep every backup (not recommended).": "Laissez vide pour conserver chaque sauvegarde (non recommandé).", "Leave empty to use the retention of the storage.": "Laisser vide pour utiliser la rétention du stockage.", "Legacy PVE 8 .list files commented or not present": "Fichiers .list hérités PVE 8 commentés ou non présents", - "Legacy ceph.list commented or not present": "Ceph.list hérité commenté ou non présent", + "Legacy ceph.list commented or not present": "Ancien fichier ceph.list commenté ou absent", "Legacy gasket-dkms cleanup could not be verified as complete.": "Impossible de confirmer que le nettoyage de l'ancien paquet gasket-dkms est terminé.", "Legacy gasket-dkms detected": "Ancien paquet gasket-dkms détecté", "Legacy network tools (e.g., ifconfig)": "Outils réseau hérités (par exemple, ifconfig)", @@ -4157,6 +4178,7 @@ "No active Proxmox storage accepts OCI template archives.": "Aucun stockage Proxmox actif n'accepte les archives de modèles OCI.", "No active exports or showmount not available": "Aucune exportation active ou showmount non disponible", "No active session": "Aucune session active", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "Aucun abonnement actif et aucun référentiel PBS utilisable. Le mode non interactif ne peut pas modifier les sources APT ;configurez-les dans Administration > Dépôts.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "Aucun abonnement actif et aucun référentiel PVE utilisable. Le mode non interactif ne peut pas modifier les sources APT ; configurez-les dans Nœud > Mises à jour > Dépôts.", "No additional GPU can be added.": "Aucun GPU supplémentaire ne peut être ajouté.", "No additional device needs to be added.": "Aucun appareil supplémentaire ne doit être ajouté.", @@ -4242,7 +4264,7 @@ "No jobs": "Aucun emploi", "No language selected.": "Aucune langue sélectionnée.", "No local disk storage configured.": "Aucun stockage sur disque local configuré.", - "No main sources.list present (skipped)": "Aucune liste de sources principale présente (ignorée)", + "No main sources.list present (skipped)": "Aucun fichier sources.list principal présent (ignoré)", "No matches for:": "Aucune correspondance pour :", "No matching PVE vzdump jobs were found for the PBS backend.": "Aucune tâche vzdump PVE correspondante n'a été trouvée pour le backend PBS.", "No matching PVE vzdump jobs were found for the local archive backend.": "Aucune tâche vzdump PVE correspondante n'a été trouvée pour le backend d'archive local.", @@ -4261,6 +4283,7 @@ "No password provided.": "Aucun mot de passe fourni.", "No paths available": "Aucun chemin disponible", "No paths selected. Select at least one path.": "Aucun chemin sélectionné. Sélectionnez au moins un chemin.", + "No pending changes": "Aucune modification en attente", "No pending paths to schedule for reboot.": "Aucun chemin en attente pour planifier le redémarrage.", "No permanent NFS mounts configured.": "Aucun montage NFS permanent configuré.", "No permanent Samba mounts configured.": "Aucun montage Samba permanent configuré.", @@ -4402,6 +4425,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM supprimé (les artefacts de marionnettes peuvent nécessiter un nettoyage manuel)", "OVH server detected": "Serveur OVH détecté", "OVH server detection and RTM installation process completed": "Processus de détection du serveur OVH et d'installation de RTM terminé", + "Object detector": "Détecteur d’objets", "Object detector for Frigate": "Détecteur d'objets pour Frigate", "Observer is not responding on port 4357": "L'observateur ne répond pas au port 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian est une application de prise de notes qui vous permet de créer, de lier et d'organiser vos notes sur votre appareil, avec des centaines de plugins et de thèmes pour personnaliser votre workflow. Vous pouvez également publier vos notes en ligne, les accéder hors ligne et les synchroniser en toute sécurité avec le chiffrement de bout en bout.", @@ -4874,12 +4898,17 @@ "Proxmology logo applied": "Logo Proxmologie appliqué", "Proxmox 9 system update allready": "La mise à jour du système Proxmox 9 est déjà terminée", "Proxmox APT repositories configured": "Dépôts Proxmox APT configurés", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Destinations Proxmox Backup Server (PBS)", + "Proxmox Backup Server APT repositories configured": "Dépôts APT de Proxmox Backup Server configurés", + "Proxmox Backup Server Update": "Mise à jour de Proxmox Backup Server", + "Proxmox Backup Server update completed.": "Mise à jour de Proxmox Backup Server terminée.", "Proxmox CIFS Storage Status:": "État de stockage Proxmox CIFS :", "Proxmox NFS Storage Status:": "Statut de stockage Proxmox NFS :", "Proxmox System Update": "Mise à jour du système Proxmox", "Proxmox Update": "Mise à jour Proxmox", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "La mise à jour des paquets de Proxmox VE 8 a échoué. Vérifiez la sortie d’APT avant de réessayer.", "Proxmox VE 8 system update completed successfully": "La mise à jour du système Proxmox VE 8 s'est terminée avec succès", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Guide de mise à niveau manuelle Proxmox VE 8 vers 9", "Proxmox VE 9.x no-subscription repository created": "Création d'un référentiel sans abonnement Proxmox VE 9.x", @@ -4887,6 +4916,7 @@ "Proxmox VE configuration completed.": "Configuration de Proxmox VE terminée.", "Proxmox VE safe update completed": "Mise à jour sécurisée de Proxmox VE terminée", "Proxmox auth logger service created and started": "Service d'enregistrement d'authentification Proxmox créé et démarré", + "Proxmox could not apply the container settings": "Proxmox n’a pas pu appliquer les paramètres du conteneur", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Le référentiel d'entreprise Proxmox peut être manquant ou inaccessible. J'essaie de passer au sans abonnement...", "Proxmox filter configured": "Filtre Proxmox configuré", "Proxmox iSCSI Storage Status:": "État du stockage Proxmox iSCSI :", @@ -5153,6 +5183,8 @@ "Remove saved credentials": "Supprimer les informations d'identification enregistrées", "Remove subscription banner": "Supprimer la bannière d'abonnement", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Supprimer l'application ?Ses disques conteneurs sont supprimés et seule une sauvegarde peut les restaurer.", + "Remove the lock left on a VM by an interrupted task": "Supprimer le verrou laissé sur une VM par une tâche interrompue", + "Remove the lock left on a container by an interrupted task": "Supprimer le verrou laissé sur un conteneur par une tâche interrompue", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Supprimer les informations d'identification enregistrées pour {registry} ?\nLes applications en cours d’exécution continueront de fonctionner.", "Remove the unprivileged flag from configuration:": "Supprimez l'indicateur sans privilèges de la configuration :", "Remove unused packages and their config": "Supprimer les packages inutilisés et leur configuration", @@ -5234,6 +5266,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "Assistant du référentiel indisponible. L'installation de NVIDIA s'est arrêtée.", "Repository policy returned an unexpected result.": "La stratégie du référentiel a renvoyé un résultat inattendu.", "Repository ready.": "Référentiel prêt.", + "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.": "les sources du référentiel ont été modifiées, mais l'actualisation de la liste des packages APT a échoué. Opération arrêtée ;les sources n’ont pas été annulées. Inspectez Administration > Référentiels et réessayez apt-get update avant de continuer.", "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.": "Les sources du référentiel ont été modifiées, mais l'actualisation de la liste des paquets APT a échoué. Opération arrêtée ; les sources n'ont pas été restaurées. Vérifiez Nœud > Mises à jour > Dépôts et relancez apt-get update avant de continuer.", "Repository switch declined; no APT source changed.": "Le changement de référentiel a été refusé ; aucune source APT n'a été modifiée.", "Repository:": "Dépôt:", @@ -5328,6 +5361,7 @@ "Reverting ZFS ARC tuning...": "Annulation du réglage ZFS ARC...", "Reverting pigz wrapper...": "Rétablissement du wrapper pigz...", "Reverting vzdump speed tuning...": "Annulation du réglage de la vitesse de vzdump...", + "Review and apply": "Vérifier et appliquer", "Review external OCI changes": "Examiner les modifications externes de OCI", "Review package upgrade": "Examiner la mise à niveau des paquets", "Review passthrough config files": "Examiner les fichiers de configuration relais", @@ -5787,6 +5821,7 @@ "ShareCoveX panel": "Panneau ShareCoveX", "Shared Directory Ready:": "Répertoire partagé prêt :", "Shared Group": "Groupe partagé", + "Shared directories": "Dossiers partagés", "Shared directory created:": "Répertoire partagé créé & #160;:", "Shared directory for consume and export": "Répertoire partagé pour la consommation et l'exportation", "Shared directory for copy/sync operations": "Répertoire partagé pour copier/sync operations", @@ -5816,6 +5851,7 @@ "Should show pve-manager/9.x.x": "Devrait afficher pve-manager/9.x.x", "Shoutrrr addresses for the notifications (optional)": "Adresses Shoutrrr pour les notifications (facultatif)", "Show All Scripts": "Afficher tous les scripts", + "Show CT users and their UID/GID on the host": "Utilisateurs du CT et leurs UID/GID sur l’hôte", "Show CT users in table format": "Afficher les utilisateurs CT sous forme de tableau", "Show Container Privilege Status": "Afficher l'état des privilèges du conteneur", "Show DNS resolution status": "Afficher l'état de la résolution DNS", @@ -5977,6 +6013,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Démarrez la VM pour commencer l'installation de Windows à partir de l'ISO montée.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Lancer les applications une fois enregistrées ? Répondez Non si les conteneurs d'origine s'exécutent toujours sur un autre hôte : les deux utiliseraient les mêmes adresses.", "Start the converted container:": "Démarrez le conteneur converti :", + "Start the interactive Proxmox package upgrade": "Lancer la mise à niveau interactive des paquets Proxmox", "Start the main system upgrade:": "Démarrez la mise à niveau principale du système :", "Start the stack with Proxmox": "Démarrer la pile avec Proxmox", "Start uploading to PBS — sets a recovery passphrase": "Commencer le téléchargement sur PBS – définit une phrase secrète de récupération", @@ -6255,6 +6292,7 @@ "The Paperless persistent volumes need at least 8 GB": "Les volumes persistants sans papier nécessitent au moins 8 Go", "The PostgreSQL volume needs at least 4 GB": "Le volume PostgreSQL nécessite au moins 4 Go", "The PostgreSQL volume needs at least 8 GB": "Le volume PostgreSQL nécessite au moins 8 Go", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "La mise à jour de Proxmox VE 8 n’a pas abouti. La mise à niveau vers Proxmox VE 9 n’a pas été lancée.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "Le trousseau de clés d'archive Proxmox est manquant ;L'installation de Ceph ne peut pas continuer en toute sécurité", "The Proxmox inventory and the local configurations differ": "L'inventaire Proxmox et les configurations locales diffèrent", "The ROCm image has no support for the AMD GPU of this host:": "L'image ROCm ne prend pas en charge le GPU AMD de cet hôte :", @@ -6325,6 +6363,7 @@ "The build could not be checked beforehand; continuing without that check.": "La build n'a pas pu être vérifiée au préalable ;continuer sans cette vérification.", "The cached image does not match the current digest": "L'image mise en cache ne correspond pas au digest actuel", "The cached image is damaged; it will be downloaded again.": "L'image mise en cache est endommagée ; elle sera à nouveau téléchargée.", + "The changes need the application installer": "Ces modifications nécessitent le programme d’installation de l’application", "The character device does not exist:": "Le dispositif de caractère n'existe pas :", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "La commande demande un mot de passe qui n'est pas repris. Après la création des utilisateurs, l'interface web demande un nom d'utilisateur et un mot de passe.", "The command does not name an image": "La commande ne nomme pas une image", @@ -6444,8 +6483,10 @@ "The host bind source does not exist:": "La source de liaison hôte n'existe pas:", "The host bind source is not a regular file or directory:": "La source de liaison hôte n'est pas un fichier ou un répertoire régulier :", "The host directory changed before it was mounted": "Le répertoire hôte a changé avant son montage", + "The host directory does not exist": "Le répertoire de l’hôte n’existe pas", "The host directory may not be accessible from an unprivileged container.": "Le répertoire hôte peut ne pas être accessible à partir d'un conteneur non privilégié.", "The host has no IPv4 address on the selected bridge": "L'hôte n'a pas d'adresse IPv4 sur le pont sélectionné", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "L’hôte perd l’accès à ces périphériques. Si le groupe contient un contrôleur réseau ou de stockage dont l’hôte a besoin, annulez au résumé.", "The host monitor does not see the real host memory": "Le moniteur hôte ne voit pas la mémoire de l'hôte réel", "The host monitor does not share this host namespace:": "L'écran hôte ne partage pas cet espace de noms d'hôte :", "The host monitor include is unknown or differs from the saved record": "L'include du moniteur d'hôte est inconnu ou diffère de l'enregistrement sauvegardé", @@ -6635,6 +6676,7 @@ "The repair must preserve the image dependencies:": "La réparation doit préserver les dépendances de l'image:", "The requested VMID block is already in use": "Le bloc VMID demandé est déjà utilisé", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "Le profil GPU d'apprentissage automatique demandé ne fonctionne pas; il n'est pas remplacé par CPU", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "Impossible d’annuler entièrement la modification des ressources. Rouvrez le menu de gestion pour vérifier et importer les réglages actuels de Proxmox.", "The restored service did not pass its health check": "Le service restauré n'a pas réussi son bilan de santé", "The restored service stopped; the recovery is not confirmed": "Le service restauré s'est arrêté; la récupération n'est pas confirmée", "The reviewed Tandoor stack does not require a privileged LXC.": "La pile Tandoor revue ne require un LXC privilégié.", @@ -6680,6 +6722,7 @@ "The selected VM": "La VM sélectionnée", "The selected VM is running.": "La VM sélectionnée est en cours d'exécution.", "The selected base folder does not exist and could not be created:": "Le dossier de base sélectionné n'existe pas et n'a pas pu être créé :", + "The selected bridge does not exist": "Le pont réseau sélectionné n’existe pas", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "Le pont sélectionné n'a pas de sous-réseau IPv4 pour le pare-feu du moniteur hôte", "The selected configuration needs to start the LXC during the installation": "La configuration sélectionnée doit démarrer le LXC pendant l'installation", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Le conteneur sélectionné n'est pas privilégié. Un conteneur privilégié est requis pour le relais direct du périphérique.", @@ -6702,6 +6745,7 @@ "The service builds its own image; only a published image can be installed": "Le service construit sa propre image; seule une image publiée peut être installée", "The service declares no image:": "Le service ne déclare aucune image:", "The setting has no final value:": "Le réglage n'a pas de valeur finale:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Les paramètres n’ont pas pu être entièrement rétablis. Vérifiez le conteneur et la modification enregistrée avant de continuer.", "The share already exists in smb.conf:": "Le partage existe déjà dans smb.conf :", "The shared destination is not a directory": "La destination partagée n'est pas un répertoire", "The shared directory points to a protected host path": "Le répertoire partagé indique un chemin d'hôte protégé", @@ -6769,19 +6813,24 @@ "These Proxmox resources were added outside ProxMenux:": "Ces ressources Proxmox ont été ajoutées en dehors de ProxMenux :", "These VMIDs are not free:": "Ces VMID ne sont pas libres:", "These are the changes that will be made": "Ce sont les changements qui seront apportés", + "These changes were made in Proxmox outside ProxMenux:": "Ces modifications ont été apportées dans Proxmox en dehors de ProxMenux :", "These container mount paths have an extension-like suffix:": "Ces chemins de montage du conteneur ont un suffixe qui ressemble à une extension :", "These containers were restored from a backup and this host has no record of their OCI application:": "Ces conteneurs ont été restaurés à partir d'une sauvegarde et cet hôte n'a aucun enregistrement de leur application OCI :", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Ces périphériques partagent le groupe IOMMU du GPU sélectionné et seront attribués à la VM avec lui :", "These host backups run with this job and expect its storage:": "Ces sauvegardes d'hôte s'exécutent avec ce travail et attendent son stockage :", "These host backups run with this job and will stop running:": "Ces sauvegardes d'hôte s'exécutent avec ce travail et cesseront de s'exécuter :", "These interface configurations will be removed": "Ces configurations d'interface seront supprimées", "These manual changes cannot be adopted safely:": "Ces modifications manuelles ne peuvent pas être adoptées en toute sécurité :", "These paths will not be restored live and will be extracted for manual recovery.": "Ces chemins ne seront pas restaurés en direct et seront extraits pour une récupération manuelle.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Ces paramètres nécessitent une adaptation propre à l’application. L’image n’a pas été téléchargée et le conteneur n’a pas été recréé.", "These values are asked during the installation:": "Ces valeurs sont demandées lors de l'installation:", "They are described in the image documentation:": "Ils sont décrits dans la documentation de l'image :", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Ils seront ajoutés à oci-compose avant de continuer. Les ressources non prises en charge ou modifiées ne sont pas importées.", + "They will be saved in oci-compose before continuing.": "Elles seront enregistrées dans oci-compose avant de continuer.", "This CIFS share is mounted with restrictive permissions.": "Ce partage CIFS est monté avec des autorisations restrictives.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Ce GPU est considéré comme incompatible avec le relais GPU vers une VM dans ProxMenux.", "This NFS share is fully restricted — even the host root cannot write to it.": "Ce partage NFS est entièrement restreint : même la racine de l'hôte ne peut pas y écrire.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Ce Proxmox Backup Server n'a pas d'abonnement actif. Passer au dépôt sans abonnement ? La source Enterprise PBS inaccessible sera désactivée.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Cette VM nécessite des étapes d'installation supplémentaires. Consultez le guide d'installation à l'adresse :\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Ce compte concerne {registry}. Entrez une image de ce registre.", "This action will:": "Cette action va :", @@ -6865,6 +6914,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Ce script ne peut pas être exécuté depuis le terminal web Proxmox.", "This script must be run as root.": "Ce script doit être exécuté en tant que root.", "This script must be run on a Proxmox host.": "Ce script doit être exécuté sur un hôte Proxmox.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Ce script appliquera les optimisations et réglages avancés suivants à votre Proxmox Backup Server", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Ce script appliquera les optimisations et ajustements avancés suivants à votre serveur Proxmox VE", "This script will update your Proxmox VE system with advanced options:": "Ce script mettra à jour votre système Proxmox VE avec des options avancées :", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Cette session est en cours d'exécution dans le terminal Monitor. L’exécuter à partir d’ici couperait la connexion en cours d’installation et laisserait le commutateur dans un état cassé.", @@ -6951,7 +7001,7 @@ "Transaction log:": "Registre des transactions & #160;:", "Translation files:": "Fichiers de traduction :", "Transmission is designed for easy, powerful use. Transmission has the features you want from a BitTorrent client: encryption, a web interface, peer exchange, magnet links, DHT, µTP, UPnP and NAT-PMP port forwarding, webseed support, watch directories, tracker editing, global and per-torrent speed limits, and more.": "Transmission est conçu pour une utilisation facile et puissante. Transmission possède les fonctionnalités que vous souhaitez d'un client BitTorrent : cryptage, interface web, échange entre pairs, liens magnétiques, DHT, μTP, UPnP et le transfert de port NAT-PMP, support de webseed, répertoires de veille, édition de tracker, limites de vitesse globales et per-torrent, et plus encore.", - "Tried pvesm path and manual detection methods": "Chemin pvesm essayé et méthodes de détection manuelle", + "Tried pvesm path and manual detection methods": "pvesm path et les méthodes de détection manuelle ont été essayés", "Trust this certificate and save it for scheduled backups?": "Faire confiance à ce certificat et l'enregistrer pour les sauvegardes planifiées ?", "Try Again": "Essayer à nouveau", "Try a different search term.": "Essayez un autre terme de recherche.", @@ -6996,6 +7046,7 @@ "Unable to identify the root filesystem. Aborting.": "Impossible d'identifier le système de fichiers racine. Avorter.", "Unable to resolve system ZFS pool disks. Aborting.": "Impossible de résoudre les disques du pool système ZFS.Avorter.", "Unable to resolve system disk topology. Aborting.": "Impossible de résoudre la topologie du disque système. Avorter.", + "Unapplied changes": "Modifications non appliquées", "Understand the security implications of privileged containers": "Comprendre les implications de sécurité des conteneurs privilégiés", "Unexpected Proxmox inventory; recovery blocked": "stocks imprévus Proxmox; récupération bloquée", "Unexpected formatting directory in the new Valkey volume": "Répertoire de formatage inattendu dans le nouveau volume Valkey", @@ -7111,6 +7162,7 @@ "Unsupported mount type": "Type de montage non supporté", "Unsupported mount type:": "Type de montage non pris en charge:", "Unsupported native device type:": "Type de périphérique natif non pris en charge:", + "Unsupported network settings": "Paramètres réseau non pris en charge", "Unsupported operation": "operation non prise en charge", "Unsupported or unknown Proxmox version; no repository changed.": "Version Proxmox non prise en charge ou inconnue ; aucun référentiel n'a été modifié.", "Unsupported output format:": "Format de sortie non pris en charge :", @@ -7191,6 +7243,7 @@ "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Assistant de mise à niveau : Proxmox VE 8 → 9 (Trixie)", "Upgrade canceled by user": "Mise à niveau annulée par l'utilisateur", "Upgrade one node at a time": "Mettre à niveau un nœud à la fois", + "Upgrade the system, offering the": "mettre à niveau le système, en offrant le", "Upgrade to Proxmox VE 9": "Mise à niveau vers Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "Mise à niveau des utilitaires NVIDIA (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "Télécharger une copie cryptée de la clé sur PBS afin de pouvoir la récupérer sur un hôte réinstallé avec juste une phrase secrète ?", @@ -7204,7 +7257,7 @@ "Usage:": "Utilisation :", "Use \"Check test progress\" to see results.": "Utilisez « Vérifier la progression du test » pour voir les résultats.", "Use 'Export to file' to save it and inspect manually.": "Utilisez « Exporter vers un fichier » pour l'enregistrer et l'inspecter manuellement.", - "Use 'pct restore' / 'qmrestore' to recover their disks from your VM backups.": "Utilisez « PCT Restore » / « qmrestore » pour récupérer leurs disques à partir des sauvegardes de votre VM.", + "Use 'pct restore' / 'qmrestore' to recover their disks from your VM backups.": "Utilisez 'pct restore' / 'qmrestore' pour récupérer leurs disques depuis les sauvegardes des VM.", "Use Custom backup and uncheck the conflicting path from the list": "Utilisez la sauvegarde personnalisée et décochez le chemin en conflit dans la liste", "Use Default Settings?": "Utiliser les paramètres par défaut ?", "Use Download first if you want to save a copy of the current key. Continue?": "utilisez d'abord Télécharger si vous souhaitez enregistrer une copie de la clé actuelle. Continuer?", @@ -7628,6 +7681,7 @@ "active VF(s)": "FV active(s)", "active VFs": "VF actives", "active Virtual Functions. Changing its driver binding would destroy every VF.": "Fonctions virtuelles actives. Changer la liaison de son pilote détruirait chaque VF.", + "added in Proxmox": "ajoutée dans Proxmox", "after copy; source tree is incomplete.": "après copie ; l'arborescence des sources est incomplète.", "after update.": "après mise à jour.", "all guests": "tous les invités", @@ -7948,8 +8002,8 @@ "partition(s). Partition table preserved.": "partition(s). Table de partition conservée.", "pasted Compose file": "fichier Compose collé", "paths for next boot (/etc/pve, guests, drivers, ...)": "chemins pour le prochain démarrage (/etc/pve, invités, pilotes, ...)", - "pct exec authorization failed": "échec de l'autorisation d'exécution PCT", - "pct push failed. Check log:": "La poussée PCT a échoué. Journal de vérification :", + "pct exec authorization failed": "Échec de l’autorisation pour pct exec", + "pct push failed. Check log:": "Échec de pct push. Consultez le journal :", "pending (reboot required to enumerate full group)": "en attente (redémarrage requis pour énumérer le groupe complet)", "phpMyAdmin is installed with arbitrary server connections enabled: the login page has a Server field where the address of the MySQL or MariaDB server is entered, together with its user and password.": "phpMyAdmin est installé avec des connexions de serveur arbitraires activées : la page de connexion a un champ Serveur où l'adresse du serveur MySQL ou MariaDB est saisie, ainsi que son utilisateur et son mot de passe.", "pigz configuration completed": "configuration pigz terminée", @@ -7972,8 +8026,8 @@ "pvesm command not found. This should not happen on Proxmox.": "Commande pvesm introuvable. Cela ne devrait pas arriver sur Proxmox.", "pvesm not available.": "pvesm non disponible.", "pvesm not found.": "pvesm introuvable.", - "pvesm path failed, trying manual detection...": "Échec du chemin pvesm, tentative de détection manuelle...", - "pvesm status failed": "le statut pvesm a échoué", + "pvesm path failed, trying manual detection...": "Échec de pvesm path, tentative de détection manuelle...", + "pvesm status failed": "Échec de pvesm status", "pyLoad is a Free and Open Source download manager written in Python and designed to be extremely lightweight, easily extensible and fully manageable via web.": "pyLoad est un gestionnaire de téléchargement gratuit et Open Source écrit en Python et conçu pour être extrêmement léger, facilement extensible et entièrement gérable via le web.", "pyLoad web interface": "PyLoad interface web", "qBittorrent WebUI password (user: admin)": "qBittorrent Mot de passe WebUI (utilisateur: admin)", @@ -7995,6 +8049,7 @@ "remapped users": "utilisateurs remappés", "remove the (now-empty) directory if possible": "supprimez le répertoire (maintenant vide) si possible", "removed from Proxmox": "supprimé de Proxmox", + "removed in Proxmox": "supprimée dans Proxmox", "removed successfully from Proxmox.": "supprimé avec succès de Proxmox.", "requires running the official installer, which restarts the Monitor service.": "nécessite l'exécution du programme d'installation officiel, qui redémarre le service Monitor.", "requires the package": "nécessite le paquet", @@ -8051,7 +8106,7 @@ "switch_gpu_mode.sh was not found.": "switch_gpu_mode.sh n'a pas été trouvé.", "sysfs ROM dump failed — trying ACPI VFCT table...": "Échec du vidage de la ROM sysfs - tentative de la table ACPI VFCT...", "syslog-ng allows you to flexibly collect, parse, classify, rewrite and correlate logs from across your infrastructure and store or route them to log analysis tools.": "syslog-ng vous permet de collecter, d'analyser, de classer, de réécrire et de corréler les logs de toute votre infrastructure et de les stocker ou de les acheminer vers des outils d'analyse de log.", - "systemctl restart networking failed:": "échec du redémarrage du réseau systemctl :", + "systemctl restart networking failed:": "Échec de systemctl restart networking :", "systemd OnCalendar expression": "expression systemd OnCalendar", "the API key was not generated on the first start": "la clé API n'a pas été générée au premier démarrage", "the NVIDIA hook of the container is not available:": "le hook NVIDIA du conteneur n'est pas disponible :", @@ -8098,18 +8153,20 @@ "using": "en utilisant", "using direct path (not guaranteed to survive reboots).": "en utilisant un chemin direct (il n'est pas garanti de survivre aux redémarrages).", "vCPUs:": "Processeurs virtuels :", + "value changed in Proxmox": "valeur modifiée dans Proxmox", "vfio-pci IDs configured": "ID vfio-pci configurés", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "ID vfio-pci dans /etc/modprobe.d/vfio.conf", "video": "vidéo", "video + recognition": "vidéo + reconnaissance", "video only": "vidéo uniquement", "vzdump backup speed optimization completed": "Optimisation de la vitesse de sauvegarde de vzdump terminée", - "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag construit ses liens à partir de l'adresse donnée pendant l'installation. S'il ne correspond pas à l'adresse du conteneur, modifier lxc.environnement. temps d'exécution: SYMFONY ENV DOMAIN NAME dans /etc/pve/lxc/.conf avec le conteneur arrêté, et recommencer.", + "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag construit ses liens avec l’adresse indiquée à l’installation. Si elle ne correspond pas à celle du conteneur, arrêtez-le, modifiez lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME dans /etc/pve/lxc/.conf, puis redémarrez-le.", "wallabag listens on port 80 of the container and stores its data in SQLite.": "wallabag écoute sur le port 80 du conteneur et stocke ses données dans SQLite.", "wallabag web interface": "Interface web wallabag", "was": "était", "was installed, but the kernel reports:": "a été installé, mais le noyau rapporte :", "when finished": "à la fin", + "when the server has no subscription (you will be asked)": "lorsque le serveur n'a pas d'abonnement (il vous sera demandé)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "reliera le GPU à vfio-pci au prochain redémarrage, cassant ainsi le pilote qui est sur le point d'être installé.", "wipefs failed on": "les wipefs ont échoué", "with": "avec", diff --git a/lang/it.json b/lang/it.json index 13e3c788..b71db0f4 100644 --- a/lang/it.json +++ b/lang/it.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (percorso vzdump predefinito di Proxmox)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (predefinito di Proxmox)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = sticky bit + rwx per tutti. Non è necessario alcun gruppo condiviso.", + "====== PBS UPDATE COMPLETED ======": "====== AGGIORNAMENTO PBS COMPLETATO ======", "====== PVE UPDATE COMPLETED ======": "====== AGGIORNAMENTO PVE COMPLETATO ======", ">> Create a new backup job <<": ">> Crea un nuovo job di backup <<", ">> Save and return <<": ">> Salva e torna indietro <<", @@ -87,6 +88,7 @@ "A multi-container application is not recreated: its containers are updated together.": "Un’applicazione con più container non viene ricreata: i suoi container vengono aggiornati insieme.", "A new ProxMenux version is available:": "È disponibile una nuova versione di ProxMenux:", "A new kernel is staged for the next boot:": "Un nuovo kernel è predisposto per il prossimo avvio:", + "A newer kernel is installed. Reboot before the next backup window to use it.": "è installato un kernel più recente. Riavviare prima della finestra di backup successiva per utilizzarlo.", "A newer version is available:": "È disponibile una versione più recente:", "A pending operation exists for": "Operazione in sospeso per il", "A pending stack assembly already exists; it is not overwritten": "Esiste già un assemblaggio dello stack in sospeso; non verrà sovrascritto", @@ -169,6 +171,7 @@ "APT language configuration updated": "Configurazione lingue APT aggiornata", "APT language downloads restored": "Download dei file lingua APT ripristinati", "APT package lists updated": "Elenchi pacchetti APT aggiornati", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "aggiornamento APT non riuscito. Controllare l'abbonamento e i repository PBS in Amministrazione > Repository. Tronco d'albero:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "Aggiornamento degli indici APT non riuscito. Controlla l'abbonamento e i repository PBS nell'interfaccia nativa di PBS; nessuna sorgente è stata modificata.", "APT update failed. Check repository access; NVIDIA installation stopped.": "Aggiornamento APT non riuscito. Controlla l’accesso al repository; installazione NVIDIA interrotta.", "Aborted": "Interrotto", @@ -443,10 +446,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "Applicare ora l'aggiornamento dei pacchetti esaminato? I servizi potrebbero riavviarsi e i pacchetti indicati per la rimozione verranno rimossi.", "Apply these changes?": "Applicare queste modifiche?", "Applying AMD-specific fixes...": "Applicazione delle correzioni specifiche AMD...", + "Applying CPU and RAM changes without rebuilding the container...": "Applicazione delle modifiche a CPU e RAM senza ricreare il container...", "Applying Changes": "Applicazione delle modifiche", "Applying Controller/NVMe passthrough to VM": "Applicazione del passthrough Controller/NVMe alla VM", "Applying balanced memory optimization settings...": "Applicazione delle impostazioni bilanciate di ottimizzazione della memoria...", "Applying changes safely...": "Applicazione sicura delle modifiche...", + "Applying container settings without rebuilding the container...": "Applicazione delle impostazioni senza ricreare il container...", "Applying custom restore": "Applicazione del ripristino personalizzato", "Applying default VM configuration": "Applicazione della configurazione predefinita della VM", "Applying host permissions for unprivileged LXC bind-mounts...": "Applicazione delle autorizzazioni sull’host per i bind mount dei container LXC non privilegiati...", @@ -538,6 +543,7 @@ "Available Borg targets:": "Destinazioni Borg disponibili:", "Available Disks on Host": "Dischi disponibili sull'host", "Available ISO Images": "Immagini ISO disponibili", + "Available PBS Version": "versione PBS disponibile", "Available PBS repositories:": "Repository PBS disponibili:", "Available PVE Version": "Versione PVE disponibile", "Available Shares:": "Condivisioni disponibili:", @@ -749,6 +755,7 @@ "Cannot verify the reused disk:": "Impossibile verificare il disco riutilizzato:", "Cannot write the image archive to {storage}. Check storage access and mount status.": "Impossibile scrivere l'archivio immagini su {storage}. Controlla l'accesso allo spazio di archiviazione e lo stato di montaggio.", "Capabilities cannot be kept and all dropped at the same time": "Non è possibile conservare alcune capability e rimuoverle tutte contemporaneamente", + "Catalog options": "Opzioni del catalogo", "Category": "Categoria", "Caution: Maximum mode generates more heat.": "Attenzione: la modalità massima genera più calore.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Controllo Ceph saltato dal flag utente (--ignore-ceph-check)", @@ -883,6 +890,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Scegli una pianificazione (sintassi calendar-event di systemd):", "Choose a script to execute:": "Scegli uno script da eseguire:", "Choose a server:": "Scegli un server:", + "Choose a setting. Changes are applied only after the final review.": "Scegli cosa configurare. Le modifiche vengono applicate solo dopo la verifica finale.", "Choose a share to mount:": "Scegli una condivisione da montare:", "Choose a specific supported GPU or USB node": "Scegli uno specifico nodo GPU o USB supportato", "Choose action for this controller/NVMe:": "Scegli l'azione per questo controller/NVMe:", @@ -1520,6 +1528,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "Inventario NVIDIA attuale acquisito: è necessario un aggiornamento", "Current NVIDIA inventory resolved: no refresh is required": "Inventario NVIDIA corrente acquisito: non serve aggiornarlo", "Current Network Configuration": "Configurazione di rete corrente", + "Current PBS Version": "versione attuale di PBS", "Current PVE Version": "Versione PVE attuale", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Gli attuali script host ProxMenux registrano le condivisioni remote come storage Proxmox con pvesm.", "Current ProxMenux version:": "Versione attuale di ProxMenux:", @@ -1685,6 +1694,7 @@ "Detected Coral hardware:": "Hardware Coral rilevato:", "Detected GPU(s):": "GPU rilevate:", "Detected Intel CPU": "Rilevata CPU Intel", + "Detected Proxmox Backup Server": "Proxmox Backup Server rilevato", "Detected Proxmox VE": "Rilevato Proxmox VE", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Rilevato Proxmox VE 8.x - Installazione di Ceph Squid per Debian", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Rilevato Proxmox VE 9.x - Installazione di Ceph Squid per Debian Trixie", @@ -1766,6 +1776,9 @@ "Disabling and stopping rpcbind service and socket...": "Disabilitazione e arresto del servizio e del socket rpcbind...", "Disabling non-free firmware warnings...": "Disabilitazione degli avvisi sul firmware non libero...", "Disabling portmapper/rpcbind for security...": "Disabilitazione di portmapper/rpcbind per sicurezza...", + "Discard changes": "Scarta le modifiche", + "Discard unapplied changes?": "Scartare le modifiche non applicate?", + "Discover NFS exports on a server": "Individuare le esportazioni NFS di un server", "Discover targets on portal:": "Scopri target sul portale:", "Discovering iSCSI targets...": "Individuazione dei target iSCSI...", "Discovery successful": "Scoperta riuscita", @@ -1845,6 +1858,7 @@ "Do you want to proceed with the system update?": "Vuoi procedere con l'aggiornamento del sistema?", "Do you want to proceed?": "Vuoi procedere?", "Do you want to reboot now?": "Vuoi riavviare ora?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "Vuoi rimuovere il banner dell'abbonamento Proxmox dall'interfaccia web di Proxmox Backup Server?", "Do you want to remove the stale entry from vfio.conf and continue?": "Vuoi rimuovere la voce obsoleta da vfio.conf e continuare?", "Do you want to remove the stale entry from vfio.conf?": "Vuoi rimuovere la voce obsoleta da vfio.conf?", "Do you want to restart the network service now to apply changes?": "Riavviare ora il servizio di rete per applicare le modifiche?", @@ -2270,6 +2284,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "Impossibile clonare il repository log2ram. Controlla /tmp/log2ram_install.log", "Failed to configure EFI disk": "Impossibile configurare il disco EFI", "Failed to configure IOMMU automatically.": "Impossibile configurare IOMMU automaticamente.", + "Failed to configure Proxmox Backup Server repositories": "Impossibile configurare i repository di Proxmox Backup Server", "Failed to configure Proxmox repositories": "Impossibile configurare i repository Proxmox", "Failed to configure TPM device in VM": "Impossibile configurare il dispositivo TPM nella VM", "Failed to configure repositories.": "Impossibile configurare i repository.", @@ -2977,6 +2992,7 @@ "Inaccessible device during VM startup": "Dispositivo inaccessibile durante l'avvio della VM", "Inactive": "Inattivo", "Inactive (entry in fstab, not currently mounted)": "Inattivo (presente in fstab, attualmente non montato)", + "Include these changes in oci-compose?": "Includere queste modifiche in oci-compose?", "Include these resources in oci-compose?": "Includere queste risorse in oci-compose?", "Includes /etc/network (may drop SSH immediately)": "Include /etc/network (potrebbe interrompere immediatamente la connessione SSH)", "Includes /etc/zfs": "Include /etc/zfs", @@ -3213,6 +3229,8 @@ "Invalid ALLOWED_HOSTS value": "Valore ALLOWED_HOSTS non valido", "Invalid Home Assistant OS check timeout": "Timeout controllo Home Assistant OS non valido", "Invalid ID": "ID non valido", + "Invalid IPv4 address": "Indirizzo IPv4 non valido", + "Invalid IPv4 gateway": "Gateway IPv4 non valido", "Invalid Intel render path": "Percorso di rendering Intel non valido", "Invalid Jellyfin path:": "Percorso Jellyfin non valido:", "Invalid MAC address:": "Indirizzo MAC non valido:", @@ -3245,6 +3263,7 @@ "Invalid administrator email": "Email amministratore non valida", "Invalid administrator user name": "Nome utente amministratore non valido", "Invalid base VMID": "VMID di base non valido", + "Invalid bridge": "Bridge non valido", "Invalid check package:": "Pacchetto controllo non valido:", "Invalid configuration path:": "Percorso configurazione non valido:", "Invalid consume/export volumes": "Volumi di acquisizione/esportazione non validi", @@ -3308,6 +3327,7 @@ "Invalid remote name": "Nome del remote non valido", "Invalid remote path": "Percorso remoto non valido", "Invalid repair version:": "Versione di riparazione non valida:", + "Invalid resource value": "Valore della risorsa non valido", "Invalid resources": "Risorse non valide", "Invalid restored volume path": "Percorso volume ripristinato non valido", "Invalid running state": "Stato di esecuzione non valido", @@ -3326,6 +3346,7 @@ "Invalid stack members": "Membri stack non validi", "Invalid stack name": "Nome stack non valido", "Invalid stack operation": "Operazione stack non valida", + "Invalid storage": "Archiviazione non valida", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID storage non valido. Usa solo lettere, numeri, trattini e trattini bassi.", "Invalid storage name": "Nome storage non valido", "Invalid suite application": "Applicazione suite non valida", @@ -4157,6 +4178,7 @@ "No active Proxmox storage accepts OCI template archives.": "Nessuno spazio di archiviazione Proxmox attivo accetta archivi modello OCI.", "No active exports or showmount not available": "Nessuna esportazione attiva o showmount non disponibile", "No active session": "Nessuna sessione attiva", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "nessun abbonamento attivo e nessun repository PBS utilizzabile. La modalità non interattiva non può modificare le origini APT;configurarli in Amministrazione > Repository.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "Nessun abbonamento attivo e nessun repository PVE utilizzabile. La modalità non interattiva non può modificare le origini APT; configurarle in Nodo > Aggiornamenti > Repository.", "No additional GPU can be added.": "Non è possibile aggiungere alcuna GPU aggiuntiva.", "No additional device needs to be added.": "Non è necessario aggiungere alcun dispositivo aggiuntivo.", @@ -4261,6 +4283,7 @@ "No password provided.": "Nessuna password fornita.", "No paths available": "Nessun percorso disponibile", "No paths selected. Select at least one path.": "Nessun percorso selezionato. Seleziona almeno un percorso.", + "No pending changes": "Nessuna modifica in sospeso", "No pending paths to schedule for reboot.": "Nessun percorso in sospeso da pianificare per il riavvio.", "No permanent NFS mounts configured.": "Nessun montaggio NFS permanente configurato.", "No permanent Samba mounts configured.": "Nessun montaggio Samba permanente configurato.", @@ -4402,6 +4425,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM rimosso (gli artefatti di Puppet potrebbero richiedere una pulizia manuale)", "OVH server detected": "Rilevato server OVH", "OVH server detection and RTM installation process completed": "Processo di rilevamento dei server OVH e installazione RTM completato", + "Object detector": "Rilevatore di oggetti", "Object detector for Frigate": "Rilevatore di oggetti per Frigate", "Observer is not responding on port 4357": "Observer non risponde sulla porta 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian è un’app per creare, collegare e organizzare le note sul tuo dispositivo, con centinaia di plugin e temi per personalizzare il flusso di lavoro. Puoi pubblicare le note online, consultarle offline e sincronizzarle in modo sicuro con crittografia end-to-end.", @@ -4874,12 +4898,17 @@ "Proxmology logo applied": "Logo Proxmology applicato", "Proxmox 9 system update allready": "Il sistema è già aggiornato a Proxmox 9", "Proxmox APT repositories configured": "Repository APT Proxmox configurati", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Destinazioni Proxmox Backup Server (PBS)", + "Proxmox Backup Server APT repositories configured": "Repository APT di Proxmox Backup Server configurati", + "Proxmox Backup Server Update": "Aggiornamento di Proxmox Backup Server", + "Proxmox Backup Server update completed.": "Aggiornamento di Proxmox Backup Server completato.", "Proxmox CIFS Storage Status:": "Stato storage CIFS Proxmox:", "Proxmox NFS Storage Status:": "Stato storage NFS Proxmox:", "Proxmox System Update": "Aggiornamento sistema Proxmox", "Proxmox Update": "Aggiornamento Proxmox", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "L’aggiornamento dei pacchetti di Proxmox VE 8 non è riuscito. Controlla l’output di APT prima di riprovare.", "Proxmox VE 8 system update completed successfully": "Aggiornamento sistema Proxmox VE 8 completato", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Guida aggiornamento manuale Proxmox VE da 8 a 9", "Proxmox VE 9.x no-subscription repository created": "Repository senza abbonamento Proxmox VE 9.x creato", @@ -4887,6 +4916,7 @@ "Proxmox VE configuration completed.": "Configurazione Proxmox VE completata.", "Proxmox VE safe update completed": "Aggiornamento sicuro di Proxmox VE completato", "Proxmox auth logger service created and started": "Servizio log autenticazione Proxmox creato e avviato", + "Proxmox could not apply the container settings": "Proxmox non è riuscito ad applicare le impostazioni del container", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Il repository enterprise Proxmox potrebbe essere mancante o inaccessibile. Tentativo di passare al repository no-subscription...", "Proxmox filter configured": "Filtro Proxmox configurato", "Proxmox iSCSI Storage Status:": "Stato storage iSCSI Proxmox:", @@ -5153,6 +5183,8 @@ "Remove saved credentials": "Rimuovi le credenziali salvate", "Remove subscription banner": "Rimuovi il banner di abbonamento", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Rimuovere l’applicazione? I dischi dei suoi container vengono eliminati e solo un backup può ripristinarli.", + "Remove the lock left on a VM by an interrupted task": "Rimuovere il blocco lasciato su una VM da un’attività interrotta", + "Remove the lock left on a container by an interrupted task": "Rimuovere il blocco lasciato su un container da un’attività interrotta", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Rimuovere le credenziali salvate per {registry}?\nLe app in esecuzione continueranno a funzionare.", "Remove the unprivileged flag from configuration:": "Rimuovi il flag unprivileged dalla configurazione:", "Remove unused packages and their config": "Rimuovi i pacchetti non utilizzati e la loro configurazione", @@ -5234,6 +5266,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "Funzione di gestione dei repository non disponibile. L’installazione di NVIDIA è stata interrotta.", "Repository policy returned an unexpected result.": "La policy del repository ha restituito un risultato imprevisto.", "Repository ready.": "Repository pronto.", + "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.": "Le sorgenti dei repository sono cambiate, ma l'aggiornamento dell'elenco dei pacchetti APT non è riuscito. Operazione interrotta; le sorgenti non sono state ripristinate. Controlla Administration > Repositories e riesegui apt-get update prima di continuare.", "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.": "Le origini del repository sono cambiate, ma l'aggiornamento dell'elenco dei pacchetti APT non è riuscito. Operazione interrotta; le origini non sono state ripristinate. Controlla Nodo > Aggiornamenti > Repository e riprova apt-get update prima di continuare.", "Repository switch declined; no APT source changed.": "Cambio di repository rifiutato; nessuna origine APT è stata modificata.", "Repository:": "Repository:", @@ -5328,6 +5361,7 @@ "Reverting ZFS ARC tuning...": "Rimozione delle ottimizzazioni di ZFS ARC…", "Reverting pigz wrapper...": "Rimozione del wrapper pigz e ripristino della configurazione precedente...", "Reverting vzdump speed tuning...": "Rimozione ottimizzazioni velocità vzdump…", + "Review and apply": "Verifica e applica", "Review external OCI changes": "Esamina le modifiche OCI esterne", "Review package upgrade": "Esamina l'aggiornamento dei pacchetti", "Review passthrough config files": "Esamina i file di configurazione passthrough", @@ -5787,6 +5821,7 @@ "ShareCoveX panel": "Pannello ShareCoveX", "Shared Directory Ready:": "Directory condivisa pronta:", "Shared Group": "Gruppo condiviso", + "Shared directories": "Cartelle condivise", "Shared directory created:": "Directory condivisa creata:", "Shared directory for consume and export": "Directory condivisa per consume ed export", "Shared directory for copy/sync operations": "Directory condivisa per copia e sincronizzazione", @@ -5816,6 +5851,7 @@ "Should show pve-manager/9.x.x": "Dovrebbe mostrare pve-manager/9.x.x", "Shoutrrr addresses for the notifications (optional)": "Indirizzi Shoutrrr per le notifiche (facoltativi)", "Show All Scripts": "Mostra tutti gli script", + "Show CT users and their UID/GID on the host": "Utenti del CT e loro UID/GID sull’host", "Show CT users in table format": "Mostra utenti CT in tabella", "Show Container Privilege Status": "Mostra stato privilegi container", "Show DNS resolution status": "Mostra stato risoluzione DNS", @@ -5977,6 +6013,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Avvia la VM per iniziare l’installazione di Windows dalla ISO montata.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Avviare le applicazioni dopo la registrazione? Rispondi No se i container originali sono ancora in esecuzione su un altro host: entrambi userebbero gli stessi indirizzi.", "Start the converted container:": "Avvia il container convertito:", + "Start the interactive Proxmox package upgrade": "Avviare l’aggiornamento interattivo dei pacchetti Proxmox", "Start the main system upgrade:": "Avvia l’aggiornamento principale del sistema:", "Start the stack with Proxmox": "Avvia lo stack con Proxmox", "Start uploading to PBS — sets a recovery passphrase": "Avvia il caricamento su PBS — viene impostata una passphrase di ripristino", @@ -6256,6 +6293,7 @@ "The Paperless persistent volumes need at least 8 GB": "I volumi persistenti di Paperless richiedono almeno 8 GB", "The PostgreSQL volume needs at least 4 GB": "Il volume PostgreSQL richiede almeno 4 GB", "The PostgreSQL volume needs at least 8 GB": "Il volume PostgreSQL richiede almeno 8 GB", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "L’aggiornamento di Proxmox VE 8 non è stato completato. L’aggiornamento a Proxmox VE 9 non è stato avviato.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "Manca il portachiavi dell’archivio Proxmox; l’installazione di Ceph non può continuare in sicurezza", "The Proxmox inventory and the local configurations differ": "L'inventario Proxmox e le configurazioni locali differiscono", "The ROCm image has no support for the AMD GPU of this host:": "L’immagine ROCm non supporta la GPU AMD di questo host:", @@ -6326,6 +6364,7 @@ "The build could not be checked beforehand; continuing without that check.": "Non è stato possibile verificare la build in anticipo; si prosegue senza tale verifica.", "The cached image does not match the current digest": "L'immagine nella cache non corrisponde al digest corrente", "The cached image is damaged; it will be downloaded again.": "L'immagine nella cache è danneggiata; verrà scaricata nuovamente.", + "The changes need the application installer": "Le modifiche richiedono il programma di installazione dell’applicazione", "The character device does not exist:": "Il dispositivo a caratteri non esiste:", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Il comando richiede una password non visualizzata durante la digitazione. Dopo la creazione degli utenti, l’interfaccia web richiede nome utente e password.", "The command does not name an image": "Il comando non specifica un’immagine", @@ -6445,8 +6484,10 @@ "The host bind source does not exist:": "La sorgente del bind mount sull’host non esiste:", "The host bind source is not a regular file or directory:": "La sorgente del bind mount sull'host non è un file regolare né una directory:", "The host directory changed before it was mounted": "La directory dell'host è cambiata prima del montaggio", + "The host directory does not exist": "La directory dell’host non esiste", "The host directory may not be accessible from an unprivileged container.": "La directory dell’host potrebbe non essere accessibile da un container non privilegiato.", "The host has no IPv4 address on the selected bridge": "L'host non ha un indirizzo IPv4 sul bridge selezionato", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "L’host perde l’accesso a questi dispositivi. Se il gruppo include un controller di rete o di archiviazione necessario all’host, annulla nel riepilogo.", "The host monitor does not see the real host memory": "Il monitoraggio dell'host non rileva la memoria reale dell'host", "The host monitor does not share this host namespace:": "Il monitor dell'host non condivide questo namespace dell'host:", "The host monitor include is unknown or differs from the saved record": "L'include del monitor host è sconosciuto o diverso dal record salvato", @@ -6636,6 +6677,7 @@ "The repair must preserve the image dependencies:": "La riparazione deve conservare le dipendenze dell'immagine:", "The requested VMID block is already in use": "Il blocco di VMID richiesto è già in uso", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "Il profilo GPU richiesto per il machine learning non funziona; non viene sostituito dalla CPU", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "Non è stato possibile annullare completamente la modifica delle risorse. Riapri il menu di gestione per verificare e importare le impostazioni attuali di Proxmox.", "The restored service did not pass its health check": "Il servizio ripristinato non ha superato il controllo dello stato", "The restored service stopped; the recovery is not confirmed": "Il servizio ripristinato si è arrestato; il recupero non è confermato", "The reviewed Tandoor stack does not require a privileged LXC.": "Lo stack Tandoor esaminato non richiede un container LXC privilegiato.", @@ -6681,6 +6723,7 @@ "The selected VM": "La VM selezionata", "The selected VM is running.": "La VM selezionata è in esecuzione.", "The selected base folder does not exist and could not be created:": "La cartella di base selezionata non esiste e non è stato possibile crearla:", + "The selected bridge does not exist": "Il bridge selezionato non esiste", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "Il bridge selezionato non ha una sottorete IPv4 per il firewall del monitoraggio dell’host", "The selected configuration needs to start the LXC during the installation": "La configurazione selezionata richiede l'avvio del container LXC durante l'installazione", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Il container selezionato non è privilegiato. È richiesto un container privilegiato per il passthrough diretto del dispositivo.", @@ -6703,6 +6746,7 @@ "The service builds its own image; only a published image can be installed": "Il servizio crea la propria immagine; si possono installare solo immagini pubblicate", "The service declares no image:": "Il servizio non dichiara alcuna immagine:", "The setting has no final value:": "L'impostazione non ha un valore finale:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Non è stato possibile ripristinare tutte le impostazioni. Controlla il container e la modifica salvata prima di continuare.", "The share already exists in smb.conf:": "La condivisione esiste già in smb.conf:", "The shared destination is not a directory": "La destinazione condivisa non è una directory", "The shared directory points to a protected host path": "La directory condivisa punta a un percorso protetto dell'host", @@ -6770,19 +6814,24 @@ "These Proxmox resources were added outside ProxMenux:": "Queste risorse Proxmox sono state aggiunte fuori da ProxMenux:", "These VMIDs are not free:": "Questi VMID non sono disponibili:", "These are the changes that will be made": "Verranno apportate queste modifiche", + "These changes were made in Proxmox outside ProxMenux:": "Queste modifiche sono state apportate in Proxmox al di fuori di ProxMenux:", "These container mount paths have an extension-like suffix:": "Questi percorsi di mount del container hanno un suffisso simile a un'estensione:", "These containers were restored from a backup and this host has no record of their OCI application:": "Questi container sono stati ripristinati da un backup e questo host non ha il record della loro applicazione OCI:", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Questi dispositivi condividono il gruppo IOMMU della GPU selezionata e verranno assegnati alla VM insieme a essa:", "These host backups run with this job and expect its storage:": "Questi backup dell’host vengono eseguiti con questo job e richiedono il suo storage:", "These host backups run with this job and will stop running:": "Questi backup dell'host vengono eseguiti con questo lavoro e interromperanno l'esecuzione:", "These interface configurations will be removed": "Queste configurazioni dell'interfaccia verranno rimosse", "These manual changes cannot be adopted safely:": "Queste modifiche manuali non possono essere acquisite in modo sicuro:", "These paths will not be restored live and will be extracted for manual recovery.": "Questi percorsi non verranno ripristinati sul sistema in esecuzione e verranno estratti per il ripristino manuale.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Queste impostazioni richiedono un adattamento specifico per l’applicazione. L’immagine non è stata scaricata e il container non è stato ricreato.", "These values are asked during the installation:": "Questi valori verranno richiesti durante l’installazione:", "They are described in the image documentation:": "Sono descritti nella documentazione dell'immagine:", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Se confermi e la verifica finale riesce, queste risorse verranno registrate in oci-compose prima di proseguire. Le risorse non supportate o modificate non vengono acquisite.", + "They will be saved in oci-compose before continuing.": "Verranno salvate in oci-compose prima di continuare.", "This CIFS share is mounted with restrictive permissions.": "Questa condivisione CIFS è montata con autorizzazioni restrittive.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Questa GPU è considerata incompatibile con il passthrough GPU a una VM in ProxMenux.", "This NFS share is fully restricted — even the host root cannot write to it.": "Questa condivisione NFS è completamente limitata: nemmeno l'utente root dell'host può scrivervi.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Questo Proxmox Backup Server non ha un abbonamento attivo. Passare al repository senza abbonamento? La sorgente Enterprise PBS non accessibile verrà disattivata.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Questa VM richiede ulteriori passaggi di installazione; vedere la guida:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Questo account è per {registry}. Inserisci un'immagine da quel registro.", "This action will:": "Questa azione:", @@ -6866,6 +6915,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Questo script non può essere eseguito dal terminale web Proxmox.", "This script must be run as root.": "Questo script deve essere eseguito come root.", "This script must be run on a Proxmox host.": "Questo script deve essere eseguito su un host Proxmox.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Questo script applicherà le seguenti ottimizzazioni e regolazioni avanzate al tuo Proxmox Backup Server", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Questo script applicherà le seguenti ottimizzazioni e regolazioni avanzate al server Proxmox VE", "This script will update your Proxmox VE system with advanced options:": "Questo script aggiornerà il sistema Proxmox VE con opzioni avanzate:", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Questa sessione è in esecuzione nel terminale Monitor. Eseguirlo da qui interromperebbe la connessione a metà installazione e lascerebbe il cambio di canale incompleto.", @@ -6997,6 +7047,7 @@ "Unable to identify the root filesystem. Aborting.": "Impossibile identificare il filesystem root. Interruzione.", "Unable to resolve system ZFS pool disks. Aborting.": "Impossibile individuare i dischi del pool ZFS di sistema. Interruzione.", "Unable to resolve system disk topology. Aborting.": "Impossibile determinare la topologia del disco di sistema. Interruzione.", + "Unapplied changes": "Modifiche non applicate", "Understand the security implications of privileged containers": "Comprendere le implicazioni sulla sicurezza dei container privilegiati", "Unexpected Proxmox inventory; recovery blocked": "Inventario Proxmox inatteso; ripristino bloccato", "Unexpected formatting directory in the new Valkey volume": "Directory di formattazione imprevista nel nuovo volume Valkey", @@ -7112,6 +7163,7 @@ "Unsupported mount type": "Tipo mount non supportato", "Unsupported mount type:": "Tipo mount non supportato:", "Unsupported native device type:": "Tipo dispositivo nativo non supportato:", + "Unsupported network settings": "Impostazioni di rete non supportate", "Unsupported operation": "Operazione non supportata", "Unsupported or unknown Proxmox version; no repository changed.": "Versione Proxmox non supportata o sconosciuta; nessun repository è stato modificato.", "Unsupported output format:": "Formato output non supportato:", @@ -7192,6 +7244,7 @@ "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Assistente aggiornamento: Proxmox VE 8 → 9 (Trixie)", "Upgrade canceled by user": "Aggiornamento annullato dall'utente", "Upgrade one node at a time": "Aggiorna un nodo alla volta", + "Upgrade the system, offering the": "aggiorna il sistema, offrendo il", "Upgrade to Proxmox VE 9": "Aggiorna a Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "Aggiornamento utilità NVIDIA (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "Caricare una copia crittografata della chiave su PBS per recuperarla su un host reinstallato con la sola passphrase?", @@ -7629,6 +7682,7 @@ "active VF(s)": "VF attive", "active VFs": "VF attive", "active Virtual Functions. Changing its driver binding would destroy every VF.": "funzioni virtuali attive. La modifica del collegamento del driver distruggerebbe ogni VF.", + "added in Proxmox": "aggiunta in Proxmox", "after copy; source tree is incomplete.": "dopo la copia; l'albero dei sorgenti è incompleto.", "after update.": "dopo l'aggiornamento.", "all guests": "tutti i guest", @@ -7996,6 +8050,7 @@ "remapped users": "utenti rimappati", "remove the (now-empty) directory if possible": "rimuovere la directory (ora vuota) se possibile", "removed from Proxmox": "rimosso da Proxmox", + "removed in Proxmox": "rimossa in Proxmox", "removed successfully from Proxmox.": "rimosso con successo da Proxmox.", "requires running the official installer, which restarts the Monitor service.": "richiede l'esecuzione del programma di installazione ufficiale, che riavvia il servizio Monitor.", "requires the package": "richiede il pacchetto", @@ -8099,6 +8154,7 @@ "using": "utilizzando", "using direct path (not guaranteed to survive reboots).": "utilizzando il percorso diretto (non è garantito che sopravviva ai riavvii).", "vCPUs:": "vCPU:", + "value changed in Proxmox": "valore modificato in Proxmox", "vfio-pci IDs configured": "ID vfio-pci configurati", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "ID vfio-pci in /etc/modprobe.d/vfio.conf", "video": "video", @@ -8111,6 +8167,7 @@ "was": "era", "was installed, but the kernel reports:": "è stato installato, ma il kernel segnala:", "when finished": "al termine", + "when the server has no subscription (you will be asked)": "quando il server non ha abbonamento (ti verrà chiesto)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "riassocerà la GPU a vfio-pci al prossimo riavvio, impedendo il funzionamento del driver che sta per essere installato.", "wipefs failed on": "wipefs non è riuscito su", "with": "con", diff --git a/lang/pt.json b/lang/pt.json index e95b4413..a0a14f5f 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (caminho vzdump padrão do Proxmox)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (padrão Proxmox)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = sticky bit + rwx para todos. Nenhum grupo compartilhado é necessário.", + "====== PBS UPDATE COMPLETED ======": "====== ATUALIZAÇÃO PBS CONCLUÍDA ======", "====== PVE UPDATE COMPLETED ======": "====== ATUALIZAÇÃO PVE CONCLUÍDA ======", ">> Create a new backup job <<": ">> Crie uma nova tarefa de backup <<", ">> Save and return <<": ">> Salvar e retornar <<", @@ -87,6 +88,7 @@ "A multi-container application is not recreated: its containers are updated together.": "Um aplicativo com vários contêineres não é recriado: seus contêineres são atualizados juntos.", "A new ProxMenux version is available:": "Uma nova versão do ProxMenux está disponível:", "A new kernel is staged for the next boot:": "Um novo kernel está preparado para a próxima inicialização:", + "A newer kernel is installed. Reboot before the next backup window to use it.": "Um kernel mais recente está instalado. Reinicie antes da próxima janela de backup para usá-lo.", "A newer version is available:": "Uma versão mais recente está disponível:", "A pending operation exists for": "Existe uma operation pendente para", "A pending stack assembly already exists; it is not overwritten": "Já existe um conjunto de pilha pendente; não é substituído", @@ -136,7 +138,7 @@ "ALL DATA ON": "TODOS OS DADOS EM", "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!": "TODOS OS DADOS NESTE DISCO SERÃO PERMANENTEMENTE PERDIDOS!", "ALL Utilities": "TODOS os utilitários", - "ALLOWED_HOSTS cannot contain line breaks": "Allowed hosts não pode conter quebras de linha", + "ALLOWED_HOSTS cannot contain line breaks": "ALLOWED_HOSTS não pode conter quebras de linha", "AList initial login": "Login inicial do AList", "AMD (ROCm)": "AMD (ROCm)", "AMD (VA-API and ROCm detection)": "AMD (detecção VA-API e ROCm)", @@ -169,6 +171,7 @@ "APT language configuration updated": "Configuração de idioma APT atualizada", "APT language downloads restored": "Downloads de idiomas APT restaurados", "APT package lists updated": "Listas de pacotes APT atualizadas", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "falha na atualização do APT.Verifique a assinatura e os repositórios do PBS em Administração > Repositórios. Registro:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "Falha ao atualizar os índices APT. Verifique a subscrição e os repositórios PBS na interface nativa do PBS; nenhuma fonte foi alterada.", "APT update failed. Check repository access; NVIDIA installation stopped.": "Falha na atualização do APT. Verifique o acesso ao repositório; a instalação do NVIDIA foi interrompida.", "Aborted": "Abortado", @@ -443,10 +446,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "Aplicar agora a atualização de pacotes revista? Os serviços podem reiniciar e os pacotes indicados para remoção serão removidos.", "Apply these changes?": "Aplicar essas alterações?", "Applying AMD-specific fixes...": "Aplicando correções específicas da AMD...", + "Applying CPU and RAM changes without rebuilding the container...": "A aplicar as alterações de CPU e RAM sem recriar o contentor...", "Applying Changes": "Aplicando alterações", "Applying Controller/NVMe passthrough to VM": "Aplicando passagem de controlador/NVMe à VM", "Applying balanced memory optimization settings...": "Aplicando configurações de otimização de memória balanceadas...", "Applying changes safely...": "Aplicando alterações com segurança...", + "Applying container settings without rebuilding the container...": "A aplicar as definições sem recriar o contentor...", "Applying custom restore": "Aplicando restauração personalizada", "Applying default VM configuration": "Aplicando configuração de VM padrão", "Applying host permissions for unprivileged LXC bind-mounts...": "Aplicando permissões de host para montagens de ligação LXC sem privilégios...", @@ -538,6 +543,7 @@ "Available Borg targets:": "Alvos Borg disponíveis:", "Available Disks on Host": "Discos disponíveis no host", "Available ISO Images": "Imagens ISO disponíveis", + "Available PBS Version": "versão PBS disponível", "Available PBS repositories:": "Repositórios PBS disponíveis:", "Available PVE Version": "Versão PVE disponível", "Available Shares:": "Ações disponíveis:", @@ -749,6 +755,7 @@ "Cannot verify the reused disk:": "Não foi possível verificar o disco reutilizado:", "Cannot write the image archive to {storage}. Check storage access and mount status.": "Não é possível gravar o arquivo de imagem em {storage}. Verifique o acesso ao armazenamento e o status de montagem.", "Capabilities cannot be kept and all dropped at the same time": "Capacidades não podem ser mantidas e todas retiradas ao mesmo tempo", + "Catalog options": "Opções do catálogo", "Category": "Categoria", "Caution: Maximum mode generates more heat.": "Cuidado: O modo máximo gera mais calor.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Verificação do Ceph ignorada pelo sinalizador do usuário (--ignore-ceph-check)", @@ -883,6 +890,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Escolha uma programação (sintaxe de evento de calendário do systemd):", "Choose a script to execute:": "Escolha um script para executar:", "Choose a server:": "Escolha um servidor:", + "Choose a setting. Changes are applied only after the final review.": "Escolha o que configurar. As alterações só são aplicadas após a revisão final.", "Choose a share to mount:": "Escolha um compartilhamento para montar:", "Choose a specific supported GPU or USB node": "Escolha um GPU ou nó USB específico compatível", "Choose action for this controller/NVMe:": "Escolha a ação para este controlador/NVMe:", @@ -1520,6 +1528,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "Inventário NVIDIA atual resolvido: uma atualização é required", "Current NVIDIA inventory resolved: no refresh is required": "Inventário NVIDIA atual resolvido: nenhuma atualização é required", "Current Network Configuration": "Configuração de rede atual", + "Current PBS Version": "versão atual do PBS", "Current PVE Version": "Versão atual do PVE", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Os scripts atuais do host ProxMenux registram compartilhamentos remotos como armazenamentos Proxmox usando pvesm.", "Current ProxMenux version:": "Versão atual do ProxMenux:", @@ -1634,8 +1643,8 @@ "Default Proxmox logo applied": "Logotipo padrão do Proxmox aplicado", "Default is none (video-only passthrough). Use SPACE to toggle selections.": "O padrão é nenhum (passagem somente de vídeo). Use ESPAÇO para alternar as seleções.", "Default journald configuration restored": "Configuração padrão do diário restaurada", - "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "O local padrão é /mnt/. O compartilhamento será montado aqui no host com permissões abertas para que um LXC sem privilégios possa montar e gravar nele. Para acesso LXC, monte este caminho com o LXC Mount Manager.", - "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "O local padrão é /mnt/. O compartilhamento será montado aqui no host. Use este caminho em /etc/fstab. Para acesso LXC, monte este caminho com o LXC Mount Manager.", + "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "A localização predefinida é /mnt/. A partilha será montada aqui no host com permissões abertas, para que um LXC sem privilégios a possa montar por bind mount e nela escrever. Utilize o LXC Mount Manager para criar essa montagem no LXC.", + "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "A localização predefinida é /mnt/. A partilha será montada aqui no host. Utilize este caminho em /etc/fstab. Para aceder a partir de um LXC, crie um bind mount deste caminho com o LXC Mount Manager.", "Default login": "Utilizador por omissão", "Default options": "Opções padrão", "Default options read/write": "Opções padrão leitura/gravação", @@ -1685,6 +1694,7 @@ "Detected Coral hardware:": "Hardware Coral detectado:", "Detected GPU(s):": "GPU(s) detectada(s):", "Detected Intel CPU": "CPU Intel detectada", + "Detected Proxmox Backup Server": "Proxmox Backup Server detectado", "Detected Proxmox VE": "Proxmox VE detectado", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Detectado Proxmox VE 8.x - Instalando Ceph Squid para Debian", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Detectado Proxmox VE 9.x - Instalando Ceph Squid para Debian Trixie", @@ -1766,6 +1776,9 @@ "Disabling and stopping rpcbind service and socket...": "Desativando e parando o serviço e soquete rpcbind...", "Disabling non-free firmware warnings...": "Desativando avisos de firmware não-livre...", "Disabling portmapper/rpcbind for security...": "Desativando portmapper/rpcbind por segurança...", + "Discard changes": "Descartar alterações", + "Discard unapplied changes?": "Descartar as alterações por aplicar?", + "Discover NFS exports on a server": "Descobrir as exportações NFS de um servidor", "Discover targets on portal:": "Descubra alvos no portal:", "Discovering iSCSI targets...": "Descobrindo alvos iSCSI...", "Discovery successful": "Descoberta bem-sucedida", @@ -1845,6 +1858,7 @@ "Do you want to proceed with the system update?": "Deseja prosseguir com a atualização do sistema?", "Do you want to proceed?": "Você quer prosseguir?", "Do you want to reboot now?": "Você quer reiniciar agora?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "Deseja remover o banner de assinatura do Proxmox da interface web do Proxmox Backup Server?", "Do you want to remove the stale entry from vfio.conf and continue?": "Deseja remover a entrada obsoleta do vfio.conf e continuar?", "Do you want to remove the stale entry from vfio.conf?": "Você deseja remover a entrada obsoleta do vfio.conf?", "Do you want to restart the network service now to apply changes?": "Deseja reiniciar o serviço de rede agora para aplicar as alterações?", @@ -2022,7 +2036,7 @@ "Enter VM ID:": "Insira o ID da VM:", "Enter VM/CT ID:": "Insira o ID da VM/CT:", "Enter ZFS pool name for the selected disk:": "Insira o nome do pool ZFS para o disco selecionado:", - "Enter a name for the mount point (used as /mnt/):": "Insira um nome para o ponto de montagem (usado como /mnt/):", + "Enter a name for the mount point (used as /mnt/):": "Introduza um nome para o ponto de montagem (será usado como /mnt/):", "Enter a name for the new virtual machine:": "Insira um nome para a nova máquina virtual:", "Enter a non-empty password or access token on a single line.": "Insira uma senha ou token de acesso não vazio em uma única linha.", "Enter a number, or write or paste a command:": "Digite um número ou escreva ou cole um comando:", @@ -2166,11 +2180,11 @@ "Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Exemplo: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak", "Example: daily or Mon..Fri 03:00": "Exemplo: diariamente ou Seg..Sex 03:00", "Example: mkdir -p /tmp/lxc_convert_114": "Exemplo: mkdir -p /tmp/lxc_convert_114", - "Example: pct config 110 | grep unprivileged": "Exemplo: configuração pct 110 | grep sem privilégios", + "Example: pct config 110 | grep unprivileged": "Exemplo: pct config 110 | grep unprivileged", "Example: pct stop 110": "Exemplo: pct stop 110", - "Example: pct stop 114": "Exemplo: parada pct 114", + "Example: pct stop 114": "Exemplo: pct stop 114", "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup": "Exemplo: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup", - "Example: pvesm path local-lvm:vm-114-disk-0": "Exemplo: caminho pvesm local-lvm:vm-114-disk-0", + "Example: pvesm path local-lvm:vm-114-disk-0": "Exemplo: pvesm path local-lvm:vm-114-disk-0", "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf": "Exemplo: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf", "Examples:": "Exemplos:", "Execute destructive rollback?": "Executar reversão destrutiva?", @@ -2270,6 +2284,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "Falha ao clonar o repositório log2ram. Verifique /tmp/log2ram_install.log", "Failed to configure EFI disk": "Falha ao configurar o disco EFI", "Failed to configure IOMMU automatically.": "Falha ao configurar o IOMMU automaticamente.", + "Failed to configure Proxmox Backup Server repositories": "Falha ao configurar os repositórios do Proxmox Backup Server", "Failed to configure Proxmox repositories": "Falha ao configurar repositórios Proxmox", "Failed to configure TPM device in VM": "Falha ao configurar o dispositivo TPM na VM", "Failed to configure repositories.": "Falha ao configurar repositórios.", @@ -2481,7 +2496,7 @@ "For a consistent export, the VM should be stopped.": "Para uma exportação consistente, a VM deve ser interrompida.", "For any question, issue or bug report, please use the official repository": "Para qualquer dúvida, problema ou relatório de bug, use o repositório oficial", "For dedicated GPUs, FLR is required by this policy to reduce VM": "Para GPUs dedicadas, o FLR é exigido por esta política para reduzir VM", - "For example: pct enter 101": "Por exemplo: pct digite 101", + "For example: pct enter 101": "Por exemplo: pct enter 101", "For guest shares add: --options guest": "Para compartilhamentos de convidados, adicione: --options guest", "For security reasons, the system cannot format it.": "Por motivos de segurança, o sistema não pode formatá-lo.", "For unprivileged containers, use instead:": "Para contêineres sem privilégios, use:", @@ -2893,8 +2908,8 @@ "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.": "Se as notificações estiverem habilitadas (Telegram/Discord/ntfy/...), você receberá uma mensagem \"Restauração do host concluída\" quando todas as tarefas em segundo plano forem concluídas.", "If passthrough fails on Windows: install RadeonResetBugFix.": "Se a passagem falhar no Windows: instale RadeonResetBugFix.", "If path not accessible (ZFS/BTRFS):": "Se o caminho não estiver acessível (ZFS/BTRFS):", - "If pvesm path returned a DEVICE (LVM):": "Se o caminho pvesm retornou um DEVICE (LVM):", - "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Se o caminho pvesm retornou um DIRETÓRIO (ZFS/BTRFS):", + "If pvesm path returned a DEVICE (LVM):": "Se pvesm path devolveu um DISPOSITIVO (LVM):", + "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Se pvesm path devolveu um DIRETÓRIO (ZFS/BTRFS):", "If repositories error:": "Se houver erro nos repositórios:", "If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Se o contêiner já tiver sido alterado, seu backup nativo anterior será restaurado. Os diretórios de host compartilhados não são revertidos. Os discos da tentativa fracassada serão removidos quando a limpeza for bem-sucedida.", "If the host freezes, remove hostpci entries from": "Se o host congelar, remova as entradas hostpci de", @@ -2977,6 +2992,7 @@ "Inaccessible device during VM startup": "Dispositivo inacessível durante a inicialização da VM", "Inactive": "Inativo", "Inactive (entry in fstab, not currently mounted)": "Inativo (entrada no fstab, não montada no momento)", + "Include these changes in oci-compose?": "Incluir essas alterações no oci-compose?", "Include these resources in oci-compose?": "Incluir esses recursos no oci-compose?", "Includes /etc/network (may drop SSH immediately)": "Inclui /etc/network (pode descartar o SSH imediatamente)", "Includes /etc/zfs": "Inclui /etc/zfs", @@ -3033,7 +3049,7 @@ "Initiator IQN is authorised on the target": "O iniciador IQN está autorizado no alvo", "Initiator IQN:": "IQN do iniciador:", "Inkscape is professional quality vector graphics software which runs on Linux, Mac OS X and Windows desktop computers.": "Inkscape é um software gráfico vetorial de qualidade profissional que funciona em computadores desktop Linux, Mac OS X e Windows.", - "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "Dentro do LXC, crie o administrador: console kimai:user:create Your USERNAME Your EMAIL ROLE_SUPER_ADMIN", + "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "Dentro do LXC, crie o administrador: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN", "Inspect disks before any action": "Inspecione os discos antes de qualquer ação", "Inspect host device nodes": "Inspecione os nós do dispositivo host", "Inspect passthrough/kernel events": "Inspecionar eventos de passagem/kernel", @@ -3210,9 +3226,11 @@ "Interrupted operation:": "operation interrompida:", "Interrupted stack operation found": "Pilha interrompida operation encontrada", "Invalid 'proxmox-ve' candidate (not 9.x or none). Please verify your repository configuration and network, then retry.": "Candidato 'proxmox-ve' inválido (não 9.x ou nenhum). Verifique a configuração e a rede do seu repositório e tente novamente.", - "Invalid ALLOWED_HOSTS value": "Valor Allowed hosts inválido", + "Invalid ALLOWED_HOSTS value": "Valor de ALLOWED_HOSTS inválido", "Invalid Home Assistant OS check timeout": "Tempo de verificação do sistema operacional Home Assistant inválido", "Invalid ID": "ID inválido", + "Invalid IPv4 address": "Endereço IPv4 inválido", + "Invalid IPv4 gateway": "Gateway IPv4 inválido", "Invalid Intel render path": "Caminho de renderização Intel inválido", "Invalid Jellyfin path:": "Caminho Jellyfin inválido:", "Invalid MAC address:": "Endereço MAC inválido:", @@ -3245,6 +3263,7 @@ "Invalid administrator email": "E- mail de administrador inválido", "Invalid administrator user name": "Nome de usuário do administrador inválido", "Invalid base VMID": "VMID base inválida", + "Invalid bridge": "Bridge inválida", "Invalid check package:": "Pacote de verificação inválido:", "Invalid configuration path:": "Caminho de configuração inválido:", "Invalid consume/export volumes": "Volumes de consumo/exportação inválidos", @@ -3308,6 +3327,7 @@ "Invalid remote name": "Nome remoto inválido", "Invalid remote path": "Caminho remoto inválido", "Invalid repair version:": "Versão de reparação inválida:", + "Invalid resource value": "Valor de recurso inválido", "Invalid resources": "Recursos inválidos", "Invalid restored volume path": "Caminho de volume restaurado inválido", "Invalid running state": "Estado de execução inválido", @@ -3326,6 +3346,7 @@ "Invalid stack members": "Membros inválidos da pilha", "Invalid stack name": "Nome da pilha inválido", "Invalid stack operation": "Pilha inválida operation", + "Invalid storage": "Armazenamento inválido", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "ID de armazenamento inválido. Use apenas letras, números, hífens e sublinhados.", "Invalid storage name": "Nome de armazenamento inválido", "Invalid suite application": "Aplicativo de suite inválido", @@ -3440,7 +3461,7 @@ "Keyfile removed.": "arquivo-chave removido.", "Keyrings method failed; trying apt-key fallback": "O método dos chaveiros falhou; tentando substituto do apt-key", "KiCad - A Cross Platform and Open Source Electronics Design Automation Suite.": "KiCad - Uma plataforma cruzada e Open Source Electronics Design Automation Suite.", - "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai não tem conta padrão. Digite o recipiente com: pct enter {main vmid}", + "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "O Kimai não tem uma conta predefinida. Aceda ao contentor com: pct enter {main_vmid}", "Kimai is a professional grade time-tracking application, free and open-source.": "Kimai é um aplicativo de rastreamento de tempo profissional, gratuito e de código aberto.", "Kometa is a powerful tool designed to give you complete control over your media libraries. With Kometa, you can take your customization to the next level, with granular control over metadata, collections, overlays, and much more.": "Kometa é uma ferramenta poderosa projetada para lhe dar controle completo sobre suas bibliotecas de mídia. Com o Kometa, você pode levar sua personalização para o próximo nível, com controle granular sobre metadados, coleções, sobreposições e muito mais.", "Kometa reads its configuration from /config/config.yml and the container only ships /config/config.yml.template. Copy the template to config.yml, fill in the required Plex and TMDb connections, then restart the container.": "Kometa lê a sua configuração a partir de /config/config.yml e o recipiente apenas navios /config/config.yml.template. Copie o modelo para config.yml, preencha o requi vermelho Plex e conexões TMDb, em seguida, reinicie o recipiente.", @@ -3492,7 +3513,7 @@ "Leave empty to keep every backup (not recommended).": "Deixe em branco para manter todos os backups (não recomendado).", "Leave empty to use the retention of the storage.": "Deixe em branco para usar a retenção do armazenamento.", "Legacy PVE 8 .list files commented or not present": "Arquivos .list PVE 8 legados comentados ou não presentes", - "Legacy ceph.list commented or not present": "Ceph.list legado comentado ou não presente", + "Legacy ceph.list commented or not present": "ceph.list antigo comentado ou inexistente", "Legacy gasket-dkms cleanup could not be verified as complete.": "Não foi possível confirmar a conclusão da limpeza do pacote gasket-dkms legado.", "Legacy gasket-dkms detected": "gasket-dkms legado detetado", "Legacy network tools (e.g., ifconfig)": "Ferramentas de rede legadas (por exemplo, ifconfig)", @@ -4157,6 +4178,7 @@ "No active Proxmox storage accepts OCI template archives.": "Nenhum armazenamento Proxmox ativo aceita arquivos de modelo OCI.", "No active exports or showmount not available": "Nenhuma exportação ativa ou showmount não disponível", "No active session": "Nenhuma sessão ativa", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "Nenhuma assinatura ativa e nenhum repositório PBS utilizável. O modo não interativo não pode alterar fontes APT;configure-os em Administração > Repositórios.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "Nenhuma assinatura ativa e nenhum repositório PVE utilizável. O modo não interativo não pode alterar fontes APT; configure-as em Nó > Atualizações > Repositórios.", "No additional GPU can be added.": "Nenhuma GPU adicional pode ser adicionada.", "No additional device needs to be added.": "Nenhum dispositivo adicional precisa ser adicionado.", @@ -4242,7 +4264,7 @@ "No jobs": "Sem empregos", "No language selected.": "Nenhum idioma selecionado.", "No local disk storage configured.": "Nenhum armazenamento em disco local configurado.", - "No main sources.list present (skipped)": "Nenhuma fonte principal.list presente (ignorada)", + "No main sources.list present (skipped)": "Não existe um sources.list principal (ignorado)", "No matches for:": "Nenhuma correspondência para:", "No matching PVE vzdump jobs were found for the PBS backend.": "Nenhum trabalho PVE vzdump correspondente foi encontrado para o backend do PBS.", "No matching PVE vzdump jobs were found for the local archive backend.": "Nenhum trabalho PVE vzdump correspondente foi encontrado para o back-end do arquivo local.", @@ -4261,6 +4283,7 @@ "No password provided.": "Nenhuma senha fornecida.", "No paths available": "Nenhum caminho disponível", "No paths selected. Select at least one path.": "Nenhum caminho selecionado. Selecione pelo menos um caminho.", + "No pending changes": "Sem alterações pendentes", "No pending paths to schedule for reboot.": "Não há caminhos pendentes para agendar a reinicialização.", "No permanent NFS mounts configured.": "Nenhuma montagem NFS permanente configurada.", "No permanent Samba mounts configured.": "Nenhuma montagem permanente do Samba configurada.", @@ -4402,6 +4425,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM removido (artefatos de marionetes podem precisar de limpeza manual)", "OVH server detected": "Servidor OVH detectado", "OVH server detection and RTM installation process completed": "Processo de detecção do servidor OVH e instalação RTM concluído", + "Object detector": "Detetor de objetos", "Object detector for Frigate": "Detector de objetos para Frigate", "Observer is not responding on port 4357": "Observador não está respondendo na porta 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian é um aplicativo de anotações que permite criar, vincular e organizar suas notas em seu dispositivo, com centenas de plugins e temas para personalizar seu fluxo de trabalho. Você também pode publicar suas notas online, acessá-las offline e sincronizá-las com segurança com criptografia de ponta a ponta.", @@ -4874,12 +4898,17 @@ "Proxmology logo applied": "Logotipo da Proxmologia aplicado", "Proxmox 9 system update allready": "Atualização do sistema Proxmox 9 já", "Proxmox APT repositories configured": "repositórios Proxmox APT configurados", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Destinos Proxmox Backup Server (PBS)", + "Proxmox Backup Server APT repositories configured": "Repositórios APT do Proxmox Backup Server configurados", + "Proxmox Backup Server Update": "Atualização do Proxmox Backup Server", + "Proxmox Backup Server update completed.": "Atualização do Proxmox Backup Server concluída.", "Proxmox CIFS Storage Status:": "Status de armazenamento CIFS do Proxmox:", "Proxmox NFS Storage Status:": "Status de armazenamento NFS do Proxmox:", "Proxmox System Update": "Atualização do sistema Proxmox", "Proxmox Update": "Atualização Proxmox", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "A atualização de pacotes do Proxmox VE 8 falhou. Reveja a saída do APT antes de tentar novamente.", "Proxmox VE 8 system update completed successfully": "Atualização do sistema Proxmox VE 8 concluída com sucesso", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Guia de atualização manual do Proxmox VE 8 a 9", "Proxmox VE 9.x no-subscription repository created": "Repositório sem assinatura Proxmox VE 9.x criado", @@ -4887,6 +4916,7 @@ "Proxmox VE configuration completed.": "Configuração do Proxmox VE concluída.", "Proxmox VE safe update completed": "Atualização segura do Proxmox VE concluída", "Proxmox auth logger service created and started": "Serviço de registrador de autenticação Proxmox criado e iniciado", + "Proxmox could not apply the container settings": "O Proxmox não conseguiu aplicar as definições do contentor", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "O repositório corporativo Proxmox pode estar ausente ou inacessível. Tentando mudar para sem assinatura...", "Proxmox filter configured": "Filtro Proxmox configurado", "Proxmox iSCSI Storage Status:": "Status de armazenamento iSCSI do Proxmox:", @@ -5153,6 +5183,8 @@ "Remove saved credentials": "Remover credenciais salvas", "Remove subscription banner": "Remover banner de assinatura", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Remover o aplicativo? Seus discos contêineres são excluídos e somente um backup pode recuperá-los.", + "Remove the lock left on a VM by an interrupted task": "Remover o bloqueio deixado numa VM por uma tarefa interrompida", + "Remove the lock left on a container by an interrupted task": "Remover o bloqueio deixado num contentor por uma tarefa interrompida", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Remover as credenciais salvas para {registry}?\nOs aplicativos em execução continuarão funcionando.", "Remove the unprivileged flag from configuration:": "Remova o sinalizador sem privilégios da configuração:", "Remove unused packages and their config": "Remova pacotes não utilizados e suas configurações", @@ -5234,6 +5266,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "Auxiliar de repositório indisponível. A instalação do NVIDIA foi interrompida.", "Repository policy returned an unexpected result.": "A política do repositório retornou um resultado inesperado.", "Repository ready.": "Repositório pronto.", + "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.": "as fontes do repositório foram alteradas, mas a atualização da lista de pacotes APT falhou. Operação interrompida;fontes não foram revertidas. Inspecione Administração > Repositórios e tente apt-get update novamente antes de continuar.", "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.": "As fontes do repositório foram alteradas, mas a atualização da lista de pacotes APT falhou. Operação interrompida; as fontes não foram revertidas. Verifique Nó > Atualizações > Repositórios e tente apt-get update novamente antes de continuar.", "Repository switch declined; no APT source changed.": "Troca de repositório recusada; nenhuma fonte APT foi alterada.", "Repository:": "Repositório:", @@ -5328,6 +5361,7 @@ "Reverting ZFS ARC tuning...": "Revertendo ajuste do ZFS ARC...", "Reverting pigz wrapper...": "Revertendo o wrapper pigz...", "Reverting vzdump speed tuning...": "Revertendo o ajuste de velocidade do vzdump...", + "Review and apply": "Rever e aplicar", "Review external OCI changes": "revisar alterações externas do OCI", "Review package upgrade": "Rever a atualização de pacotes", "Review passthrough config files": "Revise os arquivos de configuração de passagem", @@ -5787,6 +5821,7 @@ "ShareCoveX panel": "Painel ShareCoveX", "Shared Directory Ready:": "Diretório compartilhado pronto:", "Shared Group": "Grupo Compartilhado", + "Shared directories": "Diretórios partilhados", "Shared directory created:": "Directório partilhado criado:", "Shared directory for consume and export": "Diretório compartilhado para consumir e exportar", "Shared directory for copy/sync operations": "Diretório compartilhado para copiar/sincronizar operations", @@ -5816,6 +5851,7 @@ "Should show pve-manager/9.x.x": "Deve mostrar pve-manager/9.x.x", "Shoutrrr addresses for the notifications (optional)": "Endereços Shoutrrr para as notificações (opcional)", "Show All Scripts": "Mostrar todos os scripts", + "Show CT users and their UID/GID on the host": "Utilizadores do CT e UID/GID no host", "Show CT users in table format": "Mostrar usuários do CT em formato de tabela", "Show Container Privilege Status": "Mostrar status de privilégio do contêiner", "Show DNS resolution status": "Mostrar status de resolução DNS", @@ -5977,6 +6013,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Inicie a VM para iniciar a instalação do Windows a partir do ISO montado.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Iniciar os aplicativos assim que estiverem registrados? Responda Não se os contêineres originais ainda estiverem em execução em outro host: ambos usariam os mesmos endereços.", "Start the converted container:": "Inicie o contêiner convertido:", + "Start the interactive Proxmox package upgrade": "Iniciar a atualização interativa de pacotes do Proxmox", "Start the main system upgrade:": "Inicie a atualização principal do sistema:", "Start the stack with Proxmox": "Iniciar a pilha com Proxmox", "Start uploading to PBS — sets a recovery passphrase": "comece a enviar para o PBS – define uma senha de recuperação", @@ -6256,6 +6293,7 @@ "The Paperless persistent volumes need at least 8 GB": "Os volumes persistentes sem papel precisam de pelo menos 8 GB", "The PostgreSQL volume needs at least 4 GB": "O volume PostgreSQL precisa de pelo menos 4 GB", "The PostgreSQL volume needs at least 8 GB": "O volume PostgreSQL precisa de pelo menos 8 GB", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "A atualização do Proxmox VE 8 não terminou. A atualização para o Proxmox VE 9 não foi iniciada.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "O chaveiro de arquivo Proxmox está faltando;A instalação do Ceph não pode continuar com segurança", "The Proxmox inventory and the local configurations differ": "O inventário Proxmox e as configurações locais diferem", "The ROCm image has no support for the AMD GPU of this host:": "A imagem ROCm não tem suporte para a GPU AMD deste host:", @@ -6326,6 +6364,7 @@ "The build could not be checked beforehand; continuing without that check.": "A compilação não pôde ser verificada antecipadamente;continuando sem essa verificação.", "The cached image does not match the current digest": "A imagem em cache não corresponde à digest atual", "The cached image is damaged; it will be downloaded again.": "A imagem em cache está danificada; será transferida novamente.", + "The changes need the application installer": "As alterações requerem o instalador da aplicação", "The character device does not exist:": "O dispositivo de caracteres não existe:", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "O comando pede uma senha que não seja ecoada. Depois de criar os usuários, a interface web pede um nome de usuário e uma senha.", "The command does not name an image": "O comando não nomeia uma imagem", @@ -6445,8 +6484,10 @@ "The host bind source does not exist:": "O código de ligação da máquina não existe:", "The host bind source is not a regular file or directory:": "O código de ligação da máquina não é um ficheiro ou directório regular:", "The host directory changed before it was mounted": "A pasta da máquina mudou antes de ser montada", + "The host directory does not exist": "O diretório do anfitrião não existe", "The host directory may not be accessible from an unprivileged container.": "O diretório host pode não estar acessível a partir de um contêiner sem privilégios.", "The host has no IPv4 address on the selected bridge": "A máquina não tem endereço IPv4 na ponte seleccionada", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "O host perde o acesso a eles. Se o grupo incluir um controlador de rede ou de armazenamento de que o host precisa, cancele no resumo.", "The host monitor does not see the real host memory": "O monitor da máquina não vê a memória verdadeira da máquina", "The host monitor does not share this host namespace:": "O monitor da máquina não compartilha este espaço de nomes da máquina:", "The host monitor include is unknown or differs from the saved record": "O include do monitor do host é desconhecido ou difere do registro salvo", @@ -6636,6 +6677,7 @@ "The repair must preserve the image dependencies:": "O reparo deve preservar as dependências da imagem:", "The requested VMID block is already in use": "O bloco VMID solicitado já está em uso", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "O perfil de aprendizado de máquina solicitado GPU não está funcionando; não é substituído por CPU", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "Não foi possível reverter totalmente a alteração dos recursos. Volte a abrir o menu de gestão para rever e importar as definições atuais do Proxmox.", "The restored service did not pass its health check": "O serviço restaurado não passou no seu exame de saúde", "The restored service stopped; the recovery is not confirmed": "O serviço restaurado parou; a recuperação não é confirmada", "The reviewed Tandoor stack does not require a privileged LXC.": "A pilha Tandoor revisada não require um LXC privilegiado.", @@ -6681,6 +6723,7 @@ "The selected VM": "A VM selecionada", "The selected VM is running.": "A VM selecionada está em execução.", "The selected base folder does not exist and could not be created:": "A pasta base selecionada não existe e não pôde ser criada:", + "The selected bridge does not exist": "A bridge selecionada não existe", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "A ponte selecionada não possui sub-rede IPv4 para o firewall do monitor host", "The selected configuration needs to start the LXC during the installation": "A configuração selecionada precisa iniciar o LXC durante a instalação", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "O contêiner selecionado não tem privilégios. Um contêiner privilegiado é necessário para passagem direta do dispositivo.", @@ -6703,6 +6746,7 @@ "The service builds its own image; only a published image can be installed": "O serviço constrói sua própria imagem; somente uma imagem publicada pode ser instalada", "The service declares no image:": "O serviço não declara imagem:", "The setting has no final value:": "A configuração não tem valor final:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Não foi possível repor todas as definições. Reveja o contentor e a alteração guardada antes de continuar.", "The share already exists in smb.conf:": "O compartilhamento já existe em smb.conf:", "The shared destination is not a directory": "O destino compartilhado não é um diretório", "The shared directory points to a protected host path": "A pasta partilhada aponta para uma localização protegida da máquina", @@ -6770,19 +6814,24 @@ "These Proxmox resources were added outside ProxMenux:": "Estes recursos Proxmox foram adicionados fora do ProxMenux:", "These VMIDs are not free:": "Estes VMIDs não são gratuitos:", "These are the changes that will be made": "Estas são as mudanças que serão feitas", + "These changes were made in Proxmox outside ProxMenux:": "Essas alterações foram feitas no Proxmox fora do ProxMenux:", "These container mount paths have an extension-like suffix:": "Estes caminhos de montagem do contêiner têm um sufixo parecido com uma extensão:", "These containers were restored from a backup and this host has no record of their OCI application:": "Esses contêineres foram restaurados a partir de um backup e este host não possui registro de seu aplicativo OCI:", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Estes dispositivos partilham o grupo IOMMU da GPU selecionada e serão atribuídos à VM juntamente com ela:", "These host backups run with this job and expect its storage:": "Esses backups de host são executados com este trabalho e esperam seu armazenamento:", "These host backups run with this job and will stop running:": "Esses backups de host são executados com este trabalho e deixarão de ser executados:", "These interface configurations will be removed": "Essas configurações de interface serão removidas", "These manual changes cannot be adopted safely:": "Estas alterações manuais não podem ser adotadas com segurança:", "These paths will not be restored live and will be extracted for manual recovery.": "Esses caminhos não serão restaurados ao vivo e serão extraídos para recuperação manual.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Estas definições requerem uma adaptação específica da aplicação. A imagem não foi transferida e o contentor não foi recriado.", "These values are asked during the installation:": "Estes valores são questionados durante a instalação:", "They are described in the image documentation:": "Estão descritos na documentação da imagem:", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Eles serão adicionados ao oci-compose antes de continuar. Recursos não suportados ou alterados não são importados.", + "They will be saved in oci-compose before continuing.": "Elas serão salvas no oci-compose antes de continuar.", "This CIFS share is mounted with restrictive permissions.": "Este compartilhamento CIFS é montado com permissões restritivas.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Esta GPU é considerada incompatível com a passagem de GPU para uma VM no ProxMenux.", "This NFS share is fully restricted — even the host root cannot write to it.": "Este compartilhamento NFS é totalmente restrito — até mesmo o host root não pode gravar nele.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Este Proxmox Backup Server não tem assinatura ativa. Mudar para o repositório sem assinatura? A fonte Enterprise do PBS, inacessível, será desativada.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Esta VM requer etapas extras de instalação. Consulte o guia de instalação em:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Esta conta é para {registry}. Insira uma imagem desse registro.", "This action will:": "Esta ação irá:", @@ -6866,6 +6915,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Este script não pode ser executado no terminal web Proxmox.", "This script must be run as root.": "Este script deve ser executado como root.", "This script must be run on a Proxmox host.": "Este script deve ser executado em um host Proxmox.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Este script aplicará as seguintes otimizações e ajustes avançados ao seu Proxmox Backup Server", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Este script aplicará as seguintes otimizações e ajustes avançados ao seu servidor Proxmox VE", "This script will update your Proxmox VE system with advanced options:": "Este script atualizará seu sistema Proxmox VE com opções avançadas:", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Esta sessão está sendo executada no terminal Monitor. Executá-lo a partir daqui cortaria a conexão no meio da instalação e deixaria o switch quebrado.", @@ -6952,7 +7002,7 @@ "Transaction log:": "Registo de transacções:", "Translation files:": "Arquivos de tradução:", "Transmission is designed for easy, powerful use. Transmission has the features you want from a BitTorrent client: encryption, a web interface, peer exchange, magnet links, DHT, µTP, UPnP and NAT-PMP port forwarding, webseed support, watch directories, tracker editing, global and per-torrent speed limits, and more.": "Transmission é projetado para uso fácil e poderoso. Transmission tem os recursos que você deseja de um cliente BitTorrent: criptografia, uma interface web, troca de pares, links magnéticos, DHT, μTP, encaminhamento de portas UPnP e NAT-PMP, suporte a webseed, diretórios de relógio, edição de rastreadores, limites de velocidade globais e per-torrent, e muito mais.", - "Tried pvesm path and manual detection methods": "Tentei caminho pvesm e métodos de detecção manual", + "Tried pvesm path and manual detection methods": "Foram tentados pvesm path e os métodos de deteção manual", "Trust this certificate and save it for scheduled backups?": "Confiar neste certificado e salvá-lo para backups agendados?", "Try Again": "Tente novamente", "Try a different search term.": "Experimente um termo de pesquisa diferente.", @@ -6997,6 +7047,7 @@ "Unable to identify the root filesystem. Aborting.": "Não foi possível identificar o sistema de arquivos raiz. Abortando.", "Unable to resolve system ZFS pool disks. Aborting.": "Não foi possível resolver os discos do pool ZFS do sistema. Abortando.", "Unable to resolve system disk topology. Aborting.": "Não é possível resolver a topologia do disco do sistema. Abortando.", + "Unapplied changes": "Alterações por aplicar", "Understand the security implications of privileged containers": "Entenda as implicações de segurança de contêineres privilegiados", "Unexpected Proxmox inventory; recovery blocked": "Inesperado inventário Proxmox; recuperação bloqueada", "Unexpected formatting directory in the new Valkey volume": "Directório de formatação inesperado no novo volume Valkey", @@ -7112,6 +7163,7 @@ "Unsupported mount type": "Tipo de montagem não suportado", "Unsupported mount type:": "Tipo de montagem não suportado:", "Unsupported native device type:": "Tipo de dispositivo nativo não suportado:", + "Unsupported network settings": "Definições de rede não suportadas", "Unsupported operation": "operation não suportado", "Unsupported or unknown Proxmox version; no repository changed.": "Versão Proxmox não suportada ou desconhecida; nenhum repositório foi alterado.", "Unsupported output format:": "Formato de saída não suportado:", @@ -7170,7 +7222,7 @@ "Updates file is empty or unreadable.": "O arquivo de atualizações está vazio ou ilegível.", "Updating": "Actualização", "Updating APT package lists...": "Atualizando listas de pacotes APT...", - "Updating Debian Bookworm → Trixie in sources.list...": "Atualizando Debian Bookworm → Trixie em fontes.list...", + "Updating Debian Bookworm → Trixie in sources.list...": "A atualizar Debian Bookworm → Trixie em sources.list...", "Updating Figurine binary...": "Atualizando o binário da estatueta...", "Updating Lynis to the latest version...": "Atualizando Lynis para a versão mais recente...", "Updating PVE application manager...": "Atualizando o gerenciador de aplicativos PVE...", @@ -7192,6 +7244,7 @@ "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Assistente de atualização: Proxmox VE 8 → 9 (Trixie)", "Upgrade canceled by user": "Atualização cancelada pelo usuário", "Upgrade one node at a time": "Atualize um nó por vez", + "Upgrade the system, offering the": "Atualize o sistema, oferecendo o", "Upgrade to Proxmox VE 9": "Atualize para Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "Atualizando utilitários NVIDIA (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "Carregar uma cópia criptografada da chave para o PBS para que você possa recuperá-la em um host reinstalado com apenas uma senha?", @@ -7629,6 +7682,7 @@ "active VF(s)": "FV ativa(s)", "active VFs": "FVs ativas", "active Virtual Functions. Changing its driver binding would destroy every VF.": "Funções virtuais ativas. Alterar a ligação do driver destruiria todos os VF.", + "added in Proxmox": "adicionada no Proxmox", "after copy; source tree is incomplete.": "após cópia; a árvore de origem está incompleta.", "after update.": "após a atualização.", "all guests": "todos os convidados", @@ -7973,8 +8027,8 @@ "pvesm command not found. This should not happen on Proxmox.": "Comando pvesm não encontrado. Isso não deveria acontecer no Proxmox.", "pvesm not available.": "pvesm não disponível.", "pvesm not found.": "pvesm não encontrado.", - "pvesm path failed, trying manual detection...": "caminho pvesm falhou, tentando detecção manual...", - "pvesm status failed": "status pvesm falhou", + "pvesm path failed, trying manual detection...": "O pvesm path falhou; a tentar a deteção manual...", + "pvesm status failed": "O pvesm status falhou", "pyLoad is a Free and Open Source download manager written in Python and designed to be extremely lightweight, easily extensible and fully manageable via web.": "pyLoad é um gerenciador de download livre e Open Source escrito em Python e projetado para ser extremamente leve, facilmente extensível e totalmente gerenciável via web.", "pyLoad web interface": "interface web pyLoad", "qBittorrent WebUI password (user: admin)": "Senha do WebUI do qBittorrent (utilizador: administrador)", @@ -7996,6 +8050,7 @@ "remapped users": "usuários remapeados", "remove the (now-empty) directory if possible": "remova o diretório (agora vazio), se possível", "removed from Proxmox": "removido do Proxmox", + "removed in Proxmox": "removida no Proxmox", "removed successfully from Proxmox.": "removido com sucesso do Proxmox.", "requires running the official installer, which restarts the Monitor service.": "requer a execução do instalador oficial, que reinicia o serviço Monitor.", "requires the package": "requer o pacote", @@ -8033,7 +8088,7 @@ "smbclient command is not working properly.": "O comando smbclient não está funcionando corretamente.", "smbclient command not found after installation.": "Comando smbclient não encontrado após a instalação.", "sources.list update skipped (no change)": "Atualização de sources.list ignorada (sem alteração)", - "sources.list updated to Trixie": "fontes.list atualizado para Trixie", + "sources.list updated to Trixie": "sources.list atualizado para Trixie", "ssh-keygen failed. Cannot create a new SSH key.": "ssh-keygen falhou. Não é possível criar uma nova chave SSH.", "stale entry/entries for interfaces no longer present": "entradas/entradas obsoletas para interfaces não estão mais presentes", "standard performance": "desempenho padrão", @@ -8052,7 +8107,7 @@ "switch_gpu_mode.sh was not found.": "switch_gpu_mode.sh não foi encontrado.", "sysfs ROM dump failed — trying ACPI VFCT table...": "Falha no despejo de ROM do sysfs - tentando tabela ACPI VFCT...", "syslog-ng allows you to flexibly collect, parse, classify, rewrite and correlate logs from across your infrastructure and store or route them to log analysis tools.": "o syslog-ng permite coletar, analisar, classificar, reescrever e correlacionar logs de toda sua infraestrutura e armazená-los ou roteá-los para ferramentas de análise de log.", - "systemctl restart networking failed:": "falha na reinicialização da rede do systemctl:", + "systemctl restart networking failed:": "O systemctl restart networking falhou:", "systemd OnCalendar expression": "Expressão Systemd OnCalendar", "the API key was not generated on the first start": "a tecla API não foi gerada no primeiro início", "the NVIDIA hook of the container is not available:": "o hook NVIDIA do contêiner não está disponível:", @@ -8099,18 +8154,20 @@ "using": "usando", "using direct path (not guaranteed to survive reboots).": "usando caminho direto (não é garantido que sobreviva às reinicializações).", "vCPUs:": "vCPUs:", + "value changed in Proxmox": "valor alterado no Proxmox", "vfio-pci IDs configured": "IDs vfio-pci configurados", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "IDs vfio-pci em /etc/modprobe.d/vfio.conf", "video": "vídeo", "video + recognition": "vídeo + reconhecimento", "video only": "apenas vídeo", "vzdump backup speed optimization completed": "otimização da velocidade de backup do vzdump concluída", - "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag constrói seus links a partir do endereço fornecido durante a instalação. Se não corresponder ao endereço do recipiente, edite lxc.environment. tempo de execução: SYMFONY ENV DOMAIN NAME em /etc/pve/lxc/.conf com o recipiente parado, e inicie-o novamente.", + "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "O wallabag gera as ligações com o endereço indicado na instalação. Se não corresponder ao do contentor, pare-o, edite lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME em /etc/pve/lxc/.conf e volte a iniciá-lo.", "wallabag listens on port 80 of the container and stores its data in SQLite.": "wallabag escuta na porta 80 do recipiente e armazena seus dados em SQLite.", "wallabag web interface": "Interface Web wallabag", "was": "era", "was installed, but the kernel reports:": "foi instalado, mas o kernel informa:", "when finished": "quando terminado", + "when the server has no subscription (you will be asked)": "quando o servidor não tem assinatura (será solicitado)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "irá religar a GPU ao vfio-pci na próxima reinicialização, quebrando o driver que está prestes a ser instalado.", "wipefs failed on": "Wipefs falhou em", "with": "com", diff --git a/lang/sk.json b/lang/sk.json index c29bb126..13346f9e 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (predvolená cesta Proxmox vzdump)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (predvolené v Proxmoxe)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = sticky bit + rwx pre všetkých. Zdieľaná skupina nie je potrebná.", + "====== PBS UPDATE COMPLETED ======": "====== AKTUALIZÁCIA PBS DOKONČENÁ ======", "====== PVE UPDATE COMPLETED ======": "====== AKTUALIZÁCIA PVE DOKONČENÁ ======", ">> Create a new backup job <<": ">> Vytvoriť novú úlohu zálohovania <<", ">> Save and return <<": ">> Uložiť a vrátiť <<", @@ -87,6 +88,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 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á", @@ -136,7 +138,7 @@ "ALL DATA ON": "VŠETKY DÁTA NA", "ALL DATA ON THIS DISK WILL BE PERMANENTLY LOST!": "VŠETKY DÁTA NA TOMTO DISKU BUDÚ NATRVALO STRATENÉ!", "ALL Utilities": "VŠETKY nástroje", - "ALLOWED_HOSTS cannot contain line breaks": "UDELENÉ HOSTS nemôžu obsahovať prestávky na čiare", + "ALLOWED_HOSTS cannot contain line breaks": "ALLOWED_HOSTS nesmie obsahovať zalomenia riadkov", "AList initial login": "AList začiatočné prihlásenie", "AMD (ROCm)": "AMD (ROCm)", "AMD (VA-API and ROCm detection)": "AMD (detekcia VA-API a ROCm)", @@ -169,6 +171,7 @@ "APT language configuration updated": "Nastavenie jazykov pre APT bolo aktualizované", "APT language downloads restored": "Sťahovanie jazykov cez APT bolo obnovené", "APT package lists updated": "Zoznamy APT balíkov aktualizované", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "Obnovenie APT zlyhalo. Skontrolujte predplatné a repozitáre PBS v časti Správa > Repozitáre. Denník:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "Obnovenie indexov APT zlyhalo. Skontrolujte predplatné a repozitáre PBS v pôvodnom rozhraní PBS; žiadne zdroje sa nezmenili.", "APT update failed. Check repository access; NVIDIA installation stopped.": "Aktualizácia APT zlyhala. Skontrolujte prístup k úložisku; inštalácia NVIDIA sa zastavila.", "Aborted": "Zrušené", @@ -319,7 +322,7 @@ "After confirming, you will be asked to choose the NVIDIA driver version to install.": "Po potvrdení si vyberiete verziu ovládača NVIDIA, ktorú chcete nainštalovať.", "After detailed analysis, no changes are needed.": "Po podrobnej analýze nie sú potrebné žiadne zmeny.", "After detailed analysis, no cleanup is needed.": "Po podrobnej analýze nie je potrebné žiadne čistenie.", - "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Po prihlásení spustite príkaz: ip a, aby ste zistili IP adresu.\nPotom túto IP adresu zadajte do prehliadača takto:\n http://IP_ADRESA\n\nOtvorí sa tým webové rozhranie Umbral OS.", + "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Po prihlásení zistite IP adresu príkazom ip a.\nPotom ju zadajte do prehliadača v tomto tvare:\n http://IP_ADDRESS\n\nOtvorí sa panel Umbral OS.", "After pasting on the server, ensure the file is chmod 600 and owned by": "Po vložení na serveri overte, že súbor má chmod 600 a vlastní ho", "After reboot the system will be fully accessible (SSH, web UI, login), but the following components will be reinstalled in BACKGROUND — until they finish, commands like nvidia-smi may not yet be available:": "Po reštarte bude systém plne dostupný (SSH, webové rozhranie, prihlásenie), ale tieto súčasti sa budú doinštalovávať NA POZADÍ - kým neskončia, príkazy ako nvidia-smi ešte nemusia byť dostupné:", "After reboot, these components will reinstall in background:": "Po reštarte sa tieto súčasti doinštalujú na pozadí:", @@ -443,10 +446,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "Použiť teraz skontrolovanú aktualizáciu balíkov? Služby sa môžu reštartovať a balíky označené na odstránenie budú odstránené.", "Apply these changes?": "Použiť tieto zmeny?", "Applying AMD-specific fixes...": "Používam opravy špecifické pre AMD...", + "Applying CPU and RAM changes without rebuilding the container...": "Používajú sa zmeny CPU a RAM bez opätovného vytvorenia kontajnera...", "Applying Changes": "Používam zmeny", "Applying Controller/NVMe passthrough to VM": "Používam priame priradenie Controller/NVMe do VM", "Applying balanced memory optimization settings...": "Používam vyvážené nastavenia optimalizácie pamäte...", "Applying changes safely...": "Bezpečne používam zmeny...", + "Applying container settings without rebuilding the container...": "Aplikujú sa nastavenia bez opätovného vytvorenia kontajnera...", "Applying custom restore": "Používam vlastnú obnovu", "Applying default VM configuration": "Používam predvolené nastavenie VM", "Applying host permissions for unprivileged LXC bind-mounts...": "Nastavujem oprávnenia hosta pre LXC bind mounty v neprivilegovaných kontajneroch...", @@ -538,6 +543,7 @@ "Available Borg targets:": "Dostupné Borg ciele:", "Available Disks on Host": "Dostupné disky na hostovi", "Available ISO Images": "Dostupné ISO obrazy", + "Available PBS Version": "Dostupná verzia PBS", "Available PBS repositories:": "Dostupné PBS repozitáre:", "Available PVE Version": "Dostupná verzia PVE", "Available Shares:": "Dostupné zdieľania:", @@ -749,6 +755,7 @@ "Cannot verify the reused disk:": "Nie je možné overiť znovu použitý disk:", "Cannot write the image archive to {storage}. Check storage access and mount status.": "Nie je možné zapísať archív obrázkov do {storage}. Skontrolujte prístup k úložisku a stav pripojenia.", "Capabilities cannot be kept and all dropped at the same time": "Nie je možné udržať si schopnosti a zároveň ich znížiť.", + "Catalog options": "Možnosti katalógu", "Category": "Kategória", "Caution: Maximum mode generates more heat.": "Pozor: maximálny režim vytvára viac tepla.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Kontrola Ceph preskočená používateľskou voľbou (--ignore-ceph-check)", @@ -883,6 +890,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Vyberte plán (syntax systémového kalendára a udalosti):", "Choose a script to execute:": "Vyberte skript, ktorý sa má spustiť:", "Choose a server:": "Vyberte server:", + "Choose a setting. Changes are applied only after the final review.": "Vyberte nastavenie. Zmeny sa použijú až po záverečnej kontrole.", "Choose a share to mount:": "Vyberte zdieľanie na pripojenie:", "Choose a specific supported GPU or USB node": "Vyberte konkrétny podporovaný uzol GPU alebo USB", "Choose action for this controller/NVMe:": "Vyberte akciu pre tento Controller/NVMe:", @@ -1520,6 +1528,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "Aktuálne NVIDIA inventár vyriešený: obnovovanie je required", "Current NVIDIA inventory resolved: no refresh is required": "Aktuálne NVIDIA inventár vyriešený: žiadne obnovenie je required", "Current Network Configuration": "Aktuálne nastavenie siete", + "Current PBS Version": "Aktuálna verzia PBS", "Current PVE Version": "Aktuálna verzia PVE", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Aktuálne host skripty ProxMenux registrujú vzdialené zdieľania ako úložiská Proxmoxu pomocou pvesm.", "Current ProxMenux version:": "Aktuálna verzia ProxMenux:", @@ -1685,6 +1694,7 @@ "Detected Coral hardware:": "Nájdený Coral hardvér:", "Detected GPU(s):": "Nájdené GPU:", "Detected Intel CPU": "Zistený Intel procesor", + "Detected Proxmox Backup Server": "Zistený Proxmox Backup Server", "Detected Proxmox VE": "Zistený Proxmox VE", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Zistený Proxmox VE 8.x - inštalujem Ceph Squid pre Debian", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Zistený Proxmox VE 9.x - inštalujem Ceph Squid pre Debian Trixie", @@ -1766,6 +1776,9 @@ "Disabling and stopping rpcbind service and socket...": "Zakázanie a zastavenie služby rpcbind a soketu...", "Disabling non-free firmware warnings...": "Vypínam upozornenia na non-free firmware...", "Disabling portmapper/rpcbind for security...": "Z bezpečnostných dôvodov vypínam portmapper/rpcbind...", + "Discard changes": "Zahodiť zmeny", + "Discard unapplied changes?": "Zahodiť nepoužité zmeny?", + "Discover NFS exports on a server": "Zistiť NFS exporty servera", "Discover targets on portal:": "Vyhľadať targety na portáli:", "Discovering iSCSI targets...": "Vyhľadávam iSCSI targety...", "Discovery successful": "Vyhľadanie bolo úspešné", @@ -1845,6 +1858,7 @@ "Do you want to proceed with the system update?": "Chcete pokračovať v aktualizácii systému?", "Do you want to proceed?": "Chcete pokračovať?", "Do you want to reboot now?": "Chcete reštartovať teraz?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "Chcete odstrániť banner predplatného Proxmox z webového rozhrania Proxmox Backup Server?", "Do you want to remove the stale entry from vfio.conf and continue?": "Chcete odstrániť zastaraný záznam z vfio.conf a pokračovať?", "Do you want to remove the stale entry from vfio.conf?": "Chcete odstrániť zastaraný záznam z vfio.conf?", "Do you want to restart the network service now to apply changes?": "Chcete teraz reštartovať sieťovú službu, aby sa zmeny prejavili?", @@ -2270,6 +2284,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "Repozitár log2ram sa nepodarilo stiahnuť. Skontrolujte /tmp/log2ram_install.log", "Failed to configure EFI disk": "EFI disk sa nepodarilo nastaviť", "Failed to configure IOMMU automatically.": "IOMMU sa nepodarilo nastaviť automaticky.", + "Failed to configure Proxmox Backup Server repositories": "Repozitáre Proxmox Backup Server sa nepodarilo nastaviť", "Failed to configure Proxmox repositories": "Repozitáre Proxmoxu sa nepodarilo nastaviť", "Failed to configure TPM device in VM": "TPM zariadenie sa vo VM nepodarilo nastaviť", "Failed to configure repositories.": "Repozitáre sa nepodarilo nastaviť.", @@ -2977,6 +2992,7 @@ "Inaccessible device during VM startup": "nedostupné zariadenie počas štartu VM", "Inactive": "Neaktívne", "Inactive (entry in fstab, not currently mounted)": "Neaktívne (záznam je vo fstab, ale teraz nie je pripojený)", + "Include these changes in oci-compose?": "Zahrnúť tieto zmeny do oci-compose?", "Include these resources in oci-compose?": "Zahrnúť tieto zdroje do oci-compose?", "Includes /etc/network (may drop SSH immediately)": "Obsahuje /etc/network (môže okamžite prerušiť SSH)", "Includes /etc/zfs": "Obsahuje /etc/zfs", @@ -3033,7 +3049,7 @@ "Initiator IQN is authorised on the target": "IQN iniciátora je na targete povolené", "Initiator IQN:": "IQN iniciátora:", "Inkscape is professional quality vector graphics software which runs on Linux, Mac OS X and Windows desktop computers.": "Inkscape je profesionálne kvalitný vektorový grafický softvér, ktorý beží na počítačoch Linux, Mac OS X a Windows.", - "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "Vnútri LXC, vytvoriť administrátor: konzola kimai:používateľ:vytvorte VAŠE VYUŽITE VAŠE EMAIL ROLE_SUPER_ADMIN", + "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "V LXC vytvorte správcu príkazom: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN", "Inspect disks before any action": "Skontrolovať disky pred akoukoľvek akciou", "Inspect host device nodes": "Skontrolovať device nodes na hostovi", "Inspect passthrough/kernel events": "Skontrolovať passthrough/kernel udalosti", @@ -3210,9 +3226,11 @@ "Interrupted operation:": "Prerušená operation:", "Interrupted stack operation found": "Prerušený zásobník operation nájdený", "Invalid 'proxmox-ve' candidate (not 9.x or none). Please verify your repository configuration and network, then retry.": "Neplatný kandidát 'proxmox-ve' (nie je 9.x alebo chýba). Skontrolujte nastavenie repozitárov a sieť, potom skúste znova.", - "Invalid ALLOWED_HOSTS value": "Neplatná hodnota UDELENÁ HOSTS", + "Invalid ALLOWED_HOSTS value": "Neplatná hodnota ALLOWED_HOSTS", "Invalid Home Assistant OS check timeout": "Neplatný čas odhlásenia Home Assistant OS", "Invalid ID": "Neplatné ID", + "Invalid IPv4 address": "Neplatná adresa IPv4", + "Invalid IPv4 gateway": "Neplatná brána IPv4", "Invalid Intel render path": "Neplatná cesta k zobrazeniu informácií", "Invalid Jellyfin path:": "Neplatná cesta Jellyfin:", "Invalid MAC address:": "Neplatná adresa MAC:", @@ -3245,6 +3263,7 @@ "Invalid administrator email": "Neplatný administrátorský mail", "Invalid administrator user name": "Neplatné meno správcu", "Invalid base VMID": "Neplatná základňa VMID", + "Invalid bridge": "Neplatný sieťový most", "Invalid check package:": "Neplatný kontrolný balík:", "Invalid configuration path:": "Neplatná konfiguračná dráha:", "Invalid consume/export volumes": "Neplatné objemy spotreby/vývozu", @@ -3308,6 +3327,7 @@ "Invalid remote name": "Neplatné vzdialené meno", "Invalid remote path": "Neplatná vzdialená cesta", "Invalid repair version:": "Neplatná verzia opravy:", + "Invalid resource value": "Neplatná hodnota prostriedkov", "Invalid resources": "Neplatné zdroje", "Invalid restored volume path": "Neplatná cesta obnoveného objemu", "Invalid running state": "Neplatný jazdný stav", @@ -3326,6 +3346,7 @@ "Invalid stack members": "Neplatní členovia stohu", "Invalid stack name": "Neplatný názov stohu", "Invalid stack operation": "Neplatný zásobník operation", + "Invalid storage": "Neplatné úložisko", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "Neplatné ID úložiska. Použite iba písmená, čísla, pomlčky a podčiarkovníky.", "Invalid storage name": "Neplatný názov úložiska", "Invalid suite application": "Neplatná aplikácia", @@ -3440,7 +3461,7 @@ "Keyfile removed.": "Keyfile odstránený.", "Keyrings method failed; trying apt-key fallback": "Metóda keyrings zlyhala; skúšam náhradný apt-key postup", "KiCad - A Cross Platform and Open Source Electronics Design Automation Suite.": "KiCad - Cross Platform a Open Source Electronics Design Automation Suite.", - "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai nemá predvolený účet. Zadajte kontajner s: pct zadajte {main vmid}", + "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai nemá predvolený účet. Do kontajnera vstúpite príkazom: pct enter {main_vmid}", "Kimai is a professional grade time-tracking application, free and open-source.": "Kimai je profesionálna časová aplikácia, zadarmo a open-source.", "Kometa is a powerful tool designed to give you complete control over your media libraries. With Kometa, you can take your customization to the next level, with granular control over metadata, collections, overlays, and much more.": "Kometa je výkonný nástroj navrhnutý tak, aby vám plnú kontrolu nad vaše mediálne knižnice. S Kometa, môžete vziať svoje prispôsobenie na ďalšiu úroveň, s granulovanou kontrolou nad metaúdaje, zbierky, prekrývanie, a oveľa viac.", "Kometa reads its configuration from /config/config.yml and the container only ships /config/config.yml.template. Copy the template to config.yml, fill in the required Plex and TMDb connections, then restart the container.": "Kometa číta svoju konfiguráciu z /config/config.yml a kontajner iba lode /config/config.yml.templát. Skopírujte šablónu pre config.yml, vyplňte required Plex a TMDb pripojenia, potom reštartujte kontajner.", @@ -4157,6 +4178,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 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.", @@ -4261,6 +4283,7 @@ "No password provided.": "Nebolo zadané heslo.", "No paths available": "Nie sú dostupné žiadne cesty", "No paths selected. Select at least one path.": "Nie sú vybrané žiadne cesty. Vyberte aspoň jednu cestu.", + "No pending changes": "Žiadne čakajúce zmeny", "No pending paths to schedule for reboot.": "Nie sú žiadne čakajúce cesty, ktoré by sa mali naplánovať na reštart.", "No permanent NFS mounts configured.": "Nie sú nastavené žiadne trvalé NFS pripojenia.", "No permanent Samba mounts configured.": "Nie sú nastavené žiadne trvalé Samba pripojenia.", @@ -4404,6 +4427,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM bol odstránený (Puppet súbory môže byť potrebné vyčistiť ručne)", "OVH server detected": "Zistený OVH server", "OVH server detection and RTM installation process completed": "Kontrola OVH servera a inštalácia RTM sú dokončené", + "Object detector": "Detektor objektov", "Object detector for Frigate": "Detektor objektov pre Frigate", "Observer is not responding on port 4357": "Pozorovateľ nereaguje na prístav 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian je note-prijímajúce aplikácie, ktoré vám umožní vytvoriť, odkaz, a usporiadať poznámky na vašom zariadení, so stovkami pluginov a tém prispôsobiť svoje pracovné postupy. Môžete tiež zverejniť svoje poznámky on-line, prístup je offline, a synchronizovať ich bezpečne s end-to-end šifrovanie.", @@ -4876,12 +4900,17 @@ "Proxmology logo applied": "Logo Proxmology bolo použité", "Proxmox 9 system update allready": "Systém Proxmox 9 je už aktualizovaný", "Proxmox APT repositories configured": "APT repozitáre Proxmoxu sú nastavené", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Ciele Proxmox Backup Servera (PBS)", + "Proxmox Backup Server APT repositories configured": "APT repozitáre Proxmox Backup Server sú nastavené", + "Proxmox Backup Server Update": "Aktualizácia Proxmox Backup Server", + "Proxmox Backup Server update completed.": "Aktualizácia Proxmox Backup Server bola dokončená.", "Proxmox CIFS Storage Status:": "Stav CIFS úložiska v Proxmoxe:", "Proxmox NFS Storage Status:": "Stav NFS úložiska v Proxmoxe:", "Proxmox System Update": "Aktualizácia systému Proxmox", "Proxmox Update": "Aktualizácia Proxmoxu", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "Aktualizácia balíkov Proxmox VE 8 zlyhala. Pred ďalším pokusom skontrolujte výstup APT.", "Proxmox VE 8 system update completed successfully": "Aktualizácia systému Proxmox VE 8 bola úspešne dokončená", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Ručný sprievodca aktualizáciou Proxmox VE 8 na 9", "Proxmox VE 9.x no-subscription repository created": "Proxmox VE 9.x no-subscription repozitár vytvorený", @@ -4889,6 +4918,7 @@ "Proxmox VE configuration completed.": "Nastavenie Proxmox VE je dokončené.", "Proxmox VE safe update completed": "Bezpečná aktualizácia Proxmox VE je dokončená", "Proxmox auth logger service created and started": "Služba na logovanie Proxmox prihlásení bola vytvorená a spustená", + "Proxmox could not apply the container settings": "Proxmox nedokázal použiť nastavenia kontajnera", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Proxmox enterprise repozitár môže chýbať alebo byť nedostupný. Skúšam prepnúť na no-subscription...", "Proxmox filter configured": "Proxmox filter nastavený", "Proxmox iSCSI Storage Status:": "Stav iSCSI úložiska v Proxmoxe:", @@ -5155,6 +5185,8 @@ "Remove saved credentials": "Odstráňte uložené poverenia", "Remove subscription banner": "Odstrániť hlásenie o predplatnom", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Odstrániť aplikáciu? Jeho kontajnerové disky sa vymažú a iba záloha ich môže vrátiť späť.", + "Remove the lock left on a VM by an interrupted task": "Odstrániť zámok, ktorý na VM zanechala prerušená úloha", + "Remove the lock left on a container by an interrupted task": "Odstrániť zámok, ktorý na kontajneri zanechala prerušená úloha", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Odstrániť uložené poverenia pre {registry}?\nSpustené aplikácie budú naďalej fungovať.", "Remove the unprivileged flag from configuration:": "Odstrániť príznak neprivilegovaného kontajnera z nastavenia:", "Remove unused packages and their config": "Odstrániť nepoužívané balíky aj ich nastavenia", @@ -5236,6 +5268,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 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:", @@ -5330,6 +5363,7 @@ "Reverting ZFS ARC tuning...": "Vraciam späť ladenie ZFS ARC...", "Reverting pigz wrapper...": "Vraciam späť wrapper pigz...", "Reverting vzdump speed tuning...": "Vraciam späť ladenie rýchlosti vzdump...", + "Review and apply": "Skontrolovať a použiť", "Review external OCI changes": "Skontrolujte externé zmeny OCI", "Review package upgrade": "Skontrolovať aktualizáciu balíkov", "Review passthrough config files": "Skontrolovať konfiguračné súbory passthrough", @@ -5789,6 +5823,7 @@ "ShareCoveX panel": "Panel ShareCoveX", "Shared Directory Ready:": "Zdieľaný priečinok je pripravený:", "Shared Group": "Zdieľaná skupina", + "Shared directories": "Zdieľané priečinky", "Shared directory created:": "Vytvorený zdieľaný adresár:", "Shared directory for consume and export": "Zdieľaný adresár pre spotrebu a export", "Shared directory for copy/sync operations": "Zdieľaný adresár pre kopírovanie/sync operations", @@ -5818,6 +5853,7 @@ "Should show pve-manager/9.x.x": "Malo by sa zobraziť pve-manager/9.x.x", "Shoutrrr addresses for the notifications (optional)": "Adresy Shoutrrr pre upozornenia (voliteľné)", "Show All Scripts": "Zobraziť všetky skripty", + "Show CT users and their UID/GID on the host": "Používatelia CT a ich UID/GID na hostiteľovi", "Show CT users in table format": "Zobraziť používateľov CT v tabuľke", "Show Container Privilege Status": "Zobraziť stav oprávnení kontajnerov", "Show DNS resolution status": "Zobraziť stav DNS prekladu", @@ -5979,6 +6015,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Spustite VM a začnite inštaláciu Windowsu z pripojeného ISO.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Spustiť aplikácie po ich zaregistrovaní? Odpoveď Nie, ak pôvodné kontajnery stále bežia na inom hostiteľovi: oba by používali rovnaké adresy.", "Start the converted container:": "Spustiť prevedený kontajner:", + "Start the interactive Proxmox package upgrade": "Spustiť interaktívnu aktualizáciu balíkov Proxmoxu", "Start the main system upgrade:": "Spustite hlavnú aktualizáciu systému:", "Start the stack with Proxmox": "Spustiť zásobník s Proxmox", "Start uploading to PBS — sets a recovery passphrase": "Spustiť nahrávanie do PBS - nastaví obnovovaciu frázu", @@ -6257,6 +6294,7 @@ "The Paperless persistent volumes need at least 8 GB": "Trvalé objemy bez dokumentov potrebujú najmenej 8 GB", "The PostgreSQL volume needs at least 4 GB": "Objem PostgreSQL potrebuje najmenej 4 GB", "The PostgreSQL volume needs at least 8 GB": "Objem PostgreSQL potrebuje najmenej 8 GB", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "Aktualizácia Proxmox VE 8 sa nedokončila. Prechod na Proxmox VE 9 sa nespustil.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "Chýba zväzok kľúčov archívu Proxmox;Inštalácia Ceph nemôže bezpečne pokračovať", "The Proxmox inventory and the local configurations differ": "Súpis Proxmox a miestne konfigurácie sa líšia", "The ROCm image has no support for the AMD GPU of this host:": "Obraz ROCm nemá podporu pre AMD GPU tohto hostiteľa:", @@ -6327,6 +6365,7 @@ "The build could not be checked beforehand; continuing without that check.": "Zostavenie nebolo možné vopred skontrolovať;pokračovanie bez tejto kontroly.", "The cached image does not match the current digest": "Comment", "The cached image is damaged; it will be downloaded again.": "Comment", + "The changes need the application installer": "Zmeny vyžadujú inštalačný program aplikácie", "The character device does not exist:": "Znakové zariadenie neexistuje:", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Príkaz vyžaduje heslo, ktoré nie je ozvene. Po vytvorení užívateľov, webové rozhranie žiada o užívateľské meno a heslo.", "The command does not name an image": "Príkaz nepomenuje obrázok", @@ -6446,8 +6485,10 @@ "The host bind source does not exist:": "Zdroj hostiteľskej väzby neexistuje:", "The host bind source is not a regular file or directory:": "Zdroj odkazu hostiteľa nie je bežný súbor alebo adresár:", "The host directory changed before it was mounted": "Adresár hostiteľa sa zmenil predtým, ako bol pripojený", + "The host directory does not exist": "Adresár na hostiteľovi neexistuje", "The host directory may not be accessible from an unprivileged container.": "Priečinok na hostovi nemusí byť dostupný z neprivilegovaného kontajnera.", "The host has no IPv4 address on the selected bridge": "Hostiteľ nemá IPv4 adresu na vybranom moste", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "Hostiteľ k nim stratí prístup. Ak skupina obsahuje sieťový alebo úložný radič, ktorý hostiteľ potrebuje, zrušte to v súhrne.", "The host monitor does not see the real host memory": "Monitor hostiteľa nevidí skutočnú pamäť hostiteľa", "The host monitor does not share this host namespace:": "Monitor hostiteľa nezdieľa tento menový priestor hostiteľa:", "The host monitor include is unknown or differs from the saved record": "Include monitora hostiteľa je neznámy alebo sa líši od uloženého záznamu", @@ -6637,6 +6678,7 @@ "The repair must preserve the image dependencies:": "Oprava musí zachovať závislosť obrazu:", "The requested VMID block is already in use": "Požadovaný blok VMID sa už používa", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "Požadovaný profil strojového učenia sa GPU nefunguje; nie je nahradený CPU", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "Zmenu prostriedkov sa nepodarilo úplne vrátiť späť. Znova otvorte správu aplikácií, skontrolujte aktuálne nastavenia systému Proxmox a prevezmite ich.", "The restored service did not pass its health check": "Obnovená služba neprešla kontrolou stavu", "The restored service stopped; the recovery is not confirmed": "Obnovená služba sa zastavila; obnova sa nepotvrdila", "The reviewed Tandoor stack does not require a privileged LXC.": "Preskúmaný Tandoor stack require privilegovaný LXC.", @@ -6682,6 +6724,7 @@ "The selected VM": "Vybraná VM", "The selected VM is running.": "Vybraná VM beží.", "The selected base folder does not exist and could not be created:": "Vybraný základný priečinok neexistuje a nepodarilo sa ho vytvoriť:", + "The selected bridge does not exist": "Vybraný sieťový most neexistuje", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "Vybraný bridge nemá IPv4 podsieť pre firewall monitora hostiteľa", "The selected configuration needs to start the LXC during the installation": "Vybraná konfigurácia musí spustiť LXC počas inštalácie", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Vybraný kontajner je neprivilegovaný. Na priame priradenie zariadenia je potrebný privilegovaný kontajner.", @@ -6704,6 +6747,7 @@ "The service builds its own image; only a published image can be installed": "Služba buduje svoj vlastný obraz; môže byť nainštalovaný iba zverejnený obrázok", "The service declares no image:": "Služba neprehlasuje žiadny obrázok:", "The setting has no final value:": "Nastavenie nemá žiadnu konečnú hodnotu:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Nepodarilo sa vrátiť všetky nastavenia. Pred pokračovaním skontrolujte kontajner a uloženú zmenu.", "The share already exists in smb.conf:": "Zdieľanie už existuje v smb.conf:", "The shared destination is not a directory": "Zdieľaná destinácia nie je adresár", "The shared directory points to a protected host path": "Zdieľaný adresár ukazuje na chránenú cestu hostiteľa", @@ -6771,19 +6815,24 @@ "These Proxmox resources were added outside ProxMenux:": "Tieto zdroje Proxmox boli pridané mimo ProxMenux:", "These VMIDs are not free:": "Tieto VMID nie sú zadarmo:", "These are the changes that will be made": "Tieto zmeny sa vykonajú", + "These changes were made in Proxmox outside ProxMenux:": "Tieto zmeny boli vykonané v Proxmox mimo ProxMenux:", "These container mount paths have an extension-like suffix:": "Tieto cesty pripojenia v kontajneri majú príponu podobnú koncovke súboru:", "These containers were restored from a backup and this host has no record of their OCI application:": "Tieto kontajnery boli obnovené zo zálohy a tento hostiteľ nemá žiadny záznam o ich aplikácii OCI:", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Tieto zariadenia zdieľajú skupinu IOMMU vybranej GPU a priradia sa k VM spolu s ňou:", "These host backups run with this job and expect its storage:": "Tieto zálohy hostiteľa sa spúšťajú s touto úlohou a očakávajú jej uloženie:", "These host backups run with this job and will stop running:": "Tieto zálohy hostiteľa sa spúšťajú s touto úlohou a prestanú sa spúšťať:", "These interface configurations will be removed": "Tieto nastavenia rozhraní sa odstránia", "These manual changes cannot be adopted safely:": "Tieto manuálne zmeny nie je možné bezpečne prijať:", "These paths will not be restored live and will be extracted for manual recovery.": "Tieto cesty sa neobnovia za behu a rozbalia sa na ručnú obnovu.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Tieto nastavenia vyžadujú úpravu špecifickú pre aplikáciu. Obraz sa nestiahol a kontajner sa nevytvoril znova.", "These values are asked during the installation:": "Tieto hodnoty sa požadujú počas inštalácie:", "They are described in the image documentation:": "Ich popis nájdete v dokumentácii image:", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "Pred pokračovaním budú pridané do oci-compose. Nepodporované alebo zmenené zdroje sa neimportujú.", + "They will be saved in oci-compose before continuing.": "Pred pokračovaním budú uložené v oci-compose.", "This CIFS share is mounted with restrictive permissions.": "Toto CIFS zdieľanie je pripojené s obmedzujúcimi oprávneniami.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Táto GPU sa v ProxMenux považuje za nekompatibilnú s GPU passthrough do VM.", "This NFS share is fully restricted — even the host root cannot write to it.": "Toto NFS zdieľanie je úplne obmedzené - ani root na hostovi doň nevie zapisovať.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Tento Proxmox Backup Server nemá aktívne predplatné. Prepnúť na repozitár bez predplatného? Nedostupný zdroj Enterprise PBS bude vypnutý.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Táto VM vyžaduje ďalšie kroky inštalácie. Návod nájdete tu:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Tento účet je pre {registry}. Zadajte obrázok z tohto registra.", "This action will:": "Táto akcia vykoná:", @@ -6867,6 +6916,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Tento skript sa nedá spustiť z webového terminálu Proxmoxu.", "This script must be run as root.": "Tento skript musí byť spustený ako root.", "This script must be run on a Proxmox host.": "Tento skript musí byť spustený na Proxmox serveri.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Tento skript použije nasledujúce optimalizácie a pokročilé úpravy na váš Proxmox Backup Server", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Tento skript použije tieto optimalizácie a pokročilé úpravy na vašom serveri Proxmox VE", "This script will update your Proxmox VE system with advanced options:": "Tento skript aktualizuje váš Proxmox VE systém s pokročilými možnosťami:", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Táto relácia beží v termináli Monitora. Ak by sa to spustilo odtiaľto, pripojenie sa počas inštalácie preruší a prepnutie môže zostať v zlom stave.", @@ -6998,6 +7048,7 @@ "Unable to identify the root filesystem. Aborting.": "Nepodarilo sa identifikovať koreňový súborový systém. Prerušuje sa.", "Unable to resolve system ZFS pool disks. Aborting.": "Nepodarilo sa vyriešiť systémové disky ZFS fondu. Prerušuje sa.", "Unable to resolve system disk topology. Aborting.": "Nepodarilo sa vyriešiť topológiu systémového disku. Prerušuje sa.", + "Unapplied changes": "Nepoužité zmeny", "Understand the security implications of privileged containers": "Uistite sa, že rozumiete bezpečnostným dôsledkom privilegovaných kontajnerov", "Unexpected Proxmox inventory; recovery blocked": "Neočakávaný inventár Proxmox; zablokovaná obnova", "Unexpected formatting directory in the new Valkey volume": "Neočakávaný formátovanie adresára v novom hlasitosti Valkey", @@ -7113,6 +7164,7 @@ "Unsupported mount type": "Nepodporovaný typ pripojenia", "Unsupported mount type:": "Nepodporovaný typ pripojenia:", "Unsupported native device type:": "Nepodporovaný natívne typ zariadenia:", + "Unsupported network settings": "Nepodporované sieťové nastavenia", "Unsupported operation": "Nepodporovaná operation", "Unsupported or unknown Proxmox version; no repository changed.": "Nepodporovaná alebo neznáma verzia Proxmox; žiadne úložisko sa nezmenilo.", "Unsupported output format:": "Nepodporovaný výstupný formát:", @@ -7193,6 +7245,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 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?", @@ -7630,6 +7683,7 @@ "active VF(s)": "aktívne VF", "active VFs": "aktívnymi VF", "active Virtual Functions. Changing its driver binding would destroy every VF.": "aktívnymi Virtual Functions. Zmena naviazania ovládača by zrušila každú VF.", + "added in Proxmox": "pridané v Proxmox", "after copy; source tree is incomplete.": "po kopírovaní; zdrojový strom je neúplný.", "after update.": "po aktualizácii.", "all guests": "všetci hostia", @@ -7997,6 +8051,7 @@ "remapped users": "remapovanými používateľmi", "remove the (now-empty) directory if possible": "odstrániť teraz prázdny priečinok, ak to bude možné", "removed from Proxmox": "odstránené z Proxmoxu", + "removed in Proxmox": "odstránené v Proxmox", "removed successfully from Proxmox.": "úspešne odstránené z Proxmoxu.", "requires running the official installer, which restarts the Monitor service.": "vyžaduje spustenie oficiálneho inštalátora, ktorý reštartuje službu Monitor.", "requires the package": "vyžaduje balík", @@ -8100,18 +8155,20 @@ "using": "cez", "using direct path (not guaranteed to survive reboots).": "používa sa priama cesta (nie je zaručené, že prežije reštarty).", "vCPUs:": "vCPU:", + "value changed in Proxmox": "hodnota zmenená v Proxmox", "vfio-pci IDs configured": "vfio-pci ID nastavené", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "vfio-pci ID v /etc/modprobe.d/vfio.conf", "video": "video", "video + recognition": "video + rozpoznávanie", "video only": "iba video", "vzdump backup speed optimization completed": "Optimalizácia rýchlosti záloh vzdump je dokončená", - "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag buduje svoje odkazy z adresy uvedenej počas inštalácie. Ak nezodpovedá adrese kontajnera, upravte prostredie lxc. čas: SYMFONY ENV DOMAIN NAME in /etc/pve/lxc/ .conf s kontajnerom zastavil, a začať znova.", + "wallabag builds its links from the address given during the installation. If it does not match the address of the container, edit lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME in /etc/pve/lxc/.conf with the container stopped, and start it again.": "wallabag vytvára odkazy z adresy zadanej pri inštalácii. Ak sa nezhoduje s adresou kontajnera, zastavte ho, upravte lxc.environment.runtime: SYMFONY__ENV__DOMAIN_NAME v /etc/pve/lxc/.conf a znova ho spustite.", "wallabag listens on port 80 of the container and stores its data in SQLite.": "wallabag počúva port 80 kontajnera a ukladá svoje údaje v SQLite.", "wallabag web interface": "Webové rozhranie wallabag", "was": "bolo", "was installed, but the kernel reports:": "bol nainštalovaný, ale jadro hlási:", "when finished": "po dokončení", + "when the server has no subscription (you will be asked)": "keď server nemá predplatné (budete požiadaný)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "pri ďalšom reštarte znova naviaže GPU na vfio-pci, čím sa preruší ovládač, ktorý sa má nainštalovať.", "wipefs failed on": "wipefs zlyhalo na", "with": "za", diff --git a/lang/sv.json b/lang/sv.json index 377b3f16..9cffe5e4 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -37,6 +37,7 @@ "/var/lib/vz/dump (Proxmox default vzdump path)": "/var/lib/vz/dump (Proxmox standard vzdump sökväg)", "/var/lib/vz/dump (Proxmox default)": "/var/lib/vz/dump (Proxmox standard)", "1777 = sticky bit + rwx for all. No shared group needed.": "1777 = sticky bit + rwx för alla. Ingen delad grupp behövs.", + "====== PBS UPDATE COMPLETED ======": "====== PBS UPPDATERING AVSLUTAD ======", "====== PVE UPDATE COMPLETED ======": "====== PVE UPPDATERING AVSLUTAD ======", ">> Create a new backup job <<": ">> Skapa ett nytt säkerhetskopieringsjobb <<", ">> Save and return <<": ">> Spara och returnera <<", @@ -87,6 +88,7 @@ "A multi-container application is not recreated: its containers are updated together.": "Ett program med flera behållare återskapas inte: dess behållare uppdateras tillsammans.", "A new ProxMenux version is available:": "En ny ProxMenux-version är tillgänglig:", "A new kernel is staged for the next boot:": "En ny kärna är iscensatt för nästa uppstart:", + "A newer kernel is installed. Reboot before the next backup window to use it.": "En nyare kärna är installerad. Starta om innan nästa säkerhetskopieringsfönster för att använda det.", "A newer version is available:": "En nyare version finns tillgänglig:", "A pending operation exists for": "En väntande operation finns för", "A pending stack assembly already exists; it is not overwritten": "En väntande stack församling existerar redan; den är inte överskriven", @@ -169,6 +171,7 @@ "APT language configuration updated": "APT-språkkonfigurationen uppdaterad", "APT language downloads restored": "APT-språknedladdningar återställda", "APT package lists updated": "APT-paketlistor uppdaterade", + "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:": "APT-uppdatering misslyckades. Kontrollera PBS-prenumerationen och arkiven i Administration > Repositories. Logga:", "APT refresh failed. Check the PBS subscription and repositories in the native PBS interface; no source was changed.": "APT-uppdateringen misslyckades. Kontrollera PBS-prenumerationen och paketförråden i PBS eget gränssnitt; inga paketkällor ändrades.", "APT update failed. Check repository access; NVIDIA installation stopped.": "APT-uppdateringen misslyckades. Kontrollera åtkomsten till arkiven; NVIDIA-installationen stoppades.", "Aborted": "Avbruten", @@ -319,7 +322,7 @@ "After confirming, you will be asked to choose the NVIDIA driver version to install.": "Efter bekräftelse kommer du att bli ombedd att välja NVIDIA-drivrutinsversionen att installera.", "After detailed analysis, no changes are needed.": "Efter detaljerad analys behövs inga ändringar.", "After detailed analysis, no cleanup is needed.": "Efter detaljerad analys behövs ingen rengöring.", - "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "När du har loggat in, kör: ip a för att få IP-adressen.\nAnge sedan den IP-adressen i din webbläsare så här:\n http://IP_ADRESS\n\nDetta öppnar Umbral OS-instrumentpanelen.", + "After logging in, run: ip a to obtain the IP address.\nThen, enter that IP address in your web browser like this:\n http://IP_ADDRESS\n\nThis will open the Umbral OS dashboard.": "Kör ip a efter inloggningen för att ta reda på IP-adressen.\nAnge sedan adressen i webbläsaren så här:\n http://IP_ADDRESS\n\nDå öppnas instrumentpanelen för Umbral OS.", "After pasting on the server, ensure the file is chmod 600 and owned by": "Efter att ha klistrat in på servern, se till att filen är chmod 600 och ägs av", "After reboot the system will be fully accessible (SSH, web UI, login), but the following components will be reinstalled in BACKGROUND — until they finish, commands like nvidia-smi may not yet be available:": "Efter omstart kommer systemet att vara fullt tillgängligt (SSH, webbgränssnitt, inloggning), men följande komponenter kommer att installeras om i BAKGRUND - tills de är klara kanske kommandon som nvidia-smi inte är tillgängliga ännu:", "After reboot, these components will reinstall in background:": "Efter omstart kommer dessa komponenter att installeras om i bakgrunden:", @@ -443,10 +446,12 @@ "Apply the reviewed package upgrade now? Services may restart and packages listed for removal will be removed.": "Tillämpa den granskade paketuppgraderingen nu? Tjänster kan startas om och paket som anges för borttagning kommer att tas bort.", "Apply these changes?": "Tillämpa dessa ändringar?", "Applying AMD-specific fixes...": "Tillämpar AMD-specifika korrigeringar...", + "Applying CPU and RAM changes without rebuilding the container...": "Tillämpar ändringarna av CPU och RAM utan att återskapa containern...", "Applying Changes": "Tillämpa ändringar", "Applying Controller/NVMe passthrough to VM": "Tillämpar styrenhets-/NVMe-passthrough på VM", "Applying balanced memory optimization settings...": "Tillämpar inställningar för balanserad minnesoptimering...", "Applying changes safely...": "Tillämpar ändringar på ett säkert sätt...", + "Applying container settings without rebuilding the container...": "Tillämpar inställningarna utan att återskapa containern...", "Applying custom restore": "Använder anpassad återställning", "Applying default VM configuration": "Tillämpar standard VM-konfiguration", "Applying host permissions for unprivileged LXC bind-mounts...": "Tillämpar värdbehörigheter för oprivilegierade LXC bind-mounts...", @@ -538,6 +543,7 @@ "Available Borg targets:": "Tillgängliga Borg-mål:", "Available Disks on Host": "Tillgängliga diskar på värd", "Available ISO Images": "Tillgängliga ISO-bilder", + "Available PBS Version": "Tillgänglig PBS-version", "Available PBS repositories:": "Tillgängliga PBS-lager:", "Available PVE Version": "Tillgänglig PVE-version", "Available Shares:": "Tillgängliga andelar:", @@ -749,6 +755,7 @@ "Cannot verify the reused disk:": "Kan inte verifiera den återanvända disken:", "Cannot write the image archive to {storage}. Check storage access and mount status.": "Kan inte skriva bildarkivet till {storage}. Kontrollera lagringsåtkomst och monteringsstatus.", "Capabilities cannot be kept and all dropped at the same time": "Förmågor kan inte hållas och alla tappas samtidigt", + "Catalog options": "Katalogalternativ", "Category": "Kategori", "Caution: Maximum mode generates more heat.": "Varning: Maximalt läge genererar mer värme.", "Ceph check skipped by user flag (--ignore-ceph-check)": "Ceph-kontroll hoppas över av användarflaggan (--ignore-ceph-check)", @@ -883,6 +890,7 @@ "Choose a schedule (systemd calendar-event syntax):": "Välj ett schema (syntax för systemd kalenderhändelser):", "Choose a script to execute:": "Välj ett skript att köra:", "Choose a server:": "Välj en server:", + "Choose a setting. Changes are applied only after the final review.": "Välj en inställning. Ändringarna tillämpas först efter den slutliga granskningen.", "Choose a share to mount:": "Välj en resurs att montera:", "Choose a specific supported GPU or USB node": "Välj en specifik GPU- eller USB-nod som stöds", "Choose action for this controller/NVMe:": "Välj åtgärd för denna kontroller/NVMe:", @@ -1520,6 +1528,7 @@ "Current NVIDIA inventory resolved: a refresh is required": "Nuvarande NVIDIA inventering löst: en uppfriskning är required", "Current NVIDIA inventory resolved: no refresh is required": "Nuvarande NVIDIA inventering löst: ingen uppfriskning är required", "Current Network Configuration": "Aktuell nätverkskonfiguration", + "Current PBS Version": "Aktuell PBS-version", "Current PVE Version": "Aktuell PVE-version", "Current ProxMenux host scripts register remote shares as Proxmox storages using pvesm.": "Aktuella ProxMenux-värdskript registrerar fjärrresurser som Proxmox-lagringar med hjälp av pvesm.", "Current ProxMenux version:": "Aktuell ProxMenux-version:", @@ -1634,8 +1643,8 @@ "Default Proxmox logo applied": "Standard Proxmox-logotyp tillämpas", "Default is none (video-only passthrough). Use SPACE to toggle selections.": "Standard är ingen (endast videogenomströmning). Använd MELLANSLAG för att växla mellan val.", "Default journald configuration restored": "Standard journalkonfiguration återställd", - "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "Standardplatsen är /mnt/. Delingen kommer att monteras här på värden med öppna behörigheter så att en oprivilegierad LXC kan bind-mounta och skriva till den. För LXC-åtkomst, bind-montera den här sökvägen med LXC Mount Manager.", - "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "Standardplatsen är /mnt/. Delen kommer att monteras här på värden. Använd denna sökväg i /etc/fstab. För LXC-åtkomst, bind-montera den här sökvägen med LXC Mount Manager.", + "Default location is /mnt/. The share will be mounted here on the host with open permissions so an unprivileged LXC can bind-mount and write to it. For LXC access, bind-mount this path with the LXC Mount Manager.": "Standardplatsen är /mnt/. Den delade resursen monteras här på värden med öppna behörigheter, så att en oprivilegierad LXC-container kan bind-montera den och skriva till den. Använd LXC Mount Manager för att skapa monteringen i LXC-containern.", + "Default location is /mnt/. The share will be mounted here on the host. Use this path in /etc/fstab. For LXC access, bind-mount this path with the LXC Mount Manager.": "Standardplatsen är /mnt/. Den delade resursen monteras här på värden. Använd sökvägen i /etc/fstab. För åtkomst från en LXC-container, skapa en bind-montering av sökvägen med LXC Mount Manager.", "Default login": "Standard inloggning", "Default options": "Standardalternativ", "Default options read/write": "Standardalternativ läs/skriv", @@ -1685,6 +1694,7 @@ "Detected Coral hardware:": "Upptäckt Coral-hårdvara:", "Detected GPU(s):": "Upptäckta GPU:er:", "Detected Intel CPU": "Upptäckte Intel CPU", + "Detected Proxmox Backup Server": "Upptäckte Proxmox Backup Server", "Detected Proxmox VE": "Detekterade Proxmox VE", "Detected Proxmox VE 8.x - Installing Ceph Squid for Debian": "Upptäckte Proxmox VE 8.x - Installerar Ceph Squid för Debian", "Detected Proxmox VE 9.x - Installing Ceph Squid for Debian Trixie": "Upptäckte Proxmox VE 9.x - Installerar Ceph Squid för Debian Trixie", @@ -1766,6 +1776,9 @@ "Disabling and stopping rpcbind service and socket...": "Inaktivera och stoppa rpcbind-tjänsten och -socket...", "Disabling non-free firmware warnings...": "Inaktiverar icke-fri firmware-varningar...", "Disabling portmapper/rpcbind for security...": "Inaktiverar portmapper/rpcbind för säkerhet...", + "Discard changes": "Kasta ändringar", + "Discard unapplied changes?": "Kasta ändringar som inte har tillämpats?", + "Discover NFS exports on a server": "Hitta NFS-exporter på en server", "Discover targets on portal:": "Upptäck mål på portalen:", "Discovering iSCSI targets...": "Upptäcker iSCSI-mål...", "Discovery successful": "Upptäckten lyckades", @@ -1845,6 +1858,7 @@ "Do you want to proceed with the system update?": "Vill du fortsätta med systemuppdateringen?", "Do you want to proceed?": "Vill du fortsätta?", "Do you want to reboot now?": "Vill du starta om nu?", + "Do you want to remove the Proxmox subscription banner from the Proxmox Backup Server web interface?": "Vill du ta bort Proxmox-prenumerationsbannern från Proxmox Backup Server-webbgränssnittet?", "Do you want to remove the stale entry from vfio.conf and continue?": "Vill du ta bort den inaktuella posten från vfio.conf och fortsätta?", "Do you want to remove the stale entry from vfio.conf?": "Vill du ta bort den inaktuella posten från vfio.conf?", "Do you want to restart the network service now to apply changes?": "Vill du starta om nätverkstjänsten nu för att tillämpa ändringar?", @@ -2022,7 +2036,7 @@ "Enter VM ID:": "Ange VM ID:", "Enter VM/CT ID:": "Ange VM/CT ID:", "Enter ZFS pool name for the selected disk:": "Ange ZFS-poolnamn för den valda disken:", - "Enter a name for the mount point (used as /mnt/):": "Ange ett namn för monteringspunkten (används som /mnt/):", + "Enter a name for the mount point (used as /mnt/):": "Ange ett namn för monteringspunkten (används som /mnt/):", "Enter a name for the new virtual machine:": "Ange ett namn för den nya virtuella maskinen:", "Enter a non-empty password or access token on a single line.": "Ange ett icke-tomt lösenord eller åtkomsttoken på en enda rad.", "Enter a number, or write or paste a command:": "Ange ett nummer, eller skriv eller klistra in ett kommando:", @@ -2170,7 +2184,7 @@ "Example: pct stop 110": "Exempel: pct stop 110", "Example: pct stop 114": "Exempel: pct stop 114", "Example: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,backup": "Exempel: pvesm add nfs nfs-nas --server 192.168.1.50 --export /volume1/proxmox --content import,säkerhetskopia", - "Example: pvesm path local-lvm:vm-114-disk-0": "Exempel: pvesm sökväg local-lvm:vm-114-disk-0", + "Example: pvesm path local-lvm:vm-114-disk-0": "Exempel: pvesm path local-lvm:vm-114-disk-0", "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf": "Exempel: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf", "Examples:": "Exempel:", "Execute destructive rollback?": "Utföra destruktiv återställning?", @@ -2270,6 +2284,7 @@ "Failed to clone log2ram repository. Check /tmp/log2ram_install.log": "Det gick inte att klona log2ram-förrådet. Kontrollera /tmp/log2ram_install.log", "Failed to configure EFI disk": "Det gick inte att konfigurera EFI-disken", "Failed to configure IOMMU automatically.": "Det gick inte att konfigurera IOMMU automatiskt.", + "Failed to configure Proxmox Backup Server repositories": "Det gick inte att konfigurera Proxmox Backup Server-förråd", "Failed to configure Proxmox repositories": "Det gick inte att konfigurera Proxmox-förråd", "Failed to configure TPM device in VM": "Det gick inte att konfigurera TPM-enhet i virtuell dator", "Failed to configure repositories.": "Det gick inte att konfigurera förråd.", @@ -2481,7 +2496,7 @@ "For a consistent export, the VM should be stopped.": "För en konsekvent export bör den virtuella datorn stoppas.", "For any question, issue or bug report, please use the official repository": "För alla frågor, problem eller felrapporter, använd det officiella arkivet", "For dedicated GPUs, FLR is required by this policy to reduce VM": "För dedikerade GPU:er krävs FLR enligt denna policy för att minska virtuella datorer", - "For example: pct enter 101": "Till exempel: pct ange 101", + "For example: pct enter 101": "Till exempel: pct enter 101", "For guest shares add: --options guest": "För gästandelar lägg till: --options gäst", "For security reasons, the system cannot format it.": "Av säkerhetsskäl kan systemet inte formatera det.", "For unprivileged containers, use instead:": "För oprivilegierade behållare, använd istället:", @@ -2759,7 +2774,7 @@ "Host backup attached to PVE job": "Värdsäkerhetskopia kopplad till PVE-jobb", "Host data is not restored by the backup; confirm it with --acknowledge-external-data": "Värddata återställs inte av säkerhetskopieringen; bekräfta den med --acknowledge-external-data", "Host device for /dev/kvm": "Värd enhet för /dev/kvm", - "Host device for /dev/net/tun": "Värd enhet för /dev/nett/tun", + "Host device for /dev/net/tun": "Enhet på värden för /dev/net/tun", "Host device for /dev/ttyUSB0": "Värd enhet för /dev/ttyUSB0", "Host device for /dev/video10": "Värd enhet för /dev/video10", "Host device for /dev/video11": "Värd enhet för /dev/video11", @@ -2893,8 +2908,8 @@ "If notifications are enabled (Telegram/Discord/ntfy/...), you will receive a \"Host restore finished\" message when all background tasks complete.": "Om aviseringar är aktiverade (Telegram/Discord/ntfy/...), kommer du att få meddelandet \"Värdåterställning avslutad\" när alla bakgrundsuppgifter är klara.", "If passthrough fails on Windows: install RadeonResetBugFix.": "Om passthrough misslyckas på Windows: installera RadeonResetBugFix.", "If path not accessible (ZFS/BTRFS):": "Om sökväg inte är tillgänglig (ZFS/BTRFS):", - "If pvesm path returned a DEVICE (LVM):": "Om pvesm-sökvägen returnerade en DEVICE (LVM):", - "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Om pvesm-sökvägen returnerade en DIRECTORY (ZFS/BTRFS):", + "If pvesm path returned a DEVICE (LVM):": "Om pvesm path returnerade en ENHET (LVM):", + "If pvesm path returned a DIRECTORY (ZFS/BTRFS):": "Om pvesm path returnerade en KATALOG (ZFS/BTRFS):", "If repositories error:": "Om förrådsfel:", "If the container had already been changed, its previous native backup is restored. Shared host directories are not reverted. The disks of the failed attempt are removed when the cleanup succeeds.": "Om behållaren redan hade ändrats, återställs dess tidigare inbyggda säkerhetskopia. Delade värdkataloger återställs inte. Diskarna för det misslyckade försöket tas bort när rensningen lyckas.", "If the host freezes, remove hostpci entries from": "Om värden fryser, ta bort hostpci-poster från", @@ -2977,6 +2992,7 @@ "Inaccessible device during VM startup": "Otillgänglig enhet under VM-start", "Inactive": "Inaktiv", "Inactive (entry in fstab, not currently mounted)": "Inaktiv (post i fstab, inte för närvarande monterad)", + "Include these changes in oci-compose?": "Inkludera dessa ändringar i oci-compose?", "Include these resources in oci-compose?": "Inkludera dessa resurser i oci-compose?", "Includes /etc/network (may drop SSH immediately)": "Inkluderar /etc/network (kan släppa SSH omedelbart)", "Includes /etc/zfs": "Inkluderar /etc/zfs", @@ -3033,7 +3049,7 @@ "Initiator IQN is authorised on the target": "Initiator IQN är auktoriserad på målet", "Initiator IQN:": "Initiativtagare IQN:", "Inkscape is professional quality vector graphics software which runs on Linux, Mac OS X and Windows desktop computers.": "Inkscape är professionell kvalitet vektor grafik programvara som körs på Linux, Mac OS X och Windows stationära datorer.", - "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "Inuti LXC, skapa administratören: konsol kimai:user: skapa ditt USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN", + "Inside the LXC, create the administrator: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN": "Skapa administratören inne i LXC-containern: console kimai:user:create YOUR_USERNAME YOUR_EMAIL ROLE_SUPER_ADMIN", "Inspect disks before any action": "Inspektera diskar innan någon åtgärd", "Inspect host device nodes": "Inspektera värdenhetsnoder", "Inspect passthrough/kernel events": "Inspektera passthrough/kärnhändelser", @@ -3213,6 +3229,8 @@ "Invalid ALLOWED_HOSTS value": "Invalid ALLOWED_HOSTS värde", "Invalid Home Assistant OS check timeout": "Ogiltig Home Assistant OS check timeout", "Invalid ID": "Ogiltigt ID", + "Invalid IPv4 address": "Ogiltig IPv4-adress", + "Invalid IPv4 gateway": "Ogiltig IPv4-gateway", "Invalid Intel render path": "Invalid Intel render bana", "Invalid Jellyfin path:": "Invalid Jellyfin väg:", "Invalid MAC address:": "Ogiltig MAC-adress:", @@ -3245,6 +3263,7 @@ "Invalid administrator email": "Invalid administratör e-post", "Invalid administrator user name": "Invalid administratör användarnamn", "Invalid base VMID": "Ogiltig bas VMID", + "Invalid bridge": "Ogiltig brygga", "Invalid check package:": "Ogiltigt checkpaket:", "Invalid configuration path:": "Ogiltig konfigurationsväg:", "Invalid consume/export volumes": "Ogiltiga konsum/exportvolymer", @@ -3308,6 +3327,7 @@ "Invalid remote name": "Ogiltigt fjärrnamn", "Invalid remote path": "Invalid fjärrväg", "Invalid repair version:": "Ogiltig reparationsversion:", + "Invalid resource value": "Ogiltigt resursvärde", "Invalid resources": "Ogiltiga resurser", "Invalid restored volume path": "Invalid återställd volymväg", "Invalid running state": "Ogiltig körstatus", @@ -3326,6 +3346,7 @@ "Invalid stack members": "Ogiltiga stackmedlemmar", "Invalid stack name": "Invalid stack namn", "Invalid stack operation": "Ogiltig stack operation", + "Invalid storage": "Ogiltig lagring", "Invalid storage ID. Use only letters, numbers, hyphens and underscores.": "Ogiltigt lagrings-ID. Använd endast bokstäver, siffror, bindestreck och understreck.", "Invalid storage name": "Ogiltigt lagringsnamn", "Invalid suite application": "Invalid suite applikation", @@ -3440,7 +3461,7 @@ "Keyfile removed.": "Nyckelfilen har tagits bort.", "Keyrings method failed; trying apt-key fallback": "Nyckelringsmetoden misslyckades; försöker apt-key fallback", "KiCad - A Cross Platform and Open Source Electronics Design Automation Suite.": "KiCad - En Cross Platform och Open Source Electronics Design Automation Suite.", - "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai har inget standardkonto. Ange behållaren med: pct enter {main vmid}", + "Kimai has no default account. Enter the container with: pct enter {main_vmid}": "Kimai har inget standardkonto. Öppna ett skal i containern med: pct enter {main_vmid}", "Kimai is a professional grade time-tracking application, free and open-source.": "Kimai är en professionell tidsspårningsapplikation, fri och öppen källkod.", "Kometa is a powerful tool designed to give you complete control over your media libraries. With Kometa, you can take your customization to the next level, with granular control over metadata, collections, overlays, and much more.": "Kometa är ett kraftfullt verktyg för att ge dig fullständig kontroll över dina mediebibliotek. Med Kometa kan du ta din anpassning till nästa nivå, med granulär kontroll över metadata, samlingar, överlagringar och mycket mer.", "Kometa reads its configuration from /config/config.yml and the container only ships /config/config.yml.template. Copy the template to config.yml, fill in the required Plex and TMDb connections, then restart the container.": "Kometa läser sin konfiguration från /config/config.yml och behållaren endast fartyg /config/config.yml.template. Kopiera mallen till config.yml, fyll i required Plex och TMDb-anslutningar och starta sedan om behållaren.", @@ -3511,7 +3532,7 @@ "Limiting size and optimizing journald": "Begränsning av storlek och optimering av journal", "Limiting size and optimizing journald...": "Begränsar storlek och optimerar journal...", "Limnoria A robust, full-featured, and user/programmer-friendly Python IRC bot, with many existing plugins. Successor of the well-known Supybot.": "Limnoria En robust, fullfjädrad och användarvänlig Python IRC bot, med många befintliga plugins. Framgång för den välkända Supybot.", - "Limnoria joins no IRC network until its configuration file exists. Create it with the setup wizard from the Proxmox host: pct exec -- bash -c 'cd /config && limnoria-wizard'": "Limnoria går med i inget IRC-nätverk tills dess konfigurationsfil finns. Skapa den med installationsguiden från Proxmox-värden: pct exec - bash -c \"cd /config & & & & limnoria-wizard\"", + "Limnoria joins no IRC network until its configuration file exists. Create it with the setup wizard from the Proxmox host: pct exec -- bash -c 'cd /config && limnoria-wizard'": "Limnoria ansluter inte till något IRC-nätverk förrän konfigurationsfilen finns. Skapa den med installationsguiden från Proxmox-värden: pct exec -- bash -c 'cd /config && limnoria-wizard'", "Line to paste (single line, including \"command=...\" prefix):": "Rad att klistra in (en rad, inklusive \"command=...\" prefix):", "Linux Installation Options": "Linux installationsalternativ", "Linux/Mac path:": "Linux/Mac sökväg:", @@ -4157,6 +4178,7 @@ "No active Proxmox storage accepts OCI template archives.": "Ingen aktiv Proxmox-lagring accepterar OCI-mallarkiv.", "No active exports or showmount not available": "Inga aktiva exporter eller showmount är inte tillgängligt", "No active session": "Ingen aktiv session", + "No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.": "Inget aktivt abonnemang och inget användbart PBS-förråd. Icke-interaktivt läge kan inte ändra APT-källor;konfigurera dem i Administration > Repositories.", "No active subscription and no usable PVE repository. Noninteractive mode cannot change APT sources; configure them in Node > Updates > Repositories.": "Ingen aktiv prenumeration och inget användbart PVE-arkiv. Icke-interaktivt läge kan inte ändra APT-källor; konfigurera dem i Node > Updates > Repositories.", "No additional GPU can be added.": "Ingen ytterligare GPU kan läggas till.", "No additional device needs to be added.": "Ingen ytterligare enhet behöver läggas till.", @@ -4261,6 +4283,7 @@ "No password provided.": "Inget lösenord angett.", "No paths available": "Inga vägar tillgängliga", "No paths selected. Select at least one path.": "Inga sökvägar har valts. Välj minst en väg.", + "No pending changes": "Inga väntande ändringar", "No pending paths to schedule for reboot.": "Inga väntande sökvägar att schemalägga för omstart.", "No permanent NFS mounts configured.": "Inga permanenta NFS-monteringar har konfigurerats.", "No permanent Samba mounts configured.": "Inga permanenta Samba-fästen har konfigurerats.", @@ -4402,6 +4425,7 @@ "OVH RTM removed (Puppet artefacts may need manual cleanup)": "OVH RTM har tagits bort (Puppet artefakter kan behöva manuell rengöring)", "OVH server detected": "OVH-server upptäckt", "OVH server detection and RTM installation process completed": "OVH-serverdetektering och RTM-installation har slutförts", + "Object detector": "Objektdetektor", "Object detector for Frigate": "Objektdetektor för Frigate", "Observer is not responding on port 4357": "Observer svarar inte på port 4357", "Obsidian is a note-taking app that lets you create, link, and organize your notes on your device, with hundreds of plugins and themes to customize your workflow. You can also publish your notes online, access them offline, and sync them securely with end-to-end encryption.": "Obsidian är en noterande app som låter dig skapa, länka och organisera dina anteckningar på din enhet, med hundratals plugins och teman för att anpassa ditt arbetsflöde. Du kan också publicera dina anteckningar online, komma åt dem offline och synkronisera dem säkert med end-to-end-kryptering.", @@ -4577,7 +4601,7 @@ "Partition created:": "Partition skapad:", "Partition table wiped": "Skiljebord torkat", "Pass /dev/kvm to the LXC": "Pass /dev/kvm till LXC", - "Pass /dev/net/tun to the LXC": "Pass /dev/netto/tun till LXC", + "Pass /dev/net/tun to the LXC": "Ge LXC-containern åtkomst till /dev/net/tun", "Pass /dev/ttyUSB0 to the LXC": "Pass /dev/ttyUSB0 till LXC", "Pass /dev/video10 to the LXC": "Pass /dev/video10 till LXC", "Pass /dev/video11 to the LXC": "Pass /dev/video11 till LXC", @@ -4874,12 +4898,17 @@ "Proxmology logo applied": "Proxmology-logotyp applicerad", "Proxmox 9 system update allready": "Proxmox 9 systemuppdatering redan", "Proxmox APT repositories configured": "Proxmox APT-förråd konfigurerade", + "Proxmox Backup Server": "Proxmox Backup Server", "Proxmox Backup Server (PBS) destinations": "Destinationer för Proxmox Backup Server (PBS)", + "Proxmox Backup Server APT repositories configured": "Proxmox Backup Server APT-förråd konfigurerade", + "Proxmox Backup Server Update": "Proxmox Backup Server Uppdatering", + "Proxmox Backup Server update completed.": "Proxmox Backup Server-uppdatering slutförd.", "Proxmox CIFS Storage Status:": "Proxmox CIFS-lagringsstatus:", "Proxmox NFS Storage Status:": "Proxmox NFS-lagringsstatus:", "Proxmox System Update": "Proxmox systemuppdatering", "Proxmox Update": "Proxmox-uppdatering", "Proxmox VE": "Proxmox VE", + "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.": "Paketuppgraderingen av Proxmox VE 8 misslyckades. Granska APT-utdata innan du försöker igen.", "Proxmox VE 8 system update completed successfully": "Systemuppdateringen av Proxmox VE 8 slutfördes framgångsrikt", "Proxmox VE 8 to 9 Manual Upgrade Guide": "Proxmox VE 8 till 9 Manuell uppgraderingsguide", "Proxmox VE 9.x no-subscription repository created": "Proxmox VE 9.x arkiv utan prenumeration skapat", @@ -4887,6 +4916,7 @@ "Proxmox VE configuration completed.": "Proxmox VE-konfigurationen slutförd.", "Proxmox VE safe update completed": "Proxmox VE säker uppdatering slutförd", "Proxmox auth logger service created and started": "Proxmox authlogger-tjänst skapad och startade", + "Proxmox could not apply the container settings": "Proxmox kunde inte tillämpa containerns inställningar", "Proxmox enterprise repo might be missing or inaccessible. Trying to switch to no-subscription...": "Proxmox företagsrepo kan saknas eller är otillgänglig. Försöker byta till ingen prenumeration...", "Proxmox filter configured": "Proxmox-filter konfigurerat", "Proxmox iSCSI Storage Status:": "Proxmox iSCSI-lagringsstatus:", @@ -5153,6 +5183,8 @@ "Remove saved credentials": "Ta bort sparade autentiseringsuppgifter", "Remove subscription banner": "Ta bort prenumerationsbanner", "Remove the application? Its container disks are deleted, and only a backup can bring them back.": "Vill du ta bort programmet? Dess containerdiskar raderas och endast en säkerhetskopia kan ta tillbaka dem.", + "Remove the lock left on a VM by an interrupted task": "Ta bort låset som en avbruten uppgift lämnat på en VM", + "Remove the lock left on a container by an interrupted task": "Ta bort låset som en avbruten uppgift lämnat på en container", "Remove the saved credentials for {registry}?\nRunning apps will keep working.": "Ta bort de sparade referenserna för {registry}?\nAppar som körs fortsätter att fungera.", "Remove the unprivileged flag from configuration:": "Ta bort den oprivilegierade flaggan från konfigurationen:", "Remove unused packages and their config": "Ta bort oanvända paket och deras konfiguration", @@ -5222,7 +5254,7 @@ "Replace": "Ersätta", "Replace with the actual ID.": "Ersätt med det faktiska ID:t.", "Replace with your actual container ID": "Ersätt med ditt faktiska container-ID", - "Replace with the value from step 4": "Ersätt med värdet från steg 4", + "Replace with the value from step 4": "Ersätt med värdet från steg 4", "Replace invalid port(s)": "Ersätt ogiltiga port(ar)", "Replace it?": "Byt ut den?", "Replace paths with your actual values from step 5": "Ersätt sökvägar med dina faktiska värden från steg 5", @@ -5234,6 +5266,7 @@ "Repository helper unavailable. NVIDIA installation stopped.": "Arkivhjälparen är inte tillgänglig. NVIDIA-installationen stoppades.", "Repository policy returned an unexpected result.": "Arkivpolicyn gav ett oväntat resultat.", "Repository ready.": "Förvaret klart.", + "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.": "Lagringskällorna har ändrats, men uppdateringen av APT-paketlistan misslyckades. Drift stoppad;källor rullades inte tillbaka. Inspektera Administration > Repositories och försök apt-get update igen innan du fortsätter.", "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.": "Arkivkällorna har ändrats, men uppdateringen av APT-paketlistan misslyckades. Åtgärden stoppades; källorna återställdes inte. Kontrollera Node > Updates > Repositories och försök apt-get update igen innan du fortsätter.", "Repository switch declined; no APT source changed.": "Arkivbytet avvisades; ingen APT-källa har ändrats.", "Repository:": "Förvar:", @@ -5328,6 +5361,7 @@ "Reverting ZFS ARC tuning...": "Återställer ZFS ARC-inställning...", "Reverting pigz wrapper...": "Återställer pigz-omslag...", "Reverting vzdump speed tuning...": "Återställer vzdump-hastighetsinställning...", + "Review and apply": "Granska och tillämpa", "Review external OCI changes": "Granska externa OCI ändringar", "Review package upgrade": "Granska paketuppgraderingen", "Review passthrough config files": "Granska passthrough-konfigurationsfiler", @@ -5787,6 +5821,7 @@ "ShareCoveX panel": "ShareCoveX panel", "Shared Directory Ready:": "Redo för delad katalog:", "Shared Group": "Delad grupp", + "Shared directories": "Delade kataloger", "Shared directory created:": "Delad katalog skapad:", "Shared directory for consume and export": "Delad katalog för konsumering och export", "Shared directory for copy/sync operations": "Delad katalog för kopia/synkronisera operations", @@ -5816,6 +5851,7 @@ "Should show pve-manager/9.x.x": "Bör visa pve-manager/9.x.x", "Shoutrrr addresses for the notifications (optional)": "Shoutrrr-adresser för aviseringarna (valfritt)", "Show All Scripts": "Visa alla skript", + "Show CT users and their UID/GID on the host": "CT-användare och deras UID/GID på värden", "Show CT users in table format": "Visa CT-användare i tabellformat", "Show Container Privilege Status": "Visa status för containerprivilegier", "Show DNS resolution status": "Visa DNS-upplösningsstatus", @@ -5977,6 +6013,7 @@ "Start the VM to begin Windows installation from the mounted ISO.": "Starta den virtuella datorn för att påbörja Windows-installationen från den monterade ISO.", "Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.": "Starta applikationerna när de är registrerade? Svara Nej om de ursprungliga behållarna fortfarande körs på en annan värd: båda skulle använda samma adresser.", "Start the converted container:": "Starta den konverterade behållaren:", + "Start the interactive Proxmox package upgrade": "Starta den interaktiva paketuppgraderingen av Proxmox", "Start the main system upgrade:": "Starta huvudsystemuppgraderingen:", "Start the stack with Proxmox": "Börja stapeln med Proxmox", "Start uploading to PBS — sets a recovery passphrase": "Börja ladda upp till PBS — ställer in en återställningslösenfras", @@ -6256,6 +6293,7 @@ "The Paperless persistent volumes need at least 8 GB": "De papperslösa ihållande volymerna behöver minst 8 GB", "The PostgreSQL volume needs at least 4 GB": "PostgreSQL-volymen behöver minst 4 GB", "The PostgreSQL volume needs at least 8 GB": "PostgreSQL-volymen behöver minst 8 GB", + "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.": "Uppdateringen av Proxmox VE 8 slutfördes inte. Uppgraderingen till Proxmox VE 9 startades inte.", "The Proxmox archive keyring is missing; Ceph installation cannot continue safely": "Proxmox arkivnyckelringen saknas;Ceph-installationen kan inte fortsätta på ett säkert sätt", "The Proxmox inventory and the local configurations differ": "Proxmox-inventeringen och de lokala konfigurationerna skiljer sig åt", "The ROCm image has no support for the AMD GPU of this host:": "ROCm-avbilden har inget stöd för AMD GPU på denna värd:", @@ -6326,6 +6364,7 @@ "The build could not be checked beforehand; continuing without that check.": "Bygget kunde inte kontrolleras i förväg;fortsätter utan den kontrollen.", "The cached image does not match the current digest": "Den cachade bilden matchar inte den nuvarande smältningen", "The cached image is damaged; it will be downloaded again.": "Den cachade bilden är skadad; den kommer att laddas ner igen.", + "The changes need the application installer": "Ändringarna kräver programmets installationsverktyg", "The character device does not exist:": "Karaktärenheten existerar inte:", "The command asks for a password that is not echoed. After creating the users, the web interface asks for a user name and a password.": "Kommandot begär ett lösenord som inte echoed. Efter att ha skapat användarna ber webbgränssnittet om ett användarnamn och ett lösenord.", "The command does not name an image": "Kommandot heter inte en bild", @@ -6445,8 +6484,10 @@ "The host bind source does not exist:": "Värdbindningskällan finns inte:", "The host bind source is not a regular file or directory:": "Värdbindningskällan är inte en vanlig fil eller katalog:", "The host directory changed before it was mounted": "Värdkatalogen ändrades innan den monterades", + "The host directory does not exist": "Katalogen på värden finns inte", "The host directory may not be accessible from an unprivileged container.": "Värdkatalogen kanske inte är tillgänglig från en oprivilegierad behållare.", "The host has no IPv4 address on the selected bridge": "Värden har ingen IPv4-adress på den valda bron", + "The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.": "Värden förlorar åtkomsten till dem. Om gruppen innehåller en nätverks- eller lagringskontroller som värden behöver, avbryt i sammanfattningen.", "The host monitor does not see the real host memory": "Värdskärmen ser inte det verkliga värdminnet", "The host monitor does not share this host namespace:": "Värdskärmen delar inte detta värdnamnsutrymme:", "The host monitor include is unknown or differs from the saved record": "Värdövervakarens include är okänd eller skiljer sig från den sparade posten", @@ -6636,6 +6677,7 @@ "The repair must preserve the image dependencies:": "Reparationen måste bevara bildberoenden:", "The requested VMID block is already in use": "Det begärda VMID-blocket används redan", "The requested machine learning GPU profile is not working; it is not replaced by CPU": "Den begärda maskininlärningen GPU-profilen fungerar inte; den ersätts inte av CPU.", + "The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.": "Det gick inte att helt återställa resursändringen. Öppna hanteringsmenyn igen för att granska och importera de aktuella Proxmox-inställningarna.", "The restored service did not pass its health check": "Den återställda tjänsten passerade inte hälsokontrollen", "The restored service stopped; the recovery is not confirmed": "Den återställda tjänsten stannade; återhämtningen bekräftas inte", "The reviewed Tandoor stack does not require a privileged LXC.": "Den granskade Tandoor stack inte require en privilegierad LXC.", @@ -6681,6 +6723,7 @@ "The selected VM": "Den valda virtuella datorn", "The selected VM is running.": "Den valda virtuella datorn körs.", "The selected base folder does not exist and could not be created:": "Den valda basmappen finns inte och kunde inte skapas:", + "The selected bridge does not exist": "Den valda bryggan finns inte", "The selected bridge has no IPv4 subnet for the host-monitor firewall": "Den valda bryggan har inget IPv4-undernät för värd-monitorns brandvägg", "The selected configuration needs to start the LXC during the installation": "Den valda konfigurationen måste starta LXC under installationen", "The selected container is unprivileged. A privileged container is required for direct device passthrough.": "Den valda behållaren är oprivilegierad. En privilegierad behållare krävs för direkt enhetsgenomföring.", @@ -6703,6 +6746,7 @@ "The service builds its own image; only a published image can be installed": "Tjänsten bygger sin egen bild; endast en publicerad bild kan installeras.", "The service declares no image:": "Tjänsten förklarar ingen bild:", "The setting has no final value:": "Inställningen har inget slutvärde:", + "The settings could not be fully rolled back. Review the container and its saved modification before continuing.": "Det gick inte att återställa alla inställningar. Kontrollera containern och den sparade ändringen innan du fortsätter.", "The share already exists in smb.conf:": "Delningen finns redan i smb.conf:", "The shared destination is not a directory": "Den delade destinationen är inte en katalog", "The shared directory points to a protected host path": "Den delade katalogen pekar på en skyddad värdväg", @@ -6770,19 +6814,24 @@ "These Proxmox resources were added outside ProxMenux:": "Dessa Proxmox-resurser lades till utanför ProxMenux:", "These VMIDs are not free:": "Dessa VMID är inte gratis:", "These are the changes that will be made": "Det är dessa ändringar som kommer att göras", + "These changes were made in Proxmox outside ProxMenux:": "Dessa ändringar gjordes i Proxmox utanför ProxMenux:", "These container mount paths have an extension-like suffix:": "De här monteringssökvägarna i containern har ett suffix som liknar ett filtillägg:", "These containers were restored from a backup and this host has no record of their OCI application:": "Dessa behållare återställdes från en säkerhetskopia och denna värd har ingen post för deras OCI-applikation:", + "These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:": "Dessa enheter delar IOMMU-gruppen med den valda GPU:n och tilldelas till VM:en tillsammans med den:", "These host backups run with this job and expect its storage:": "Dessa värdsäkerhetskopior körs med det här jobbet och förväntar sig dess lagring:", "These host backups run with this job and will stop running:": "Dessa värdsäkerhetskopior körs med detta jobb och kommer att sluta köras:", "These interface configurations will be removed": "Dessa gränssnittskonfigurationer kommer att tas bort", "These manual changes cannot be adopted safely:": "Dessa manuella ändringar kan inte användas på ett säkert sätt:", "These paths will not be restored live and will be extracted for manual recovery.": "Dessa sökvägar kommer inte att återställas live och kommer att extraheras för manuell återställning.", + "These settings require an application-specific adaptation. The image was not downloaded and the container was not recreated.": "Dessa inställningar kräver en anpassning för just det här programmet. Avbildningen hämtades inte och containern återskapades inte.", "These values are asked during the installation:": "Dessa värden tillfrågas under installationen:", "They are described in the image documentation:": "De beskrivs i bilddokumentationen:", "They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.": "De kommer att läggas till oci-compose innan du fortsätter. Resurser som inte stöds eller ändras importeras inte.", + "They will be saved in oci-compose before continuing.": "De kommer att sparas i oci-compose innan du fortsätter.", "This CIFS share is mounted with restrictive permissions.": "Denna CIFS-andel är monterad med restriktiva behörigheter.", "This GPU is considered incompatible with GPU passthrough to a VM in ProxMenux.": "Denna GPU anses vara inkompatibel med GPU-genomföring till en virtuell dator i ProxMenux.", "This NFS share is fully restricted — even the host root cannot write to it.": "Denna NFS-resurs är helt begränsad — även värdroten kan inte skriva till den.", + "This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.": "Denna Proxmox Backup Server har ingen aktiv prenumeration, byta till arkivet utan prenumeration? Den otillgängliga Enterprise PBS-källan kommer att inaktiveras.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Denna virtuella dator kräver extra installationssteg, se installationsguiden på:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This account is for {registry}. Enter an image from that registry.": "Detta konto är för {registry}. Ange en bild från det registret.", "This action will:": "Denna åtgärd kommer att:", @@ -6866,6 +6915,7 @@ "This script cannot be executed from the Proxmox web terminal.": "Detta skript kan inte köras från Proxmox webbterminal.", "This script must be run as root.": "Detta skript måste köras som root.", "This script must be run on a Proxmox host.": "Detta skript måste köras på en Proxmox-värd.", + "This script will apply the following optimizations and advanced adjustments to your Proxmox Backup Server": "Detta skript kommer att tillämpa följande optimeringar och avancerade justeringar på din Proxmox Backup Server", "This script will apply the following optimizations and advanced adjustments to your Proxmox VE server": "Detta skript kommer att tillämpa följande optimeringar och avancerade justeringar på din Proxmox VE-server", "This script will update your Proxmox VE system with advanced options:": "Detta skript kommer att uppdatera ditt Proxmox VE-system med avancerade alternativ:", "This session is running in the Monitor terminal. Running it from here would cut the connection mid-install and leave the switch in a broken state.": "Den här sessionen körs i Monitor-terminalen. Om du kör den härifrån bryts anslutningen mitt under installationen och läget förblir felaktigt.", @@ -6952,7 +7002,7 @@ "Transaction log:": "Transaktionslogg:", "Translation files:": "Översättningsfiler:", "Transmission is designed for easy, powerful use. Transmission has the features you want from a BitTorrent client: encryption, a web interface, peer exchange, magnet links, DHT, µTP, UPnP and NAT-PMP port forwarding, webseed support, watch directories, tracker editing, global and per-torrent speed limits, and more.": "Transmission är utformad för enkel, kraftfull användning. Transmission har de funktioner du vill ha från en BitTorrent-klient: kryptering, ett webbgränssnitt, peer-utbyte, magnetlänkar, DHT, μTP, UPnP och NAT-PMP-port vidarebefordran, webseed support, watch kataloger, tracker redigering, globala och per-torrent hastighet gränser, och mer.", - "Tried pvesm path and manual detection methods": "Testade pvesm-väg och manuella detekteringsmetoder", + "Tried pvesm path and manual detection methods": "Har provat pvesm path och metoder för manuell identifiering", "Trust this certificate and save it for scheduled backups?": "Lita på det här certifikatet och spara det för schemalagda säkerhetskopieringar?", "Try Again": "Försök igen", "Try a different search term.": "Prova en annan sökterm.", @@ -6997,6 +7047,7 @@ "Unable to identify the root filesystem. Aborting.": "Det går inte att identifiera rotfilsystemet. Avbryter.", "Unable to resolve system ZFS pool disks. Aborting.": "Det går inte att lösa system ZFS pooldiskar. Avbryter.", "Unable to resolve system disk topology. Aborting.": "Det gick inte att lösa systemdisktopologin. Avbryter.", + "Unapplied changes": "Ändringar som inte har tillämpats", "Understand the security implications of privileged containers": "Förstå säkerhetskonsekvenserna av privilegierade containrar", "Unexpected Proxmox inventory; recovery blocked": "Oväntad Proxmox inventering; återhämtning blockerad", "Unexpected formatting directory in the new Valkey volume": "Oväntad formateringskatalog i den nya Valkey-volymen", @@ -7112,6 +7163,7 @@ "Unsupported mount type": "Unsupported Mount Typ", "Unsupported mount type:": "Unsupported Mount Typ:", "Unsupported native device type:": "Inbyggd enhetstyp som inte stöds:", + "Unsupported network settings": "Nätverksinställningarna stöds inte", "Unsupported operation": "Unsupported operation", "Unsupported or unknown Proxmox version; no repository changed.": "Ostödd eller okänd version av Proxmox; inget arkiv har ändrats.", "Unsupported output format:": "Utdataformat som inte stöds:", @@ -7192,6 +7244,7 @@ "Upgrade assistant: Proxmox VE 8 → 9 (Trixie)": "Uppgraderingsassistent: Proxmox VE 8 → 9 (Ttrixie)", "Upgrade canceled by user": "Uppgraderingen avbröts av användaren", "Upgrade one node at a time": "Uppgradera en nod i taget", + "Upgrade the system, offering the": "Uppgradera systemet och erbjuder", "Upgrade to Proxmox VE 9": "Uppgradera till Proxmox VE 9", "Upgrading NVIDIA utils (Arch)...": "Uppgraderar NVIDIA utils (Arch)...", "Upload an encrypted copy of the key to PBS so you can recover it on a reinstalled host with just a passphrase?": "Ladda upp en krypterad kopia av nyckeln till PBS så att du kan återställa den på en ominstallerad värd med bara en lösenfras?", @@ -7629,6 +7682,7 @@ "active VF(s)": "aktiva VF(s)", "active VFs": "aktiva VF:er", "active Virtual Functions. Changing its driver binding would destroy every VF.": "aktiva virtuella funktioner. Att ändra dess drivrutinsbindning skulle förstöra varje VF.", + "added in Proxmox": "lagt till i Proxmox", "after copy; source tree is incomplete.": "efter kopia; källträdet är ofullständigt.", "after update.": "efter uppdatering.", "all guests": "alla gäster", @@ -7973,8 +8027,8 @@ "pvesm command not found. This should not happen on Proxmox.": "pvesm-kommandot hittades inte. Detta bör inte hända på Proxmox.", "pvesm not available.": "pvesm inte tillgänglig.", "pvesm not found.": "pvesm hittades inte.", - "pvesm path failed, trying manual detection...": "pvesm sökväg misslyckades, försöker manuell upptäckt...", - "pvesm status failed": "pvesm-status misslyckades", + "pvesm path failed, trying manual detection...": "pvesm path misslyckades, försöker identifiera manuellt...", + "pvesm status failed": "pvesm status misslyckades", "pyLoad is a Free and Open Source download manager written in Python and designed to be extremely lightweight, easily extensible and fully manageable via web.": "pyLoad är en gratis och öppen källkod nedladdningshanterare skriven i Python och utformad för att vara extremt lätt, lättföränderlig och fullt hanterbar via webben.", "pyLoad web interface": "PyLoad webbgränssnitt", "qBittorrent WebUI password (user: admin)": "qBittorrent WebUI lösenord (användare: admin)", @@ -7996,6 +8050,7 @@ "remapped users": "ommappade användare", "remove the (now-empty) directory if possible": "ta bort den (nu tomma) katalogen om möjligt", "removed from Proxmox": "borttagen från Proxmox", + "removed in Proxmox": "borttagen i Proxmox", "removed successfully from Proxmox.": "har tagits bort från Proxmox.", "requires running the official installer, which restarts the Monitor service.": "kräver att det officiella installationsprogrammet körs, vilket startar om Monitor-tjänsten.", "requires the package": "kräver paketet", @@ -8052,7 +8107,7 @@ "switch_gpu_mode.sh was not found.": "switch_gpu_mode.sh hittades inte.", "sysfs ROM dump failed — trying ACPI VFCT table...": "sysfs ROM-dump misslyckades — försöker ACPI VFCT-tabell...", "syslog-ng allows you to flexibly collect, parse, classify, rewrite and correlate logs from across your infrastructure and store or route them to log analysis tools.": "syslog-ng låter dig flexibelt samla in, analysera, klassificera, skriva och korrelera loggar från hela din infrastruktur och lagra eller dirigera dem för att logga analysverktyg.", - "systemctl restart networking failed:": "systemctl omstart av nätverk misslyckades:", + "systemctl restart networking failed:": "systemctl restart networking misslyckades:", "systemd OnCalendar expression": "systemd OnCalendar-uttryck", "the API key was not generated on the first start": "API-nyckeln genererades inte i första början", "the NVIDIA hook of the container is not available:": "NVIDIA-hooken för behållaren är inte tillgänglig:", @@ -8099,6 +8154,7 @@ "using": "använder", "using direct path (not guaranteed to survive reboots).": "använder direkt sökväg (inte garanterat att överleva omstarter).", "vCPUs:": "vCPU:er:", + "value changed in Proxmox": "värde ändrat i Proxmox", "vfio-pci IDs configured": "vfio-pci ID:n konfigurerade", "vfio-pci IDs in /etc/modprobe.d/vfio.conf": "vfio-pci ID i /etc/modprobe.d/vfio.conf", "video": "video", @@ -8111,6 +8167,7 @@ "was": "var", "was installed, but the kernel reports:": "installerades, men kärnan rapporterar:", "when finished": "När den är färdig", + "when the server has no subscription (you will be asked)": "när servern inte har något abonnemang (du kommer att bli tillfrågad)", "will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.": "kommer att binda om GPU:n till vfio-pci vid nästa omstart, vilket bryter drivrutinen som är på väg att installeras.", "wipefs failed on": "wipefs misslyckades", "with": "med", diff --git a/oci/README.md b/oci/README.md index bde5cfb3..a18414a4 100644 --- a/oci/README.md +++ b/oci/README.md @@ -142,7 +142,7 @@ An application made of several images is one entry of the catalog and one instal | Operation | What changes | What is kept | |---|---|---| | **Update** | The rootfs is replaced by the image the saved tag publishes today | Record, container disks, host directories, network and devices | -| **Modify** | An editor opens with the current resources, network, paths and GPU; the container is rebuilt with the changes | The data of container disks and host directories | +| **Modify** | Edit and confirm the configuration; supported native settings apply directly, while other changes use the existing recreation flow | Native edits keep the installed image and rootfs; both paths preserve persistent data | | **Recreate** (multi-container) | Every container is rebuilt from the image it was installed with | Data, network and devices | | **Watchdog** | Whether the application is started again when it stops on its own | Everything; only the choice is saved | | **Remove** | The LXC, or every container of the application, its disks and its record | Host directories, with their content | @@ -159,6 +159,10 @@ Settings changed in Proxmox VE after the installation, such as memory, cores or A rolling tag is not an unattended update: the digest changes only through an explicit update. +For standalone applications, CPU and RAM changes apply live. Supported network settings, environment overrides, directory mounts, additional managed volumes and native devices use a controlled stop/start when needed. Modify does not query the registry or download an image for these native edits, even if the cached archive has already been cleaned. Removing a managed mount detaches its disk without deleting its data; subsequent image updates park that unused disk and return it to the container. An optional retained backup does not force recreation. + +The native path checks the saved configuration and installation identity, keeps a private before/after journal, and restores the original settings if applying them or checking the restarted service fails. It refuses to overwrite concurrent edits. Changes that require installer adaptations, such as NVIDIA runtime changes, GPU permission/environment setup, file mounts, completion notes or a refreshed catalog template, use the existing validated recreation flow. The route is chosen before applying any settings, so one proposal is not partially applied by both paths. The recreation flow reuses a verified cached archive when it matches the resolved image; it downloads an image when a suitable archive is unavailable. Image changes and coordinated stack operations retain their separate update flow. + ## From a Compose file or a docker run command to an oci-compose The converter reads a Compose file with one service. A `docker run` command and an image reference alone are first written as a Compose file and follow the same path, so the three produce the same kind of oci-compose. diff --git a/oci/remote/install_oci.sh b/oci/remote/install_oci.sh index 7d1d8e85..b3b100e6 100755 --- a/oci/remote/install_oci.sh +++ b/oci/remote/install_oci.sh @@ -359,13 +359,18 @@ cleanup_canonical_inputs() { # A derived check accepts any HTTP answer below 500: the application is up, # even when its first page is a redirect or asks for a login. healthcheck_probe() { + local code status=0 if [[ ${HC_ANY_STATUS:-false} == true ]]; then - local code - code=$(curl "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null) || return 1 - [[ $code =~ ^[1-4][0-9][0-9]$ ]] + code=$(curl "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null) || status=$? + (( status == 0 )) && [[ $code =~ ^[1-4][0-9][0-9]$ ]] && return 0 else - curl "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null + curl "${CURL_ARGS[@]}" "$HC_URL" >/dev/null 2>&1 || status=$? + (( status == 0 )) && return 0 fi + # A port taken from the compose file may serve a database or SSH: an answer + # in another protocol (HTTP/0.9, weird or empty reply, reset) means it is up. + [[ ${HC_ACCEPT_NON_HTTP:-false} == true ]] \ + && (( status == 1 || status == 8 || status == 52 || status == 56 )) } cleanup_failed_install() { @@ -1025,15 +1030,18 @@ apply_extra_hosts() { } # Settings an update gives back to the rebuilt container before it starts: -# the LAN leg of a suite or stack member and its start order. +# the LAN leg of a suite or stack member, its start order and current tags. apply_kept_settings() { local key value while IFS=$'\t' read -r key value; do [[ -n $key ]] || continue - [[ $key == net1 || $key == startup ]] \ + [[ $key == net1 || $key == startup || $key == tags ]] \ || die "$(translate "Unsupported kept setting:") $key" - oci_quiet pct set "$VMID" "--$key" "$value" \ - || die "$(translate "Could not restore the container setting:") $key" + if [[ $key == tags && -z $value ]]; then + oci_quiet pct set "$VMID" --delete tags + else + oci_quiet pct set "$VMID" "--$key" "$value" + fi || die "$(translate "Could not restore the container setting:") $key" done < <(jq -r '.kept_proxmox_settings // {} | to_entries[] | [.key, .value] | @tsv' "$DEPLOYMENT_FILE") } @@ -1091,7 +1099,7 @@ ensure_runtime_home() { passwd_file="${rootfs}/etc/passwd" mount_ct_rootfs if ! home=$(python3 "$OCI_RUNTIME_RESOLVER" --home "$passwd_file" "$uid" 2>>"${OCI_LOG:-/dev/stderr}"); then - oci_quiet pct unmount "$VMID" || true + oci_quiet pct unmount "$VMID" || die "$(translate "Could not unmount the container filesystem")" return 0 fi oci_quiet pct unmount "$VMID" || die "$(translate "Could not unmount the container filesystem")" @@ -1230,6 +1238,17 @@ apply_installer_profile() { fi [[ -d ${rootfs}${target} ]] \ || { oci_log "The volume to prepare does not exist: $target"; failed=1; break; } + # A reused volume may intentionally be mounted read-only (for example, + # /run/secrets). Do not remove files or change ownership/mode on it. + # Use the most specific containing mount: an RW child can override RO. + if [[ $(jq -r --arg target "$target" ' + [.mounts[]? | select(.container_path as $path | + $target == $path or ($target | startswith($path + "/")))] + | sort_by(.container_path | length) | last | .read_only // false' \ + "$DEPLOYMENT_FILE") == true ]]; then + oci_log "Skipping preparation of read-only volume: $target" + continue + fi if [[ $remove_lost_found == true ]]; then rm -rf "${rootfs}${target}/lost+found" || { failed=1; break; } fi @@ -2253,6 +2272,7 @@ if [[ $START_AFTER == 1 && $HAS_STARTUP_HEALTHCHECK == 1 ]]; then [[ -n $IP ]] || die "$(translate "The healthcheck cannot run without an IP address")" HC_URL="${HC_SCHEME}://${IP}:${HC_PORT}${HC_PATH}" HC_ANY_STATUS=$(jq -r '.accept_any_status // false' <<<"$STARTUP_HEALTHCHECK") + HC_ACCEPT_NON_HTTP=$(jq -r '.accept_non_http // false' <<<"$STARTUP_HEALTHCHECK") if [[ $HC_ANY_STATUS == true ]]; then CURL_ARGS=(-sS --noproxy '*' -o /dev/null -w '%{http_code}' --max-time "$HC_REQUEST_TIMEOUT") else diff --git a/oci/remote/oci_carried_record.py b/oci/remote/oci_carried_record.py index 725640dd..14375ef5 100644 --- a/oci/remote/oci_carried_record.py +++ b/oci/remote/oci_carried_record.py @@ -38,6 +38,8 @@ DIRECTORY = '.proxmenux' NAME = 'oci-record.json' KIND = 'proxmenux.oci-carried-record' STAMP = 'carried.sha256' +CONFIG_CACHE = 'carried-config.json' +LOCAL_CONFIGS = Path('/etc/pve/lxc') LIMIT = 16 * 1024 * 1024 STACK_CONTRACT = '/etc/pve/priv/proxmenux-stack-{}.json' NVIDIA_HOOK = re.compile(r'/usr/local/lib/proxmenux/oci/nvidia-mount-([a-f0-9]{64})\.sh') @@ -279,6 +281,47 @@ def read_stamp(path): return value if isinstance(value, dict) else {} +def configuration(root, vmid, verify=False): + """Reuse pct's formatted output only while its local source is unchanged. + + Opening Manage checks every carried copy. Starting a fresh pct process + for each unchanged guest dominates that check. The private host cache is + keyed by the complete native configuration, including notes, pending + changes and snapshot sections. Lifecycle verification always calls pct. + An unavailable source, invalid cache or concurrent edit falls back to pct. + """ + source = LOCAL_CONFIGS / f'{int(vmid)}.conf' + cache = instances.location(root, vmid).parent / CONFIG_CACHE + + def fingerprint(): + try: + return sha(source.read_bytes()) + except OSError: + return None + + before = fingerprint() + if before is not None and not verify: + try: + if not cache.is_symlink() and cache.stat().st_size <= LIMIT: + value = json.loads(cache.read_text()) + if (isinstance(value, dict) and value.get('schema_version') == 1 + and value.get('source_sha256') == before + and isinstance(value.get('config'), str) and value['config'] + and fingerprint() == before): + return value['config'] + except (OSError, ValueError): + pass + result = _run('pct', 'config', str(vmid)) + if result.returncode != 0: + return None + # Do not associate output from one revision with another revision's hash. + if before is not None and fingerprint() == before: + with contextlib.suppress(OSError, ValueError): + instances.write(cache, {'schema_version': 1, 'source_sha256': before, + 'config': result.stdout}) + return result.stdout + + def superseded(record, existing, stamp): """Whether the container carries a copy this host did not write, with a record that differs from the one of this host. That is a container that @@ -308,10 +351,10 @@ def carry(root, vmid, mount_stopped=True, verify=False, adopted=False): if record.get('status') != 'installed' or record.get('pending_transaction') \ or record.get('pending_stack_transaction'): return 'skipped' - result = _run('pct', 'config', str(vmid)) - if result.returncode != 0 or instances.identity(result.stdout.encode()) != record['installation_id']: + config = configuration(root, vmid, verify) + if config is None or instances.identity(config.encode()) != record['installation_id']: return 'skipped' - value = bundle(record, result.stdout) + value = bundle(record, config) expected = digest(value) record_path = instances.location(root, vmid) path = record_path.parent / STAMP @@ -340,7 +383,7 @@ def carry(root, vmid, mount_stopped=True, verify=False, adopted=False): else: value['saved_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat() value['generation'] = uuid.uuid4().hex - write_copy(rootfs, value, mapped_root(result.stdout)) + write_copy(rootfs, value, mapped_root(config)) stamp = {'generation': value['generation']} outcome = 'carried' if outcome == 'stale': diff --git a/oci/remote/oci_instance_reconcile.py b/oci/remote/oci_instance_reconcile.py index 07291125..cd8632a7 100644 --- a/oci/remote/oci_instance_reconcile.py +++ b/oci/remote/oci_instance_reconcile.py @@ -9,6 +9,7 @@ import oci_instances as instances import oci_gpu_devices as gpu_devices import oci_host_mounts as host_mounts import oci_instance_transaction as transaction +import oci_native_modify as native_modify from oci_installation_state import parse_config, sha from oci_ui import translate @@ -88,6 +89,43 @@ def _device(key, value): return device +ENVIRONMENT_KEYS = ('env', 'lxc.environment.runtime') + + +def _environment(record, config): + """Variables changed in Proxmox since the last operation. Values are + applied but never listed: they may be passwords.""" + before = native_modify.environment(record['observed']['config'].encode()) + now = native_modify.environment(config) + defaults = {item.split('=', 1)[0] for item in record['observed']['image'].get('defaults', {}).get('Env', []) + if '=' in item} + removed = sorted(before.keys() - now.keys()) + # An image default comes back with a new container; its absence cannot be kept. + if any(name in defaults for name in removed): + raise ValueError(f"{translate('These manual changes cannot be adopted safely:')} env") + changed = sorted(name for name in now if before.get(name) != now[name]) + return {name: now[name] for name in changed}, removed + + +def _adopt_environment(deployment, values, removed): + items = deployment.setdefault('environment', []) + details = [] + for name, value in values.items(): + matches = [item for item in items if item['name'] == name] + if len(matches) > 1: + raise ValueError(translate('Duplicate variable in the contract; review it before editing')) + if matches: + matches[0]['value'] = value + details.append(f"env {name}: {translate('value changed in Proxmox')}") + else: + items.append({'name': name, 'value': value, 'sensitive': True}) + details.append(f"env {name}: {translate('added in Proxmox')}") + for name in removed: + items[:] = [item for item in items if item['name'] != name] + details.append(f"env {name}: {translate('removed in Proxmox')}") + return details + + def propose(record, config): if (record.get('status') != 'installed' or record.get('stack') or record.get('stack_member') or record.get('native_stack_intent') or record.get('deployment', {}).get('stack_managed')): @@ -101,16 +139,20 @@ def propose(record, config): # was checked above, so a presentation-only change needs no adoption. if 'description' in changed: changed.remove('description') + environment = None + if any(key in ENVIRONMENT_KEYS for key in changed): + changed = [key for key in changed if key not in ENVIRONMENT_KEYS] + environment = _environment(record, config) new_keys = [key for key in changed if key not in before and re.fullmatch(r'(mp|dev)[0-9]+', key)] unsupported = [key for key in changed if key not in new_keys and key not in transaction.ADOPTABLE and key not in transaction.KEPT_AS_IS] if unsupported: raise ValueError(f"{translate('These manual changes cannot be adopted safely:')} {', '.join(unsupported)}") - if not new_keys: + if not new_keys and environment is None: return None candidate = copy.deepcopy(record) deployment = candidate['deployment'] - details = [] + details = _adopt_environment(deployment, *environment) if environment else [] for key in new_keys: if key.startswith('mp'): mount = _mount(key, current[key], record['vmid']) @@ -122,8 +164,13 @@ def propose(record, config): deployment.setdefault('devices', []).append(device) kind = 'GPU' if gpu_devices.gpu_path(device['host_path']) else 'USB/Coral/NPU' details.append(f"{key}: {device['host_path']} ({kind})") + skipped = [*new_keys, *(ENVIRONMENT_KEYS if environment else ())] filtered = b'\n'.join(line for line in config.splitlines() - if not any(line.startswith(key.encode() + b': ') for key in new_keys)) + b'\n' + if not any(line.startswith(key.encode() + b': ') for key in skipped)) + if environment: + filtered += b''.join(b'\n' + line for line in record['observed']['config'].encode().splitlines() + if any(line.startswith(key.encode() + b': ') for key in ENVIRONMENT_KEYS)) + filtered += b'\n' for key, value in transaction.external_changes(record, filtered).items(): section, name = transaction.ADOPTABLE[key] target = deployment.setdefault(section, {}) if section else deployment diff --git a/oci/remote/oci_instance_transaction.py b/oci/remote/oci_instance_transaction.py index 59579967..cc36b0d0 100644 --- a/oci/remote/oci_instance_transaction.py +++ b/oci/remote/oci_instance_transaction.py @@ -320,6 +320,36 @@ def mounts(config): return result +def unused_volumes(config): + return {key: value for key, value in parse_config(config).items() + if re.fullmatch(r'unused[0-9]+', key)} + + +def move_retained_unused(state, to_stage): + """Unused disks are not in vzdump. Park and return them separately, using + PVE's volume reassignment API, never --delete (which frees an unused disk). + The journal's keys and both ownership markers also make retries safe.""" + keys = set(state.get('retained_unused', {})) + if not keys: + return + vmid, stage = state['vmid'], state['stage'] + guest_marker = state['record']['installation_id'] + holder_marker = 'proxmenux-transaction=' + state['id'] + source, target = (vmid, stage) if to_stage else (stage, vmid) + source_marker, target_marker = ((guest_marker, holder_marker) if to_stage + else (holder_marker, guest_marker)) + for key in sorted(keys): + present = unused_volumes(owned(source, source_marker)) + destination = unused_volumes(owned(target, target_marker)) + if key not in present: + if key in destination: + continue # Already reassigned before an interruption. + raise ValueError(translate('There are extra disks or bind mounts outside the journal; the rootfs is not replaced')) + if key in destination or (to_stage and present.keys() - keys): + raise ValueError(translate('There are extra disks or bind mounts outside the journal; the rootfs is not replaced')) + run('pct', 'move-volume', str(source), key, '--target-vmid', str(target), '--target-volume', key) + + def is_file_bind(mount, files=()): return mount['type'] == 'host-bind' and (mount['source'] in files or Path(mount['source']).is_file()) @@ -352,6 +382,22 @@ def profile_keys(deployment): return keys +GUESSED_ENDPOINT = 'compose-first-tcp-port-fallback' +# curl exit codes for a port that accepted the connection and answered in a +# protocol other than HTTP: HTTP/0.9 refused, weird reply, empty reply, reset. +NON_HTTP_ANSWER = (1, 8, 52, 56) + + +def probe_http(args): + """Run one curl probe and return its output and exit code.""" + log('$ ' + shlex.join(str(arg) for arg in args)) + try: + result = subprocess.run(args, capture_output=True, timeout=60, close_fds=True) + except subprocess.TimeoutExpired: + return b'', -1 + return result.stdout, result.returncode + + def effective_healthcheck(template): """The template's own health check or, when it has none, one derived from its web address: any HTTP answer below 500 means the application is up.""" @@ -367,9 +413,14 @@ def effective_healthcheck(template): path = str(endpoint.get('path') or '/') if (endpoint.get('scheme') in ('http', 'https') and str(endpoint.get('port', '')).isdigit() and path.startswith('/') and not any(c.isspace() for c in path)): - return {'scheme': endpoint['scheme'], 'port': int(endpoint['port']), 'path': path, - 'timeout_seconds': 300, 'request_timeout_seconds': 10, 'stability_seconds': 4, - 'verify_tls': False, 'accept_any_status': True} + check = {'scheme': endpoint['scheme'], 'port': int(endpoint['port']), 'path': path, + 'timeout_seconds': 300, 'request_timeout_seconds': 10, 'stability_seconds': 4, + 'verify_tls': False, 'accept_any_status': True} + # A port taken from the compose file because the recipe names no + # web address may serve a database or SSH: any answer counts. + if endpoint.get('source') == GUESSED_ENDPOINT: + check['accept_non_http'] = True + return check # Without a web address, the new image must at least keep the container running. return {'type': 'running', 'timeout_seconds': 90, 'stability_seconds': 20} @@ -415,10 +466,9 @@ def candidate_contract(record, operation, proposal=None): return with_default_healthcheck(result) -# Resource settings edited in Proxmox that the new container keeps. # Proxmox settings the rebuilt container gets back exactly as they are: the -# LAN leg ProxMenux adds to a suite or stack member, and the start order. -KEPT_AS_IS = ('net1', 'startup') +# LAN leg ProxMenux adds to a suite or stack member, the start order and tags. +KEPT_AS_IS = ('net1', 'startup', 'tags') ADOPTABLE = {'memory': ('resources', 'memory_mb'), 'swap': ('resources', 'swap_mb'), 'cores': ('resources', 'cores'), 'cpulimit': ('resources', 'cores'), 'cpuunits': ('resources', 'cpu_units'), 'onboot': (None, 'onboot')} @@ -453,7 +503,7 @@ def external_changes(record, config, adopt=True): return values -def preflight(record, candidate, config, coordinated=None): +def preflight(record, candidate, config, coordinated=None, in_place=False, baseline=True): deployment = record['deployment'] desired = candidate['deployment'] if coordinated and (record.get('native_stack_intent') or deployment.get('rootfs_adaptation_replay_required')): @@ -462,7 +512,7 @@ def preflight(record, candidate, config, coordinated=None): raise ValueError(translate('Stack members are updated together with their stack')) if instances.identity(config) != record['installation_id']: raise ValueError(translate('The container identity changed; the container is not replaced')) - for key, value in external_changes(record, config, adopt=not coordinated).items(): + for key, value in (external_changes(record, config, adopt=not coordinated) if baseline else {}).items(): section, name = ADOPTABLE[key] recorded = deployment.get(section, {}) if section else deployment target = desired.setdefault(section, {}) if section else desired @@ -491,7 +541,17 @@ def preflight(record, candidate, config, coordinated=None): runtime_keys.add('lxc.mount.entry') runtime_settings.check(config, deployment, record['vmid']) coordinated_keys = {'hookscript', *KEPT_AS_IS} if coordinated else set(KEPT_AS_IS) - if '[' in config.decode() or keys - BASIC - runtime_keys - coordinated_keys - {k for k in keys if re.fullmatch(r'(mp|dev)[0-9]+', k)}: + preserved_keys = {'nameserver', 'searchdomain', 'timezone', 'protection'} if in_place else set() + detached = unused_volumes(config) + if baseline and detached != unused_volumes(record['observed'].get('config', '').encode()): + raise ValueError(translate('The container disks do not match the saved record')) + used_volumes = {value.split(',', 1)[0] for key, value in cfg.items() + if key == 'rootfs' or re.fullmatch(r'mp[0-9]+', key)} + if (len(set(detached.values())) != len(detached) + or any(not re.fullmatch(r'[A-Za-z0-9_.-]+:[^\s,]+', value) or value in used_volumes + for value in detached.values())): + raise ValueError(translate('The container disks do not match the saved record')) + if '[' in config.decode() or keys - BASIC - runtime_keys - coordinated_keys - preserved_keys - {k for k in keys if re.fullmatch(r'(mp|dev|unused)[0-9]+', k)}: raise ValueError(translate('The container has advanced Proxmox settings outside the supported profile')) unprivileged = deployment.get('security', {}).get('unprivileged', True) is not False if (desired.get('security', {}).get('unprivileged', True) is not False) != unprivileged \ @@ -511,9 +571,10 @@ def preflight(record, candidate, config, coordinated=None): target = host_mounts.valid_path(mount['container_path']) if mount['type'] == 'host-bind' and mount['source'] in declared and mount.get('read_only'): continue - if (any(target == p or target.startswith(p.rstrip('/') + '/') - or p.startswith(target.rstrip('/') + '/') for p in paths)): - raise ValueError(translate('Mount paths must not overlap')) + other = next((p for p in paths if target == p or target.startswith(p.rstrip('/') + '/') + or p.startswith(target.rstrip('/') + '/')), None) + if other: + raise ValueError(f"{translate('Mount paths must not overlap')}: {target} - {other}") if mount['type'] == 'managed-volume': if mount.get('backup') is not True or not isinstance(mount.get('size_gb'), int) or mount['size_gb'] < 1: raise ValueError(translate('Managed disks must have backup enabled and a valid size')) @@ -532,7 +593,8 @@ def preflight(record, candidate, config, coordinated=None): raise ValueError(translate('Changing the rootfs or its storage requires a separate migration')) actual = mounts(config) gpu_devices.check(config, deployment) - gpu_devices.verify_baseline(record['observed'].get('gpu_devices', {}), deployment) + if baseline: + gpu_devices.verify_baseline(record['observed'].get('gpu_devices', {}), deployment) recorded_files = {m['source']: bool(m.get('read_only')) for m in deployment.get('mounts', []) if is_file_bind(m, files)} if files != recorded_files: raise ValueError(translate('A container mount has a source, backup or permission different from the saved record')) @@ -547,7 +609,9 @@ def preflight(record, candidate, config, coordinated=None): or mount.get('ro', '0') != ('1' if old[target].get('read_only') else '0')): raise ValueError(translate('A container mount has a source, backup or permission different from the saved record')) for target in old.keys() & new.keys(): - if any(old[target].get(k) != new[target].get(k) for k in ('type', 'source', 'size_gb')): + directory_bind = old[target]['type'] == new[target]['type'] == 'host-bind' + fixed = ('type', 'size_gb') if directory_bind else ('type', 'source', 'size_gb') + if any(old[target].get(k) != new[target].get(k) for k in fixed): raise ValueError(translate('Changing the storage or size of a disk requires a migration; empty disks are not created')) protected = ('/bin', '/sbin', '/etc', '/usr', '/lib', '/lib64', '/proc', '/sys', '/dev', '/run') for target in new.keys() - old.keys(): @@ -734,11 +798,10 @@ def healthcheck(vmid, template): args += ['-w', '%{http_code}'] if not check.get('verify_tls', False): args.append('-k') - try: - answer = run(*args, f'{scheme}://{ip}:{port}{path}') - ok = not any_status or re.fullmatch(rb'[1-4][0-9][0-9]', answer.strip()) is not None - except RuntimeError: - pass + answer, status = probe_http([*args, f'{scheme}://{ip}:{port}{path}']) + ok = status == 0 and (not any_status or re.fullmatch(rb'[1-4][0-9][0-9]', answer.strip()) is not None) + if not ok and check.get('accept_non_http') and status in NON_HTTP_ANSWER: + ok = True if ok: stable_since = stable_since or time.monotonic() if time.monotonic() - stable_since >= stability: @@ -1041,6 +1104,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i for p, m in actual.items() if p in required and not m['volume'].startswith('/')]) state = {'schema_version': 1, 'id': directory.name, 'vmid': vmid, 'operation': operation, 'record': record, 'candidate_contract': candidate, 'before_config': before.decode(), + 'retained_unused': unused_volumes(before), 'original_description': description, 'archive': str(archive), 'archive_sha256': filehash(archive), 'registry_digest': registry_digest or image['manifest_digest'], @@ -1051,6 +1115,9 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i 'original_gpu_devices': original_gpu, 'desired_gpu_devices': desired_gpu, 'was_running': run('pct', 'status', str(vmid)).strip() == b'status: running'} state['kept_config'] = {key: cfg[key] for key in KEPT_AS_IS if key in cfg} + # The installer adds default tags. Preserve an explicit absence too, so + # deleting all tags in Proxmox does not bring them back on the next update. + state['kept_config']['tags'] = cfg.get('tags', '') if not coordinated and state['kept_config']: # Applied by the installer before the new container starts, so the # application has its LAN leg from the first second. @@ -1058,9 +1125,10 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i if coordinated: state['coordinated'] = coordinated state['preserved_stack_config'] = {key: cfg[key] for key in ('net1', 'startup', 'hookscript') if key in cfg} + state['preserved_stack_config']['tags'] = cfg.get('tags', '') if operation == 'update': state['preserved_stack_config'].update({key: cfg[key] for key in - ('net0', 'features', 'cmode', 'console', 'tty', 'cpuunits', 'tags') if key in cfg}) + ('net0', 'features', 'cmode', 'console', 'tty', 'cpuunits') if key in cfg}) checkpoint(journal, state, 'prepared') pending = copy.deepcopy(record) pending.update(status='updating', pending_transaction=str(journal), transaction_id=state['id']) @@ -1114,6 +1182,7 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i else: owned(stage, 'proxmenux-transaction=' + state['id']) run('pct', 'move-volume', str(vmid), mount['key'], '--target-vmid', str(stage), '--target-volume', mount['key']) + move_retained_unused(state, to_stage=True) checkpoint(journal, state, 'data-parked') if show: msg_ok(translate('Data volumes protected')) @@ -1123,8 +1192,8 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i msg_info(translate('Installing the new image...') if update else translate('Recreating the container...')) current = owned(vmid, record['installation_id']) expected = b''.join(line for line in before.splitlines(keepends=True) - if not re.match(rb'mp[0-9]+: ', line)) - if mounts(current) or current != expected: + if not re.match(rb'(mp|unused)[0-9]+: ', line)) + if mounts(current) or unused_volumes(current) or current != expected: raise ValueError(translate('A concurrent change was detected; the container is not removed')) host_mounts.verify_sources(original_sources) gpu_devices.verify(original_gpu) @@ -1139,9 +1208,13 @@ def apply(root, vmid, archive, operation, proposal=None, registry_digest=None, i install_candidate(root, journal, state, progress) restore_description(vmid, state) restore_firewall(vmid, state) + move_retained_unused(state, to_stage=False) if coordinated: for key, value in state['preserved_stack_config'].items(): - run('pct', 'set', str(vmid), '--' + key, value) + if key == 'tags' and not value: + run('pct', 'set', str(vmid), '--delete', 'tags') + else: + run('pct', 'set', str(vmid), '--' + key, value) check_runtime_mounts(run('pct', 'config', str(vmid)), candidate['deployment']) gpu_devices.check(run('pct', 'config', str(vmid)), candidate['deployment']) state['staged_config_sha256'] = sha(owned(vmid, record['installation_id'])) @@ -1273,6 +1346,9 @@ def complete_recovery(root, journal, state): def recover(root, journal): state = json.loads(journal.read_text()) + if state.get('kind') == 'native-modify': + import oci_native_modify + return oci_native_modify.recover(root, journal) vmid = state['vmid'] show = not state.get('coordinated') if show: @@ -1335,8 +1411,9 @@ def recover(root, journal): stop(vmid) current = owned(vmid, record['installation_id']) gpu_devices.check_recovery_entries(current, state) - if any(re.fullmatch(r'unused[0-9]+', k) for k in parse_config(current)): + if unused_volumes(current).keys() - state.get('retained_unused', {}).keys(): raise ValueError(translate('There are extra disks or bind mounts outside the journal; the rootfs is not replaced')) + move_retained_unused(state, to_stage=True) known_binds = {(m['source'], m['container_path']) for plan in (state['record']['deployment'], state['candidate_contract']['deployment']) for m in plan.get('mounts', []) if m['type'] == 'host-bind'} @@ -1364,6 +1441,7 @@ def recover(root, journal): host_mounts.verify_sources(state.get('original_host_sources', {})) run('pct', 'restore', str(vmid), backup, '--force', '1', '--storage', state['record']['deployment']['rootfs']['storage']) + move_retained_unused(state, to_stage=False) state['restore_config_sha256'] = sha(owned(vmid, state['record']['installation_id'])) checkpoint(journal, state, 'checking-recovery') if show: diff --git a/oci/remote/oci_instances.py b/oci/remote/oci_instances.py index f039b956..0f4d19ce 100644 --- a/oci/remote/oci_instances.py +++ b/oci/remote/oci_instances.py @@ -230,8 +230,9 @@ def identity(config): return match.group(1) if match else None -def same_config_except_notes(record, config): - """Accept a presentation-only note edit, never a changed OCI identity or LXC setting.""" +def same_config_except_notes(record, config, ignore_tags=False): + """Accept a presentation-only note edit, never a changed OCI identity or LXC setting. + With `ignore_tags`, Proxmox tags may differ too: the operation keeps them as they are.""" previous = record['observed']['config'].encode() expected = record['installation_id'] if identity(previous) != expected or identity(config) != expected: @@ -242,7 +243,8 @@ def same_config_except_notes(record, config): notes = [line for line in lines if line.startswith(b'description: ')] if len(notes) != 1: return None - return b''.join(line for line in lines if not line.startswith(b'description: ')) + dropped = (b'description: ', b'tags: ') if ignore_tags else (b'description: ',) + return b''.join(line for line in lines if not line.startswith(dropped)) original = without_notes(previous) return original is not None and original == without_notes(config) diff --git a/oci/remote/oci_native_modify.py b/oci/remote/oci_native_modify.py new file mode 100644 index 00000000..8cefa086 --- /dev/null +++ b/oci/remote/oci_native_modify.py @@ -0,0 +1,426 @@ +"""Edit an installed application's LXC configuration, retaining its rootfs/data. + +Runtime changes use a controlled stop/start, not pct destroy/create. Values, +including secrets and NUL-separated environment, reach PVE over stdin only. +""" +import copy +import hashlib +import ipaddress +import json +from pathlib import Path +import re +import subprocess +import uuid + +import oci_instances as instances +import oci_instance_transaction as transaction +import oci_resource_modify as resources +import oci_gpu_devices as devices +import oci_host_mounts as host_mounts +from oci_installation_state import parse_config, sha +from oci_ui import translate, msg_info, msg_ok, msg_warn + +PERL_UPDATE = r''' +use strict; use warnings; use JSON::PP; +use PVE::RPCEnvironment; use PVE::INotify; use PVE::API2::LXC::Config; +PVE::RPCEnvironment->setup_default_cli_env(); +my $p = decode_json(do { local $/; }); +$p->{node} = PVE::INotify::nodename(); +PVE::API2::LXC::Config->update_vm($p); +''' + + +def environment(config): + result = {} + entries = [] + for line in config.decode().splitlines(): + if line.startswith('lxc.environment.runtime: '): + entries.append(line.split(': ', 1)[1]) + elif line.startswith('env: '): + # pct config exposes PVE's public env property, whereas older + # observations and native files contain repeated runtime lines. + entries.extend(line.split(': ', 1)[1].split('\0')) + for entry in entries: + name, sep, value = entry.partition('=') + if not sep or name in result: + raise ValueError(translate('Duplicate variable in the contract; review it before editing')) + result[name] = value + return result + + +def environment_values(items): + result = {} + for item in items: + name, value = item['name'], str(item['value']) + if (not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name) or name in result + or any(ord(c) < 32 or ord(c) == 127 for c in value)): + raise ValueError(translate('The value contains an unsupported character')) + result[name] = value + return result + + +def changes(record, candidate, config): + """Only unchanged recipes and native settings; adapters keep their installer.""" + old = transaction.with_default_healthcheck(copy.deepcopy(record)) + candidate = transaction.with_default_healthcheck(copy.deepcopy(candidate)) + if (old['template'] != candidate['template'] or old['deployment'].get('host_monitor') + or any(k in record for k in ('stack', 'stack_member', 'native_stack_intent')) + or old['deployment'].get('stack_managed')): + return None + before, after = old['deployment'], candidate['deployment'] + changed = {k for k in before.keys() | after.keys() if before.get(k) != after.get(k)} + allowed = {'resources', 'network', 'environment', 'devices', 'mounts', 'hardware_profile'} + if not changed or changed - allowed: + return None + resource_keys = {k for k in before.get('resources', {}).keys() | after.get('resources', {}).keys() + if before.get('resources', {}).get(k) != after.get('resources', {}).get(k)} + if resource_keys - {'cores', 'memory_mb', 'swap_mb', 'cpu_units'}: + return None + if 'devices' in changed: + if any(d.get('kind', 'character-device') not in ('character-device', 'block-device') + or d.get('append_host_device_gid_to_environment') + for plan in (before, after) for d in plan.get('devices', [])): + return None + if before.get('device_permissions') or after.get('device_permissions'): + return None + if 'mounts' in changed: + if any(transaction.is_file_bind(m) for plan in (before, after) for m in plan.get('mounts', [])): + return None + if transaction.nested_mounts.without(config) != config: + # The installer also regenerates the recursive bind entries and + # hooks associated with these mounts. + return None + old_mounts = {m['container_path']: m for m in before.get('mounts', [])} + for mount in after.get('mounts', []): + original = old_mounts.get(mount['container_path']) + # Permission grants and installer-specific mount attributes need + # their normal installer, including when only their source changes. + if mount.get('grant_access'): + return None + if original: + fields = {k for k in original.keys() | mount.keys() if original.get(k) != mount.get(k)} + native_fields = {'read_only', 'backup', 'create_if_missing'} + if original['type'] == mount['type'] == 'host-bind': + native_fields.add('source') + if fields - native_fields: + return None + return changed + + +def plan(record, candidate, config): + changed = changes(record, candidate, config) + if not changed: + return None + transaction.preflight(record, candidate, config, in_place=True) + old, new = record['deployment'], candidate['deployment'] + cfg = parse_config(config) + values, delete = {}, [] + if 'resources' in changed: + for key, native, minimum in (('cores', 'cpulimit' if 'cpulimit' in cfg and 'cores' not in cfg else 'cores', 1), + ('memory_mb', 'memory', 128), ('swap_mb', 'swap', 0), ('cpu_units', 'cpuunits', 1)): + value = new['resources'].get(key) + if old['resources'].get(key) == value: + continue + if value is None and key == 'cpu_units': + if native in cfg: delete.append(native) + elif isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ValueError(translate('Invalid resource value')) + else: + values[native] = str(value) + if 'network' in changed: + network = new['network'] + if set(network) - {'bridge', 'ipv4', 'gateway', 'firewall', 'host_managed', 'mac_address'}: + raise ValueError(translate('Unsupported network settings')) + if not re.fullmatch(r'[A-Za-z0-9_.:-]+', network['bridge']): + raise ValueError(translate('Invalid bridge')) + if not Path('/sys/class/net', network['bridge'], 'bridge').is_dir(): + raise ValueError(translate('The selected bridge does not exist')) + address = network['ipv4'] + if address not in ('dhcp', 'manual'): + if ipaddress.ip_interface(address).version != 4: + raise ValueError(translate('Invalid IPv4 address')) + gateway = network.get('gateway') + if gateway and ipaddress.ip_address(gateway).version != 4: + raise ValueError(translate('Invalid IPv4 gateway')) + fields = dict(p.split('=', 1) for p in cfg['net0'].split(',') if '=' in p) + fields.update(bridge=network['bridge'], ip=address) + if gateway: fields['gw'] = gateway + else: fields.pop('gw', None) + for key, native in (('firewall', 'firewall'), ('host_managed', 'host-managed')): + if old['network'].get(key) != network.get(key): + fields[native] = '1' if network.get(key) else '0' + if old['network'].get('mac_address') != network.get('mac_address'): + raise ValueError(translate('The MAC address of the container cannot be kept')) + values['net0'] = ','.join(k+'='+v for k, v in fields.items()) + if 'environment' in changed: + env = environment(config) + previous = environment_values(old.get('environment', [])) + wanted = environment_values(new.get('environment', [])) + defaults = dict(item.split('=', 1) for item in record['observed']['image'].get('defaults', {}).get('Env', []) if '=' in item) + for name in previous.keys() - wanted.keys(): + if name in defaults: env[name] = defaults[name] + else: env.pop(name, None) + env.update(wanted) + if env: values['env'] = '\0'.join(k+'='+v for k, v in env.items()) + else: delete.append('env') + if 'devices' in changed: + inventory = devices.planned(new) + existing = {v['path']: k for k, v in ((k, dict(p.split('=', 1) for p in v.split(','))) + for k, v in cfg.items() if re.fullmatch(r'dev[0-9]+', k))} + used = set(existing.values()) + for path, key in existing.items(): + if path not in inventory: delete.append(key) + for device in new.get('devices', []): + path = device['host_path'] + key = existing.get(path) + if not key: + key = next('dev'+str(n) for n in range(256) if 'dev'+str(n) not in used) + used.add(key) + info = inventory[path] + mode = info['mode'] if device.get('mode') == 'preserve-host' else int(device.get('mode', '0660'), 8) + values[key] = (f"path={path},mode={mode:04o},uid={int(device.get('uid', 0))}," + f"gid={info['gid'] if device['gid_strategy'] == 'host-device-gid' else 0}," + f"deny-write={1 if device.get('deny_write') else 0}") + if 'mounts' in changed: + actual = transaction.mounts(config) + used = {m['key'] for m in actual.values()} + wanted = {m['container_path']: m for m in new.get('mounts', [])} + for target, mount in actual.items(): + if target not in wanted: delete.append(mount['key']) + for target, mount in wanted.items(): + if target in actual: + original = next(m for m in old['mounts'] if m['container_path'] == target) + if mount == original: continue + fields = dict(p.split('=', 1) for p in actual[target]['value'].split(',')[1:] if '=' in p) + source = actual[target]['volume'] + if mount['type'] == 'host-bind': + source = host_mounts.valid_path(mount['source']) + if not Path(source).is_dir() and not mount.get('create_if_missing'): + raise ValueError(translate('The host directory does not exist')) + fields.update(mp=target, backup='1' if mount['backup'] else '0', + ro='1' if mount.get('read_only') else '0') + values[actual[target]['key']] = source+','+','.join(k+'='+v for k, v in fields.items()) + continue + key = next('mp'+str(n) for n in range(256) if 'mp'+str(n) not in used) + used.add(key) + source = mount['source'] + if mount['type'] == 'managed-volume': + if not re.fullmatch(r'[A-Za-z0-9_-]+', source): + raise ValueError(translate('Invalid storage')) + source += ':'+str(mount['size_gb']) + else: + source = host_mounts.valid_path(source) + if not Path(source).is_dir(): + if not mount.get('create_if_missing'): + raise ValueError(translate('The host directory does not exist')) + values[key] = (f"{source},mp={target},backup={1 if mount['backup'] else 0}," + f"ro={1 if mount.get('read_only') else 0}") + return {'values': values, 'delete': delete, 'restart': bool(changed - {'resources'}), + 'changed': sorted(changed)} + + +def verify_values(config, operation): + """PVE may canonicalize property strings; compare their actual fields.""" + cfg = parse_config(config) + for key, value in operation['values'].items(): + if key == 'env': + if environment(config) != dict(item.split('=', 1) for item in value.split('\0')): + raise ValueError(translate('Proxmox could not apply the container settings')) + elif re.fullmatch(r'(mp|dev|net)[0-9]+', key): + if re.fullmatch(r'mp[0-9]+', key): + source = value.split(',', 1)[0] + actual_source = cfg.get(key, '').split(',', 1)[0] + allocating = re.fullmatch(r'[A-Za-z0-9_-]+:[0-9]+', source) + if (not allocating and actual_source != source) or (allocating and not actual_source.startswith(source.split(':')[0]+':')): + raise ValueError(translate('Proxmox could not apply the container settings')) + desired = dict(p.split('=', 1) for p in value.split(',') if '=' in p) + actual = dict(p.split('=', 1) for p in cfg.get(key, '').split(',') if '=' in p) + defaults = {'uid': '0', 'gid': '0', 'deny-write': '0', 'ro': '0', 'backup': '0', 'mode': '0660', 'firewall': '0'} + def matches(k, v): + observed = actual.get(k, defaults.get(k)) + if k == 'mode': return observed is not None and int(observed, 8) == int(v, 8) + return observed == v + if any(not matches(k, v) for k, v in desired.items()): + raise ValueError(translate('Proxmox could not apply the container settings')) + elif cfg.get(key) != value: + raise ValueError(translate('Proxmox could not apply the container settings')) + if any(key in cfg or (key == 'env' and environment(config)) for key in operation['delete']): + raise ValueError(translate('Proxmox could not apply the container settings')) + + +def set_config(vmid, installation_id, expected, values, delete=()): + raw = (resources.CONFIG_DIR / f'{vmid}.conf').read_bytes() + if instances.command('pct', 'config', str(vmid)) != expected or instances.identity(expected) != installation_id: + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + payload = dict(values, vmid=vmid, digest=hashlib.sha1(raw).hexdigest()) + if delete: payload['delete'] = ','.join(delete) + transaction.log('Applying native settings: '+', '.join(sorted(set(values) | set(delete)))) + process = subprocess.run(['perl', '-e', PERL_UPDATE], input=json.dumps(payload).encode(), + capture_output=True, timeout=120) + # API errors may include secret values. Only keep the exit status. + if process.returncode: + raise RuntimeError(translate('Proxmox could not apply the container settings')) + + +def untouched(before, after, keys): + keys = set(keys) + if 'env' in keys: keys.add('lxc.environment.runtime') + def keep(config): + return b''.join(line for line in config.splitlines(keepends=True) + if line.split(b':', 1)[0].decode() not in keys + and not re.match(rb'unused[0-9]+:', line)) + return keep(before) == keep(after) + + +def restore_settings(root, record, before, now, operation): + vmid = record['vmid'] + keys = set(operation['values']) | set(operation['delete']) + if operation['restart']: transaction.stop(vmid) + original = parse_config(before) + restore = {k: original[k] for k in keys if k in original and k != 'env'} + remove = [k for k in keys if k not in original and k != 'env' and k in parse_config(now)] + if 'env' in keys: + env = environment(before) + if env: restore['env'] = '\0'.join(k+'='+v for k, v in env.items()) + elif environment(now): remove.append('env') + if restore or remove: + set_config(vmid, record['installation_id'], now, restore, remove) + restored_config = instances.command('pct', 'config', str(vmid)) + verify_values(restored_config, {'values': restore, 'delete': remove}) + if not untouched(before, restored_config, keys): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + restored = copy.deepcopy(record) + obs = record['observed'] + restored['observed'] = instances.observe(vmid, record['installation_id'], + obs['archive_path'], obs['resolved_registry_digest'], obs['image']) + if restored['observed']['config_sha256'] != sha(restored_config): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + return restored + + +def recover(root, journal): + """Recover through the normal Manage interrupted-operation menu.""" + state = json.loads(journal.read_text()) + record, operation = state['record'], state['operation_plan'] + vmid = record['vmid'] + current = instances.read(root, vmid) + if (state['kind'] != 'native-modify' or state['phase'] in ('committed', 'rolled-back') + or current.get('pending_transaction') != str(journal) + or current.get('transaction_id') != state['id'] + or current['installation_id'] != record['installation_id']): + raise ValueError(translate('The record does not belong to this operation')) + before = state['config'].encode() + now = instances.command('pct', 'config', str(vmid)) + keys = set(operation['values']) | set(operation['delete']) + if instances.identity(now) != record['installation_id'] or not untouched(before, now, keys): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + if state.get('after_config'): + if now not in (before, state['after_config'].encode()): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + elif now != before: + # A crash during the PVE API call is recoverable only when the full + # planned edit is present. Refuse unknown partial/concurrent changes. + verify_values(now, operation) + host_mounts.verify_sources(state.get('original_host_sources', {})) + tx_record = restore_settings(root, record, before, now, operation) if now != before else copy.deepcopy(record) + if state['was_running']: + if transaction.run('pct', 'status', str(vmid)).strip() != b'status: running': + transaction.run('pct', 'start', str(vmid)) + transaction.healthcheck(vmid, record['template']) + instances.write(instances.location(root, vmid), tx_record) + state['phase'] = 'rolled-back' + instances.write(journal, state) + msg_ok(translate('Previous installation restored')) + + +def apply(root, record, candidate, config, acknowledge_external_data=False): + operation = plan(record, candidate, config) + if not operation: + raise ValueError(translate('The changes need the application installer')) + original_sources, desired_sources = transaction.freeze_host_sources( + record, candidate, acknowledge_external_data) + vmid = record['vmid'] + folder = instances.location(root, vmid).parent / 'modifications' / uuid.uuid4().hex + running = transaction.run('pct', 'status', str(vmid)).strip() == b'status: running' + journal = {'kind': 'native-modify', 'id': folder.name, 'vmid': vmid, 'operation': 'recreate', + 'record': record, 'config': config.decode(), 'operation_plan': operation, + 'phase': 'prepared', 'was_running': running, 'original_host_sources': original_sources} + instances.write(folder / 'before.json', journal) + transaction.open_log(folder) + transaction._run['journal'] = folder / 'before.json' + pending = copy.deepcopy(record) + pending.update(status='updating', transaction_id=folder.name, pending_transaction=str(folder / 'before.json')) + instances.write(instances.location(root, vmid), pending) + updated_config = None + changed = False + try: + if operation['restart'] and running: + transaction.stop(vmid) + # Do not accept a concurrent edit merely because the service stopped. + if instances.command('pct', 'config', str(vmid)) != config: + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + host_mounts.verify_sources(desired_sources) + for mount in candidate['deployment'].get('mounts', []): + if mount['type'] == 'host-bind' and mount.get('create_if_missing'): + Path(mount['source']).mkdir(parents=True, exist_ok=True) + host_mounts.verify_sources(original_sources) + # Capture newly-created directories before installing their mounts. + pinned = {p: host_mounts.validate_source(p) for p in desired_sources} + for path, previous in desired_sources.items(): + if previous['exists'] and not host_mounts.same_source(previous, pinned[path]): + raise ValueError(translate('A shared source does not match its recorded identity')) + host_mounts.verify_sources(pinned) + msg_info(translate('Applying container settings without rebuilding the container...')) + if operation['values'] or operation['delete']: + changed = True # API updates may partially succeed before an error. + set_config(vmid, record['installation_id'], config, operation['values'], operation['delete']) + updated_config = instances.command('pct', 'config', str(vmid)) + verify_values(updated_config, operation) + keys = set(operation['values']) | set(operation['delete']) + if not untouched(config, updated_config, keys): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + transaction.preflight(candidate, candidate, updated_config, in_place=True, baseline=False) + journal.update(phase='configured', after_config=updated_config.decode()) + instances.write(folder / 'before.json', journal) + if operation['restart'] and running: + transaction.run('pct', 'start', str(vmid)) + transaction.healthcheck(vmid, candidate['template']) + observed = record['observed'] + fresh = instances.observe(vmid, record['installation_id'], observed['archive_path'], + observed['resolved_registry_digest'], observed['image']) + host_mounts.verify_observation(pinned, fresh) + if fresh['config_sha256'] != sha(updated_config): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + result = copy.deepcopy(record) + result.update(deployment=copy.deepcopy(candidate['deployment']), observed=fresh) + instances.write(instances.location(root, vmid), result) + journal['phase'] = 'committed' + instances.write(folder / 'before.json', journal) + except BaseException: + try: + now = instances.command('pct', 'config', str(vmid)) + keys = set(operation['values']) | set(operation['delete']) + if changed: + if not untouched(config, now, keys) or (updated_config is not None and now != updated_config): + raise ValueError('Concurrent edit; rollback refused') + restored = restore_settings(root, record, config, now, operation) + else: + restored = copy.deepcopy(record) + if operation['restart'] and running: + if transaction.run('pct', 'status', str(vmid)).strip() != b'status: running': + transaction.run('pct', 'start', str(vmid)) + transaction.healthcheck(vmid, record['template']) + instances.write(instances.location(root, vmid), restored) + journal['phase'] = 'rolled-back' + instances.write(folder / 'before.json', journal) + except Exception as error: + transaction.log('native rollback failed: '+type(error).__name__) + msg_warn(translate('The settings could not be fully rolled back. Review the container and its saved modification before continuing.')) + raise + try: + import oci_carried_record + oci_carried_record.carry(root, vmid, mount_stopped=False) + except (OSError, ValueError, RuntimeError): + transaction.log('carried record will be refreshed on the next management entry') + msg_ok(translate('Changes applied. Data kept.')) + return folder diff --git a/oci/remote/oci_resource_modify.py b/oci/remote/oci_resource_modify.py new file mode 100644 index 00000000..9d4083cf --- /dev/null +++ b/oci/remote/oci_resource_modify.py @@ -0,0 +1,118 @@ +"""Apply CPU/RAM-only proposals to the existing guest, without an image archive.""" +import copy +import hashlib +from pathlib import Path +import uuid + +import oci_instances as instances +import oci_instance_transaction as transaction +from oci_installation_state import parse_config, sha +from oci_ui import translate, msg_info, msg_ok, msg_warn + +CONFIG_DIR = Path('/etc/pve/lxc') + + +def changes(record, candidate, config): + """Return a narrow native edit, or None when the normal installer is needed.""" + if (any(key in record for key in ('stack', 'stack_member', 'native_stack_intent')) + or record['deployment'].get('stack_managed') + or instances.identity(config) != record['installation_id']): + return None + previous = transaction.with_default_healthcheck(copy.deepcopy(record)) + candidate = transaction.with_default_healthcheck(copy.deepcopy(candidate)) + old, new = previous['deployment'], candidate['deployment'] + if previous['template'] != candidate['template'] or old.get('host_monitor'): + return None + if {k: v for k, v in old.items() if k != 'resources'} != { + k: v for k, v in new.items() if k != 'resources'}: + return None + original, desired = old.get('resources', {}), new.get('resources', {}) + changed = {key for key in original.keys() | desired.keys() if original.get(key) != desired.get(key)} + if not changed or changed - {'cores', 'memory_mb'}: + return None + cfg = parse_config(config) + cpu = 'cpulimit' if 'cpulimit' in cfg and 'cores' not in cfg else 'cores' + parameters = {} + for key in sorted(changed): + value = desired.get(key) + if isinstance(value, bool) or not isinstance(value, int) or value < (128 if key == 'memory_mb' else 1): + return None + native = 'memory' if key == 'memory_mb' else cpu + if native not in cfg: + return None + parameters[native] = str(value) + return parameters + + +def guarded_set(vmid, installation_id, expected, parameters): + # pct uses this SHA1 under its own configuration lock. Sample the native + # file before checking pct's view, so a concurrent edit cannot be accepted + # accidentally by taking a fresh digest after validation. + native = (CONFIG_DIR / f'{vmid}.conf').read_bytes() + current = instances.command('pct', 'config', str(vmid)) + if current != expected or instances.identity(current) != installation_id: + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + command = ['pct', 'set', str(vmid), '--digest', hashlib.sha1(native).hexdigest()] + for key, value in sorted(parameters.items()): + command += ['--' + key, value] + transaction.run(*command) + + +def expected_config(before, after, parameters): + def unrelated(config): + return b''.join(line for line in config.splitlines(keepends=True) + if line.split(b':', 1)[0].decode() not in parameters) + new = parse_config(after) + return unrelated(before) == unrelated(after) and all(new.get(key) == value for key, value in parameters.items()) + + +def apply(root, record, candidate, config, parameters): + """The caller holds the instance lock and has checked image identity/preflight.""" + if changes(record, candidate, config) != parameters: + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + if transaction.external_changes(record, config): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + vmid = record['vmid'] + directory = instances.location(root, vmid).parent / 'modifications' / uuid.uuid4().hex + instances.write(directory / 'before.json', {'record': record, 'config': config.decode()}) + transaction.open_log(directory) + msg_info(translate('Applying CPU and RAM changes without rebuilding the container...')) + applied = False + after = None + try: + guarded_set(vmid, record['installation_id'], config, parameters) + applied = True + after = instances.command('pct', 'config', str(vmid)) + if not expected_config(config, after, parameters): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + observed = record['observed'] + fresh = instances.observe(vmid, record['installation_id'], observed['archive_path'], + observed['resolved_registry_digest'], observed['image']) + if fresh['config_sha256'] != sha(after): + raise ValueError(translate('The instance changed while it was being edited; open Modify again')) + updated = copy.deepcopy(record) + updated.update(deployment=copy.deepcopy(candidate['deployment']), observed=fresh) + instances.write(instances.location(root, vmid), updated) + except Exception: + if applied: + try: + current = instances.command('pct', 'config', str(vmid)) + if not expected_config(config, current, parameters): + raise ValueError('Concurrent configuration change; resource rollback refused') + previous = parse_config(config) + guarded_set(vmid, record['installation_id'], current, + {key: previous[key] for key in parameters}) + instances.write(instances.location(root, vmid), record) + except Exception as error: + transaction.log(f'resource rollback: {type(error).__name__}') + msg_warn(translate('The resource edit could not be fully rolled back. Open Manage again to review and import the current Proxmox settings.')) + raise + # Updating the carried record is best effort here, as it is after other + # management operations. The normal menu also refreshes it on return. + try: + import oci_carried_record + oci_carried_record.carry(root, vmid, mount_stopped=False) + except (OSError, ValueError, RuntimeError): + transaction.log('carried record will be refreshed on the next management entry') + msg_ok(translate('Changes applied. Data kept.')) + return directory diff --git a/oci/remote/oci_runtime.py b/oci/remote/oci_runtime.py index ee3b9d56..3d842fd3 100644 --- a/oci/remote/oci_runtime.py +++ b/oci/remote/oci_runtime.py @@ -4,7 +4,9 @@ from __future__ import annotations import json +import os import shlex +import stat import sys import tarfile from pathlib import Path @@ -90,14 +92,39 @@ def effective_entrypoint( def home_directory(passwd_path: Path, uid: int) -> str | None: - """Return the safe home directory recorded for a container UID.""" - for line in passwd_path.read_text(encoding="utf-8").splitlines(): + """Read a regular image passwd file; only an absent UID defaults to /.""" + if uid < 0: + return None + # The caller supplies /etc/passwd. Pin etc so an image + # symlink or a rename during lookup cannot redirect the host-side read. + directory = os.open(passwd_path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + if not stat.S_ISREG(os.stat(passwd_path.name, dir_fd=directory, follow_symlinks=False).st_mode): + return None + descriptor = os.open( + passwd_path.name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory, + ) + try: + # Recheck the opened object, not its name; a FIFO replacement + # must neither block open() nor reach a read operation. + if not stat.S_ISREG(os.fstat(descriptor).st_mode): + return None + with os.fdopen(descriptor, encoding="utf-8", closefd=False) as stream: + content = stream.read() + finally: + os.close(descriptor) + finally: + os.close(directory) + for line in content.splitlines(): fields = line.split(":") - if len(fields) < 7 or fields[2] != str(uid): + if len(fields) < 3 or fields[2] != str(uid): continue + if len(fields) != 7: + return None home = fields[5] - return home if home.startswith("/") else None - return None + safe = home.startswith("/") and not any(ord(char) < 32 or ord(char) == 127 for char in home) + return home if safe else None + return "/" def main() -> int: diff --git a/oci/remote/oci_stack_native.py b/oci/remote/oci_stack_native.py index b679ee74..2b564205 100644 --- a/oci/remote/oci_stack_native.py +++ b/oci/remote/oci_stack_native.py @@ -192,7 +192,7 @@ class NativeAdapter: config = instances.command('pct', 'config', str(vmid)) if instances.identity(config) != record['installation_id']: raise ValueError(translate('The identity of a member was replaced')) - if (not self.journal.exists() or not self.state().get('replacement_intent')) and not instances.same_config_except_notes(record, config): + if (not self.journal.exists() or not self.state().get('replacement_intent')) and not instances.same_config_except_notes(record, config, ignore_tags=True): raise ValueError(translate('A member configuration changed during the preparation')) else: if Path('/etc/pve/lxc/%s.conf' % vmid).exists(): diff --git a/oci/remote/oci_update_current.py b/oci/remote/oci_update_current.py index bba91c04..f4f94038 100644 --- a/oci/remote/oci_update_current.py +++ b/oci/remote/oci_update_current.py @@ -8,6 +8,7 @@ import re import shlex import subprocess import sys +import tarfile import tempfile import oci_instances as instances @@ -56,7 +57,7 @@ def run_quiet(args, error, capture=False): process.wait() -def resolve_archive(desired, config, current=None, check=None, required_digest=None): +def resolve_archive(desired, config, current=None, check=None, required_digest=None, local_archives=()): # Shared by individual and coordinated operations; no guest mutation here. # When the registry still serves the current digest nothing is downloaded. reference = desired['template']['container_contract']['image']['reference'] @@ -83,6 +84,27 @@ def resolve_archive(desired, config, current=None, check=None, required_digest=N return None, digest if check: check() + verifier = Path(__file__).with_name('verify_oci_archive.py') + + def intact(path): + try: + run_quiet([sys.executable, str(verifier), str(path)], + translate('The image did not pass the integrity check')) + return same_image(candidate, image_from_archive(str(path))) + except (RuntimeError, OSError, ValueError, KeyError, tarfile.TarError): + return False + + # Initial installations and updates use different archive names. The + # instance record knows the actual archive; validate it before looking + # only for the deterministic update-cache name. Never delete a rejected + # recorded path: another instance may still refer to it. + for saved in local_archives: + path = Path(saved) if saved else None + if path and not path.is_symlink() and path.is_file(): + msg_info(translate('Verifying the image integrity...')) + if intact(path): + msg_ok(translate('Using the verified image from the cache')) + return path, digest storage = desired['deployment']['template_storage'] if not re.fullmatch(r'[A-Za-z0-9_-]+', storage): raise ValueError(translate('Invalid template storage')) @@ -91,16 +113,6 @@ def resolve_archive(desired, config, current=None, check=None, required_digest=N archive.parent.mkdir(parents=True, exist_ok=True) if archive.is_symlink(): raise ValueError(translate('Unsafe OCI archive path')) - verifier = Path(__file__).with_name('verify_oci_archive.py') - - def intact(path): - try: - run_quiet([sys.executable, str(verifier), str(path)], - translate('The image did not pass the integrity check')) - except RuntimeError: - return False - return same_image(candidate, image_from_archive(str(path))) - if archive.exists(): msg_info(translate('Verifying the image integrity...')) if intact(archive): @@ -168,6 +180,16 @@ def application_name(record, vmid): return f'CT {vmid}' +def backup_native_settings(vmid, storage): + """An optional retained backup does not require rebuilding the guest.""" + if storage: + import oci_keep_backup + oci_keep_backup.validate(storage) + transaction.run('vzdump', str(vmid), '--storage', storage, '--mode', 'snapshot', + '--compress', 'zstd', '--notes-template', + 'ProxMenux OCI: before modifying container settings') + + def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=None): operation = 'recreate' if proposal is not None else 'update' msg_info(translate('Checking the container before the update...') if operation == 'update' @@ -179,11 +201,42 @@ def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=Non config = instances.command('pct', 'config', str(vmid)) desired = transaction.candidate_contract(record, operation, proposal) changes = transaction.external_changes(record, config) + resource_changes = None + if operation == 'recreate' and not changes: + import oci_resource_modify + resource_changes = oci_resource_modify.changes(record, desired, config) + # CPU/RAM edits retain the installed image and all unrelated native + # settings. They do not implicitly update the application or depend + # on the registry and the narrower recreation profile. + if resource_changes: + msg_ok(translate('Container checked')) + import oci_operation_notice + with oci_operation_notice.operation([vmid], 'modify', application_name(record, vmid)): + backup_native_settings(vmid, keep_backup) + return oci_resource_modify.apply(instances.ROOT, record, desired, config, resource_changes) + if operation == 'recreate' and not changes: + import oci_native_modify + if oci_native_modify.changes(record, desired, config): + import oci_operation_notice + with oci_operation_notice.operation([vmid], 'modify', application_name(record, vmid)): + # Validate the native plan before an optional full backup. + oci_native_modify.plan(record, desired, config) + backup_native_settings(vmid, keep_backup) + return oci_native_modify.apply(instances.ROOT, record, desired, config, + acknowledge_external_data=acknowledge_external_data) + # An unsupported native edit still uses the existing installer. Decide + # the route before changing anything; actual validation/API failures + # must not silently switch to a destructive fallback. transaction.preflight(record, desired, config) msg_ok(translate('Container checked')) current = record['observed']['image'] if operation == 'update' else None - archive, digest = resolve_archive(desired, config, current, lambda: work_backup.locate( - instances.location(instances.ROOT, vmid).parent / 'transactions' / 'check', [vmid], record, vmid)) + check = lambda: work_backup.locate( + instances.location(instances.ROOT, vmid).parent / 'transactions' / 'check', [vmid], record, vmid) + if operation == 'recreate': + archive, digest = resolve_archive(desired, config, current, check, + local_archives=[record['observed'].get('archive_path')]) + else: + archive, digest = resolve_archive(desired, config, current, check) if archive is None: msg_ok(translate('The image is already up to date; nothing was changed.')) return @@ -207,6 +260,9 @@ def update(vmid, acknowledge_external_data=False, proposal=None, keep_backup=Non transaction.apply(instances.ROOT, vmid, archive, operation, proposal=proposal, registry_digest=digest, acknowledge_external_data=acknowledge_external_data, keep_backup=file_storage) + marker = os.environ.get('OCI_RECREATED_MARKER') + if marker: + Path(marker).touch() def main(): diff --git a/oci/remote/oci_work_backup.py b/oci/remote/oci_work_backup.py index b37a2c26..251ffe71 100644 --- a/oci/remote/oci_work_backup.py +++ b/oci/remote/oci_work_backup.py @@ -192,21 +192,25 @@ def application(root, vmid): return primary_id, members or [vmid] -def status(root, vmid): +def status(root, vmid, only_needed=False): """What the menu needs to know before an operation: whether the backup fits on the system disk, the storage chosen earlier and the ones that - have room.""" + have room. The menu can skip unused alternatives once a destination fits; + the default retains the complete status used by the maintenance CLI.""" primary_id, vmids = application(root, vmid) record = instances.read(root, primary_id) default = instances.location(root, primary_id).parent needed, free = required(vmids), available(default) saved = (record.get('deployment') or {}).get(KEY) - path = storage_path(saved) if saved else None + fits = free >= needed + path = storage_path(saved) if saved and not (only_needed and fits) else None saved_free = available(path) if path is not None and path.is_dir() else None - return {'needed': needed, 'free': free, 'fits': free >= needed, 'saved': saved, - 'saved_fits': saved_free is not None and saved_free >= needed, + saved_fits = saved_free is not None and saved_free >= needed + candidates = [] if only_needed and (fits or saved_fits) else file_storages() + return {'needed': needed, 'free': free, 'fits': fits, 'saved': saved, + 'saved_fits': saved_fits, 'candidates': [{'storage': item['storage'], 'free': item['free']} - for item in file_storages() if item['free'] >= needed]} + for item in candidates if item['free'] >= needed]} def choose(root, vmid, storage): diff --git a/oci/src/proxmenux_oci/cli.py b/oci/src/proxmenux_oci/cli.py index 9266f283..14dda6e2 100644 --- a/oci/src/proxmenux_oci/cli.py +++ b/oci/src/proxmenux_oci/cli.py @@ -578,8 +578,6 @@ def interactive(catalog: Catalog) -> int: f"{translate('All applications')} ({len(applications)})") elif selection == "manage": from .management import interactive_management - console.show_logo() - console.msg_title(translate("Manage installed OCI applications")) interactive_management(PROJECT_ROOT, ui) elif selection == "registries": from .registry_accounts import open_registry_accounts diff --git a/oci/src/proxmenux_oci/installer.py b/oci/src/proxmenux_oci/installer.py index a10f92c5..f8e88d43 100644 --- a/oci/src/proxmenux_oci/installer.py +++ b/oci/src/proxmenux_oci/installer.py @@ -1588,7 +1588,7 @@ def _run_remote_install( shutil.copy2(project_root / 'remote' / 'haos_healthcheck.py', temporary / 'haos_healthcheck.py') shutil.copy2(project_root / 'remote' / 'oci_installation_state.py', temporary / 'oci_installation_state.py') shutil.copy2(project_root / 'remote' / 'oci_instances.py', temporary / 'oci_instances.py') - for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'): + for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_resource_modify.py', 'oci_native_modify.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'): shutil.copy2(project_root / 'remote' / helper, temporary / helper) if deployment_kind == 'generic-multi-lxc-stack': shutil.copy2(project_root / 'remote' / 'install_generic_stack.py', temporary / 'install_generic_stack.py') @@ -1625,7 +1625,7 @@ def _run_remote_install( archive.add(project_root / 'remote' / 'haos_healthcheck.py', arcname='haos_healthcheck.py') archive.add(project_root / 'remote' / 'oci_installation_state.py', arcname='oci_installation_state.py') archive.add(project_root / 'remote' / 'oci_instances.py', arcname='oci_instances.py') - for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'): + for helper in ('oci_ui.sh', 'oci_ui.py', 'oci_description.py', 'oci_console.py', 'oci_native_stack.py', 'oci_native_stack.sh', 'oci_instance_transaction.py', 'oci_host_mounts.py', 'oci_nested_mounts.py', 'oci_runtime_settings.py', 'oci_gpu_devices.py', 'oci_accelerators.py', 'oci_nvidia_runtime.py', 'oci_nvidia_refresh.py', 'oci_nvidia_dynamic.py', 'oci_update_current.py', 'oci_resource_modify.py', 'oci_native_modify.py', 'oci_stack_replay.py', 'oci_stack_plan.py', 'oci_stack_transaction.py', 'oci_stack_native.py', 'oci_image_cache.py', 'nvidia_lxc_mount_lab.sh', 'oci_nvidia_setup.sh', 'oci_immich_ml.sh', 'oci_rocm_check.py'): archive.add(project_root / 'remote' / helper, arcname=helper) if deployment_kind == 'generic-multi-lxc-stack': archive.add(project_root / 'remote' / 'install_generic_stack.py', arcname='install_generic_stack.py') diff --git a/oci/src/proxmenux_oci/management.py b/oci/src/proxmenux_oci/management.py index cbf8e041..2744032d 100644 --- a/oci/src/proxmenux_oci/management.py +++ b/oci/src/proxmenux_oci/management.py @@ -106,13 +106,32 @@ def _config_hostname(vmid): return '' +def _selected_configuration(vmid): + """Reuse pct's output only while its complete native source is unchanged. + + The forced carried-record verification has just fetched a fresh config. + Older installations without that source-keyed cache still query pct. + The lifecycle backend independently validates again before any mutation. + """ + import oci_instances as instances + try: + import oci_carried_record + reader = getattr(oci_carried_record, 'configuration', None) + config = reader(instances.ROOT, vmid) if reader else None + if isinstance(config, str) and config: + return config.encode() + except (ImportError, OSError, ValueError, RuntimeError, subprocess.SubprocessError): + pass + return instances.command('pct', 'config', str(vmid)) + + def check_selected(project, row): """Validate only the chosen container; lifecycle backends validate its stack.""" sys.path.insert(0, str(project / 'remote')) import oci_instances as instances with instances.locked(instances.ROOT): record = instances.read(instances.ROOT, row['vmid']) - config = instances.command('pct', 'config', str(row['vmid'])) + config = _selected_configuration(row['vmid']) marker = instances.identity(config) reason = 'matched' if marker == record['installation_id'] else 'identity-unconfirmed' return public_row(record, {'reason': reason}) @@ -132,7 +151,7 @@ def _run_lifecycle(command, title): return completed.returncode == 0 -def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=False): +def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=False, heading=None): """Leave inside each container the copy of its record that a restore on another host needs. It is refreshed after every operation. Returns what was found for each container. Every installed application is looked at, @@ -140,6 +159,9 @@ def carry_records(project, mount_stopped=True, vmids=None, verify=False, shown=F from . import console sys.path.insert(0, str(project / 'remote')) if shown: + if heading: + console.show_logo() + console.msg_title(heading) console.msg_info(translate('Updating the copy of the record...')) try: import oci_carried_record @@ -234,6 +256,17 @@ def interactive_management(project, ui): ui.message(translate('OCI management could not be completed. Check the backend status; no additional cleanup has been authorized.'), translate('OCI management')) +def _records_unread(project): + """Whether an installed application has no cached configuration yet; only + then does the first sync read every container and take a while.""" + sys.path.insert(0, str(project / 'remote')) + import oci_carried_record as carried + return any(not (carried.instances.location(carried.instances.ROOT, row['vmid']).parent + / carried.CONFIG_CACHE).exists() + for row in saved_inventory(project) + if row.get('status') == 'installed' and not row.get('pending')) + + def _clean_orphans(project): """Remove, silently, what is left of containers that exist on no node of the cluster.""" sys.path.insert(0, str(project / 'remote')) @@ -253,7 +286,8 @@ def _interactive_management(project, ui): _clean_orphans(project) recover_automatically(project) offer_recovery(project, ui) - carry_records(project, mount_stopped=False, shown=True) + carry_records(project, mount_stopped=False, shown=_records_unread(project), + heading=translate('Manage installed OCI applications')) # After each operation the list is shown again. Every operation leaves # the copies of the records up to date, so they are not all refreshed again. first = True @@ -313,7 +347,7 @@ def _work_backup_ready(project, ui, vmid): import oci_instances as instances import oci_work_backup try: - state = oci_work_backup.status(instances.ROOT, vmid) + state = oci_work_backup.status(instances.ROOT, vmid, only_needed=True) except (OSError, ValueError, KeyError, RuntimeError, subprocess.SubprocessError): # What cannot be measured here is told by the engine when it stops. return True @@ -397,23 +431,22 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()): return _toggle_watchdog(project, ui, row['vmid'], watched) if action == 'remove': return _remove(project, ui, row['vmid']) - if not _work_backup_ready(project, ui, row['vmid']): - return False record = instances.read(instances.ROOT, row['vmid']) import oci_instance_reconcile as reconcile try: - current_config = instances.command('pct', 'config', str(row['vmid'])) + current_config = _selected_configuration(row['vmid']) adoption = reconcile.propose(record, current_config) except (OSError, ValueError, RuntimeError) as error: ui.message(str(error), translate('Review external OCI changes')) return False if adoption: summary = '\n'.join(adoption['details']) - if not ui.review( - f"{translate('These Proxmox resources were added outside ProxMenux:')}\n\n{summary}\n\n" - + translate('They will be added to oci-compose before continuing. Unsupported or changed resources are not imported.'), + # Variables reordered in Proxmox change nothing that needs a decision. + if summary and not ui.review( + f"{translate('These changes were made in Proxmox outside ProxMenux:')}\n\n{summary}\n\n" + + translate('They will be saved in oci-compose before continuing.'), translate('Review external OCI changes'), - question=translate('Include these resources in oci-compose?'), default=False): + question=translate('Include these changes in oci-compose?'), default=False): return False try: record = reconcile.commit(instances.ROOT, row['vmid'], adoption) @@ -423,29 +456,16 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()): proposal = None if action == 'modify': from .recreation import edit_recreation - from .cli import _deployment_summary_text - from .ui import BacktrackUI, RestartWizard - wizard = BacktrackUI(ui) - try: - while True: - try: - proposal = edit_recreation(record, wizard) - approved = wizard.review(_deployment_summary_text(proposal['candidate']['template'], - proposal['candidate']['deployment']), translate('Modify OCI'), - question=translate('Apply these changes?'), default=True) - break - except RestartWizard: - wizard.restart() - except ValueError as error: - if not wizard.retry_last(error): - raise - finally: - wizard.close() - if not approved: + proposal = edit_recreation(record, ui) + if proposal is None: return False elif not ui.review(translate('If the image channel has a new version, the CT is stopped, backed up and verified, and replaced with the new image to update the container. If the update fails after the container was changed, the backup is restored. An update that stops halfway is recovered from the OCI management menu.'), translate('Update OCI'), question=translate('Update now?'), default=True): return False + # Opening/cancelling a confirmation needs no disk or storage probes. + # Both accepted operations retain this check before the backend starts. + if not _work_backup_ready(project, ui, row['vmid']): + return False command = [sys.executable, str(project / 'remote/oci_update_current.py'), str(row['vmid']), *lifecycle_args] # Host directories are configured by the user, who knows they are @@ -453,15 +473,24 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()): if '--acknowledge-external-data' not in command: command.append('--acknowledge-external-data') title = translate('Modify OCI') if proposal else translate('Update OCI') - if proposal is None: - completed = _run_lifecycle(command, title) - else: - # Saved environment/secrets must never be passed on the command line. - with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as file: - json.dump(proposal, file) - file.flush() - completed = _run_lifecycle(command + ['--proposal', file.name], title) - if completed and not getattr(ui, 'unattended', False): + # The downloaded image is only worth mentioning when a new container + # was created from it, not after a change applied to the running one. + with tempfile.TemporaryDirectory() as folder: + marker = Path(folder) / 'recreated' + os.environ['OCI_RECREATED_MARKER'] = str(marker) + try: + if proposal is None: + completed = _run_lifecycle(command, title) + else: + # Saved environment/secrets must never be passed on the command line. + with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as file: + json.dump(proposal, file) + file.flush() + completed = _run_lifecycle(command + ['--proposal', file.name], title) + finally: + os.environ.pop('OCI_RECREATED_MARKER', None) + recreated = marker.exists() + if completed and recreated and not getattr(ui, 'unattended', False): images.offer_removal(ui, [row['vmid']]) return completed action = ui.choose(translate('Interrupted operation'), [('status', translate('View status')), @@ -495,7 +524,10 @@ def _removal_summary(project, vmid): record = instances.read(instances.ROOT, member) except (OSError, ValueError, KeyError): record = {} - config = oci_remove.guest_config(member) + try: + config = _selected_configuration(member) + except (OSError, RuntimeError, subprocess.SubprocessError): + config = None cfg = parse_config(config) if config else {} name = (cfg.get('hostname') or (record.get('stack_member') or {}).get('name') or record.get('deployment', {}).get('hostname') or '') diff --git a/oci/src/proxmenux_oci/recreation.py b/oci/src/proxmenux_oci/recreation.py index d8ddf4b9..e1f3d897 100644 --- a/oci/src/proxmenux_oci/recreation.py +++ b/oci/src/proxmenux_oci/recreation.py @@ -4,7 +4,7 @@ import re from pathlib import Path, PurePosixPath from .i18n import translate -from .ui import UserCancelled +from .ui import BacktrackUI, RestartWizard, UserCancelled def positive_integer(ui, prompt, value, minimum=1): @@ -30,13 +30,13 @@ def edit_network(deployment, ui): network.update(bridge=bridge, ipv4=ipv4, gateway=gateway) -def edit_acceleration(candidate, ui): +def edit_acceleration(candidate, ui, confirm=True): from .installer import configure_acceleration deployment = candidate['deployment'] template = candidate.get('template', {}) installer = template.get('proxmox', {}).get('installer_profile', {}) hardware = installer.get('hardware_acceleration') - if not hardware or not ui.confirm(translate('Change GPU acceleration?'), False): + if not hardware or (confirm and not ui.confirm(translate('Change GPU acceleration?'), False)): return profiles = hardware.get('profiles', []) reference = template.get('container_contract', {}).get('image', {}).get('reference', '') @@ -104,44 +104,143 @@ def edit_environment(deployment, ui): environment.append(item) -def edit_recreation(record, ui): - candidate = copy.deepcopy(record) - refresh_template(candidate, ui) +def modification_options(candidate): + """Only offer supported settings; never put environment values in the menu.""" deployment = candidate['deployment'] resources = deployment['resources'] - resources['cores'] = positive_integer(ui, translate('Cores'), resources['cores']) - resources['memory_mb'] = positive_integer(ui, translate('RAM in MiB'), resources['memory_mb'], 128) + profile = candidate.get('template', {}).get('proxmox', {}).get('installer_profile', {}) + network = deployment.get('network', {}) + address = network.get('ipv4', 'dhcp') + + def row(label, value): + return f"{label:<26} {value}" + + options = [ + ('cores', row(translate('CPU cores'), resources['cores'])), + ('memory', row(translate('RAM in MiB'), resources['memory_mb'])), + ('network', row(translate('Network'), + f"{network.get('bridge', '-')} · {'DHCP' if address == 'dhcp' else address}")), + # Only the paths added by hand: the application's own volumes are not extra. + ('paths', row(translate('Extra paths'), sum(1 for m in deployment.get('mounts', []) if m.get('custom')))), + ] + if profile.get('shared_directories'): + options.append(('shares', translate('Shared directories'))) + if profile.get('hardware_acceleration'): + hardware = profile['hardware_acceleration'] + selected = next((p for p in hardware.get('profiles', []) + if p['id'] == deployment.get('hardware_profile')), {}) + options.append(('acceleration', row(translate('Acceleration'), + translate(selected.get('label') or deployment.get('hardware_profile') or 'CPU')))) + if profile.get('object_detector'): + options.append(('detector', translate('Object detector'))) + options += [ + ('devices', row(translate('Devices'), len(deployment.get('devices', [])))), + ('environment', row(translate('Variables'), len(deployment.get('environment', [])))), + ('catalog', translate('Catalog options')), + ('apply', translate('Review and apply')), + ('discard', translate('Discard changes')), + ] + return options + + +def edit_setting(candidate, ui, setting): + deployment = candidate['deployment'] template = candidate.get('template', {}) - if template.get('proxmox', {}).get('installer_profile', {}).get('shared_directories'): + profile = template.get('proxmox', {}).get('installer_profile', {}) + if setting == 'cores': + resources = deployment['resources'] + resources['cores'] = positive_integer(ui, translate('Cores'), resources['cores']) + elif setting == 'memory': + resources = deployment['resources'] + resources['memory_mb'] = positive_integer(ui, translate('RAM in MiB'), resources['memory_mb'], 128) + elif setting == 'network': + edit_network(deployment, ui) + elif setting == 'shares': from .installer import ask_shared_directories deployment['mounts'] += ask_shared_directories( ui, template, deployment['mounts'], deployment['rootfs']['storage'], True, optional=True, unprivileged=deployment.get('security', {}).get('unprivileged', True)) - from .custom_mounts import ask_custom_mounts - deployment['mounts'] = ask_custom_mounts( - ui, deployment['mounts'], deployment['rootfs']['storage']) - if ui.confirm(translate('Change the access network?'), False): - edit_network(deployment, ui) - edit_acceleration(candidate, ui) - from .installer import configure_detector - installer_profile = candidate.get('template', {}).get('proxmox', {}).get('installer_profile', {}) - deployment['devices'], notes = configure_detector(installer_profile, deployment.get('devices', []), ui) - if notes: - deployment['completion_notes'] = notes - from .extra_devices import ask_extra_devices - reference = candidate.get('template', {}).get('container_contract', {}).get('image', {}).get('reference', '') - repository = reference.split('@')[0].rsplit(':', 1)[0] - deployment['devices'] = ask_extra_devices( - ui, deployment.get('devices', []), deployment.get('security', {}).get('unprivileged', True), - allow_coral=repository in ('ghcr.io/blakeblackshear/frigate', 'codeproject/ai-server', 'docker.io/codeproject/ai-server')) - from .extra_devices import device_permissions - deployment['device_permissions'] = device_permissions( - reference, deployment['devices'], deployment.get('device_permissions')) - edit_environment(deployment, ui) - proposal = {'operation': 'recreate', 'candidate': candidate} - if record.get('observed', {}).get('config_sha256'): - proposal['base_config_sha256'] = record['observed']['config_sha256'] - return proposal + elif setting == 'paths': + from .custom_mounts import ask_custom_mounts + deployment['mounts'] = ask_custom_mounts(ui, deployment['mounts'], deployment['rootfs']['storage']) + elif setting == 'acceleration': + edit_acceleration(candidate, ui, confirm=False) + elif setting == 'detector': + from .installer import configure_detector + deployment['devices'], notes = configure_detector(profile, deployment.get('devices', []), ui) + if notes: + deployment['completion_notes'] = notes + elif setting == 'devices': + from .extra_devices import ask_extra_devices + reference = template.get('container_contract', {}).get('image', {}).get('reference', '') + repository = reference.split('@')[0].rsplit(':', 1)[0] + deployment['devices'] = ask_extra_devices( + ui, deployment.get('devices', []), deployment.get('security', {}).get('unprivileged', True), + allow_coral=repository in ('ghcr.io/blakeblackshear/frigate', 'codeproject/ai-server', 'docker.io/codeproject/ai-server')) + elif setting == 'environment': + edit_environment(deployment, ui) + elif setting == 'catalog': + refresh_template(candidate, ui) + else: + raise ValueError(setting) + if setting in ('acceleration', 'detector', 'devices'): + from .extra_devices import device_permissions + reference = candidate.get('template', {}).get('container_contract', {}).get('image', {}).get('reference', '') + permissions = device_permissions(reference, deployment.get('devices', []), deployment.get('device_permissions')) + if permissions is not None or 'device_permissions' in deployment: + deployment['device_permissions'] = permissions + + +def edit_section(candidate, ui, setting): + """A cancelled or invalid section cannot leave a half-edited draft.""" + wizard = BacktrackUI(ui) + try: + while True: + edited = copy.deepcopy(candidate) + try: + edit_setting(edited, wizard, setting) + return edited + except RestartWizard: + wizard.restart() + except UserCancelled: + return candidate + except ValueError as error: + if not wizard.retry_last(error): + ui.message(str(error), translate('Modify OCI')) + return candidate + finally: + wizard.close() + + +def edit_recreation(record, ui): + """Keep a draft across independently editable settings; apply it once.""" + from .cli import _deployment_summary_text + candidate = copy.deepcopy(record) + selected = 'cores' + while True: + pending = candidate != record + status = translate('Unapplied changes') if pending else translate('No pending changes') + text = (f"CT {record.get('vmid', '-')}: {candidate['deployment'].get('hostname', '')}\n" + f"{status}\n\n" + + translate('Choose a setting. Changes are applied only after the final review.')) + selected = ui.choose(text, modification_options(candidate), selected, + title=translate('Modify OCI'), show_tags=False) + if selected is None or selected == 'discard': + if not pending or ui.confirm(translate('Discard unapplied changes?'), False): + return None + selected = 'apply' + elif selected == 'apply': + if not pending: + ui.message(translate('Nothing was changed.'), translate('Modify OCI')) + return None + if ui.review(_deployment_summary_text(candidate['template'], candidate['deployment']), + translate('Modify OCI'), question=translate('Apply these changes?'), default=True): + proposal = {'operation': 'recreate', 'candidate': candidate} + if record.get('observed', {}).get('config_sha256'): + proposal['base_config_sha256'] = record['observed']['config_sha256'] + return proposal + else: + candidate = edit_section(candidate, ui, selected) def refresh_template(candidate, ui): diff --git a/oci/src/proxmenux_oci/registry_accounts.py b/oci/src/proxmenux_oci/registry_accounts.py index f500520c..2ad9791d 100644 --- a/oci/src/proxmenux_oci/registry_accounts.py +++ b/oci/src/proxmenux_oci/registry_accounts.py @@ -166,7 +166,7 @@ def registry_accounts_menu(ui, engine=ENGINE_ROOT, auth=None): return try: options = [('add', translate('Sign in to a registry'))] - options += [(f'account-{index}', f"{row['registry']:<28} {row['account'] or row['method']} [{translate('saved')}]") + options += [(f'account-{index}', f"{row['registry']:<28} [{translate('saved')}]") for index, row in enumerate(rows)] options += [('storage', translate('Credential storage and access'))] count = translate('{count} saved account.') if len(rows) == 1 else translate('{count} saved accounts.') diff --git a/oci/src/proxmenux_oci/ui.py b/oci/src/proxmenux_oci/ui.py index 8238d3c2..6ff5955f 100644 --- a/oci/src/proxmenux_oci/ui.py +++ b/oci/src/proxmenux_oci/ui.py @@ -221,6 +221,10 @@ class DialogUI: def _run(self, widget: list[str], title: str | None = None) -> subprocess.CompletedProcess[str]: environment = os.environ.copy() + # ncurses otherwise waits a full second to distinguish Escape from + # the prefix of a cursor/function key. Keep a short sequence window, + # while respecting an explicit terminal-specific setting. + environment.setdefault("ESCDELAY", "150") if environment.get("TERM", "").casefold() in {"", "dumb", "unknown"}: environment["TERM"] = "xterm-256color" # dialog draws on the terminal and writes the selection to stderr. diff --git a/oci/tests/test_carried_config_cache.py b/oci/tests/test_carried_config_cache.py new file mode 100644 index 00000000..026a3598 --- /dev/null +++ b/oci/tests/test_carried_config_cache.py @@ -0,0 +1,134 @@ +"""A fast Manage entry still detects config edits and verifies lifecycle work.""" +import contextlib +import copy +import json +import os +from pathlib import Path +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'remote')) +import oci_carried_record as carried + + +class CarriedConfigCacheTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) / 'instances' + self.source = Path(self.tmp.name) / 'lxc' + self.source.mkdir() + self.config = self.source / '120.conf' + self.config.write_text('# proxmenux-instance=example\nunprivileged: 1\n') + (self.root / '120').mkdir(parents=True) + self.cache = self.root / '120' / carried.CONFIG_CACHE + self.patch = patch.object(carried, 'LOCAL_CONFIGS', self.source) + self.patch.start() + self.addCleanup(self.patch.stop) + + def response(self, text='description: proxmenux-instance=example\nunprivileged: 1\n', status=0): + return SimpleNamespace(returncode=status, stdout=text) + + def test_warm_reads_avoid_pct_and_cache_is_private(self): + with patch.object(carried, '_run', return_value=self.response()) as run: + first = carried.configuration(self.root, 120) + self.assertEqual(carried.configuration(self.root, 120), first) + run.assert_called_once_with('pct', 'config', '120') + self.assertEqual(self.cache.stat().st_mode & 0o777, 0o600) + + def test_native_edits_invalidate_even_when_size_and_mtime_are_restored(self): + with patch.object(carried, '_run', side_effect=[self.response(), self.response('new')]) as run: + carried.configuration(self.root, 120) + info = self.config.stat() + self.config.write_text(self.config.read_text().replace('example', 'changed')) + os.utime(self.config, ns=(info.st_atime_ns, info.st_mtime_ns)) + self.assertEqual(carried.configuration(self.root, 120), 'new') + self.assertEqual(run.call_count, 2) + + def test_verify_forces_a_fresh_pct_read_with_a_warm_cache(self): + with patch.object(carried, '_run', side_effect=[self.response(), self.response('verified')]) as run: + carried.configuration(self.root, 120) + self.assertEqual(carried.configuration(self.root, 120, verify=True), 'verified') + self.assertEqual(run.call_count, 2) + + def test_missing_native_source_never_uses_or_populates_a_cache(self): + self.config.unlink() + with patch.object(carried, '_run', return_value=self.response()) as run: + carried.configuration(self.root, 120) + carried.configuration(self.root, 120) + self.assertEqual(run.call_count, 2) + self.assertFalse(self.cache.exists()) + + def test_concurrent_edit_does_not_cache_output_for_the_wrong_revision(self): + def command(*args): + self.config.write_text('changed while pct was reading\n') + return self.response() + with patch.object(carried, '_run', side_effect=command): + carried.configuration(self.root, 120) + self.assertFalse(self.cache.exists()) + + def test_edit_during_cache_lookup_falls_back_to_pct(self): + with patch.object(carried, '_run', return_value=self.response()): + carried.configuration(self.root, 120) + read_text = Path.read_text + def edit_during_read(path, *args, **kwargs): + text = read_text(path, *args, **kwargs) + if path == self.cache: + self.config.write_text('changed while reading the cache\n') + return text + with patch.object(Path, 'read_text', edit_during_read), \ + patch.object(carried, '_run', return_value=self.response('fresh')) as run: + self.assertEqual(carried.configuration(self.root, 120), 'fresh') + run.assert_called_once_with('pct', 'config', '120') + + def test_corrupt_cache_and_pct_failure_are_not_accepted(self): + for text in ('not json', '[]', '{"schema_version": 999}'): + with self.subTest(cache=text): + self.cache.write_text(text) + with patch.object(carried, '_run', return_value=self.response(status=1)) as run: + self.assertIsNone(carried.configuration(self.root, 120)) + run.assert_called_once() + self.assertEqual(self.cache.read_text(), text) + + def test_symlink_cache_does_not_read_or_replace_its_target(self): + target = Path(self.tmp.name) / 'outside' + target.write_text('private original') + self.cache.symlink_to(target) + with patch.object(carried, '_run', return_value=self.response()) as run: + self.assertEqual(carried.configuration(self.root, 120), self.response().stdout) + run.assert_called_once() + self.assertTrue(self.cache.is_symlink()) + self.assertEqual(target.read_text(), 'private original') + + def test_cached_config_still_detects_a_restored_container_record(self): + installation = '488ed3cb-1145-477a-b90f-c46777d69fb2' + record = {'schema_version': 1, 'vmid': 120, 'installation_id': installation, + 'status': 'installed', 'deployment': {'hostname': 'current'}} + carried.instances.write(carried.instances.location(self.root, 120), record) + native = f'description: proxmenux-instance={installation}\n' + with patch.object(carried, '_run', return_value=self.response(native)): + carried.configuration(self.root, 120) + rootfs = Path(self.tmp.name) / 'rootfs' + rootfs.mkdir() + previous = copy.deepcopy(record) + previous['deployment']['hostname'] = 'restored-snapshot' + carried.write_copy(rootfs, {'kind': carried.KIND, 'schema_version': 1, + 'record': previous, 'generation': 'restored-generation'}, + (os.getuid(), os.getgid())) + stamp = self.root / '120' / carried.STAMP + stamp.write_text(json.dumps({'generation': 'host-generation'})) + with patch.object(carried, '_run', side_effect=AssertionError('cached config expected')), \ + patch.object(carried, 'read_cluster', return_value=None), \ + patch.object(carried, 'running_pid', return_value=123), \ + patch.object(carried, 'container_root', return_value=contextlib.nullcontext(rootfs)): + self.assertEqual(carried.carry(self.root, 120, mount_stopped=False), 'stale') + self.assertFalse(carried.instances.location(self.root, 120).exists()) + self.assertTrue((self.root / '120' / f'retired-{installation}.json').exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_dialog_escape.py b/oci/tests/test_dialog_escape.py new file mode 100644 index 00000000..f5063878 --- /dev/null +++ b/oci/tests/test_dialog_escape.py @@ -0,0 +1,34 @@ +"""Escape delay is scoped to dialog and preserves terminal-specific overrides.""" +import os +from pathlib import Path +import subprocess +import sys +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src')) +from proxmenux_oci.ui import DialogUI, UserCancelled + + +class DialogEscapeTests(unittest.TestCase): + def test_escape_uses_short_delay_without_changing_parent_environment(self): + with patch.dict(os.environ, {'TERM': 'xterm'}, clear=True), \ + patch('proxmenux_oci.ui.subprocess.run', return_value=subprocess.CompletedProcess([], 255, stderr='')) as run: + self.assertIsNone(DialogUI().choose('Menu', [('1', 'First')])) + self.assertEqual(run.call_args.kwargs['env']['ESCDELAY'], '150') + self.assertNotIn('ESCDELAY', os.environ) + + def test_explicit_delay_is_preserved(self): + with patch.dict(os.environ, {'ESCDELAY': '900'}), \ + patch('proxmenux_oci.ui.subprocess.run', return_value=subprocess.CompletedProcess([], 0, stderr='1')) as run: + self.assertEqual(DialogUI().choose('Menu', [('1', 'First')]), '1') + self.assertEqual(run.call_args.kwargs['env']['ESCDELAY'], '900') + + def test_escape_in_search_input_keeps_cancellation_behavior(self): + with patch('proxmenux_oci.ui.subprocess.run', return_value=subprocess.CompletedProcess([], 255, stderr='')): + with self.assertRaises(UserCancelled): + DialogUI().ask('Search') + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_external_tags.py b/oci/tests/test_external_tags.py new file mode 100644 index 00000000..0f09c5e3 --- /dev/null +++ b/oci/tests/test_external_tags.py @@ -0,0 +1,203 @@ +"""Proxmox tags can change without preventing OCI management or being lost.""" +import copy +import io +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from contextlib import redirect_stdout +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'remote')) + +import oci_instance_reconcile as reconcile +import oci_instance_transaction as transaction +from oci_installation_state import sha + +MARKER = '11111111-1111-4111-8111-111111111111' +BASE = (f'description: proxmenux-instance={MARKER}\n' + 'rootfs: local-lvm:vm-200-disk-0,size=8G\n' + 'unprivileged: 1\ncores: 2\nmemory: 512\n' + 'net0: name=eth0,bridge=vmbr0,hwaddr=02:00:00:00:00:01,ip=dhcp\n' + 'net1: name=eth1,bridge=vmbr1,ip=dhcp\nstartup: order=20\n').encode() + + +def config(tags): + return BASE + (f'tags: {tags}\n'.encode() if tags is not None else b'') + + +def record(tags='oci;proxmenux'): + before = config(tags) + return { + 'schema_version': 1, 'vmid': 200, 'status': 'installed', + 'installation_id': MARKER, + 'template': {'id': 'test-app', 'container_contract': {'volumes': []}}, + 'deployment': { + 'vmid': 200, 'rootfs': {'storage': 'local-lvm', 'size_gb': 8}, + 'network': {}, 'resources': {'cores': 2, 'memory_mb': 512}, + 'mounts': [], 'devices': [], + }, + 'observed': { + 'config': before.decode(), 'config_sha256': sha(before), + 'image': {'architecture': 'amd64', 'manifest_digest': 'sha256:old', + 'defaults': {'Volumes': {}}}, + }, + } + + +class ExternalTagsTests(unittest.TestCase): + def test_adding_editing_reordering_and_removing_tags_need_no_adoption(self): + for old, new in ((None, 'homelab'), ('oci;proxmenux', 'homelab;oci;proxmenux'), + ('oci;proxmenux', 'proxmenux;oci'), ('oci;proxmenux', None), + ('oci;proxmenux', '')): + with self.subTest(old=old, new=new): + saved, current = record(old), config(new) + self.assertIsNone(reconcile.propose(saved, current)) + self.assertEqual(transaction.external_changes(saved, current), {}) + + def test_tags_can_change_together_with_cpu_and_memory(self): + current = config('homelab').replace(b'cores: 2', b'cores: 4').replace(b'memory: 512', b'memory: 2048') + self.assertIsNone(reconcile.propose(record(), current)) + self.assertEqual(transaction.external_changes(record(), current), {'cores': 4, 'memory': 2048}) + + def test_tag_changes_do_not_bypass_identity_or_network_checks(self): + for current in (config('homelab').replace(MARKER.encode(), b'22222222-2222-4222-8222-222222222222'), + config('homelab').replace(b'bridge=vmbr0', b'bridge=vmbr2')): + with self.subTest(config=current): + with self.assertRaises(ValueError): + reconcile.propose(record(), current) + with self.assertRaises(ValueError): + transaction.external_changes(record(), current) + + def test_new_volume_still_requires_adoption_when_tags_also_change(self): + saved = record() + current = config('homelab') + b'mp0: local-lvm:vm-200-disk-1,mp=/data,size=8G,backup=1\n' + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + path = root / '200/oci-compose.json' + path.parent.mkdir() + path.write_text(json.dumps(saved)) + with patch.object(reconcile.instances, 'ROOT', root): + proposal = reconcile.propose(saved, current) + self.assertEqual(proposal['candidate']['deployment']['mounts'][0]['container_path'], '/data') + self.assertEqual(len(proposal['details']), 1) + self.assertEqual(json.loads(path.read_text()), saved) + + +class Prepared(Exception): + """Stop a mocked transaction before it can stop or modify any container.""" + + +class TagReplayTests(unittest.TestCase): + def prepare(self, folder, tags, operation, coordinated=None): + current, saved = config(tags), record() + archive = folder / 'image.tar' + archive.write_bytes(b'fixture') + states = [] + + def checkpoint(_path, state, phase): + self.assertEqual(phase, 'prepared') + states.append(copy.deepcopy(state)) + raise Prepared() + + def command(*args): + if args[:2] == ('pct', 'config'): + return current + if args[:3] == ('pvesh', 'get', '/cluster/ha/resources'): + return b'[]' + if args[:2] == ('pct', 'status'): + return b'status: running' + self.fail(f'Unexpected host command: {args}') + + proposal = {'operation': 'recreate', 'candidate': copy.deepcopy(saved)} if operation == 'recreate' else None + image = {'architecture': 'amd64', 'os': 'linux', 'manifest_digest': 'sha256:new', 'defaults': {}} + with (patch.object(transaction.instances, 'read', return_value=saved), + patch.object(transaction, 'run', side_effect=command), + patch.object(transaction, 'check_archive'), + patch.object(transaction, 'image_from_archive', return_value=image), + patch.object(transaction.work_backup, 'locate', side_effect=lambda directory, *_: directory), + patch.object(transaction, 'open_log'), + patch.object(transaction, 'checkpoint', side_effect=checkpoint), + redirect_stdout(io.StringIO()), self.assertRaises(Prepared)): + transaction.apply(folder, 200, archive, operation, proposal=proposal, coordinated=coordinated) + return states[0] + + @unittest.skipUnless(shutil.which('bash') and shutil.which('jq'), 'bash and jq are needed') + def test_update_and_modify_replay_current_tags_including_no_tags(self): + installer = (ROOT / 'remote/install_oci.sh').read_text() + function = re.search(r'^apply_kept_settings\(\) \{\n.*?^\}\n', installer, re.M | re.S) + self.assertIsNotNone(function) + for operation in ('update', 'recreate'): + for tags in ('homelab;oci;proxmenux', 'oci', None, ''): + with self.subTest(operation=operation, tags=tags), tempfile.TemporaryDirectory() as name: + folder = Path(name) + state = self.prepare(folder, tags, operation) + kept = state['runtime_deployment']['kept_proxmox_settings'] + deployment = folder / 'deployment.json' + deployment.write_text(json.dumps({'kept_proxmox_settings': kept})) + calls = folder / 'calls' + script = ''' +pct() { printf '%s\\0' "$@" >> "$CALLS"; printf '\\n' >> "$CALLS"; } +oci_quiet() { "$@"; } +translate() { printf '%s' "$1"; } +die() { printf '%s\\n' "$*" >&2; exit 1; } +''' + done = subprocess.run(['bash', '-euc', script + function.group(0) + '\napply_kept_settings'], + env=dict(os.environ, VMID='200', DEPLOYMENT_FILE=str(deployment), CALLS=str(calls)), + capture_output=True, text=True) + self.assertEqual(done.returncode, 0, done.stderr) + commands = [line.split(b'\0')[:-1] for line in calls.read_bytes().splitlines()] + expected = [b'set', b'200', b'--tags', tags.encode()] if tags else [b'set', b'200', b'--delete', b'tags'] + self.assertIn(expected, commands) + self.assertIn([b'set', b'200', b'--net1', b'name=eth1,bridge=vmbr1,ip=dhcp'], commands) + self.assertIn([b'set', b'200', b'--startup', b'order=20'], commands) + + def test_stack_operations_also_preserve_tags_and_their_absence(self): + for operation in ('update', 'recreate'): + for tags in ('homelab;oci', None, ''): + with self.subTest(operation=operation, tags=tags), tempfile.TemporaryDirectory() as name: + state = self.prepare(Path(name), tags, operation, coordinated={'stack': 'test'}) + self.assertEqual(state['preserved_stack_config']['tags'], tags or '') + + @unittest.skipUnless(shutil.which('bash') and shutil.which('jq'), 'bash and jq are needed') + def test_native_stack_check_accepts_tag_edits_and_nothing_else(self): + saved = record() + instances = transaction.instances + for new in ('homelab;oci;proxmenux', 'proxmenux;oci', '', None): + with self.subTest(new=new): + self.assertTrue(instances.same_config_except_notes(saved, config(new), ignore_tags=True)) + self.assertFalse(instances.same_config_except_notes(saved, config(new))) + changed = config('homelab').replace(b'cores: 2', b'cores: 4') + self.assertFalse(instances.same_config_except_notes(saved, changed, ignore_tags=True)) + other = config('homelab').replace(MARKER.encode(), b'22222222-2222-4222-8222-222222222222') + self.assertFalse(instances.same_config_except_notes(saved, other, ignore_tags=True)) + source = (ROOT / 'remote/oci_stack_native.py').read_text() + self.assertIn('same_config_except_notes(record, config, ignore_tags=True)', source) + + def test_installer_still_refuses_unsupported_kept_settings(self): + installer = (ROOT / 'remote/install_oci.sh').read_text() + function = re.search(r'^apply_kept_settings\(\) \{\n.*?^\}\n', installer, re.M | re.S).group(0) + with tempfile.TemporaryDirectory() as name: + deployment = Path(name) / 'deployment.json' + deployment.write_text(json.dumps({'kept_proxmox_settings': {'unprivileged': '0'}})) + script = ''' +oci_quiet() { echo 'must not run' >&2; exit 99; } +translate() { printf '%s' "$1"; } +die() { printf '%s\\n' "$*" >&2; exit 1; } +''' + done = subprocess.run(['bash', '-euc', script + function + '\napply_kept_settings'], + env=dict(os.environ, VMID='200', DEPLOYMENT_FILE=str(deployment)), + capture_output=True, text=True) + self.assertEqual(done.returncode, 1) + self.assertIn('Unsupported kept setting: unprivileged', done.stderr) + self.assertNotIn('must not run', done.stderr) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_guessed_endpoint_healthcheck.py b/oci/tests/test_guessed_endpoint_healthcheck.py new file mode 100644 index 00000000..8ce9003f --- /dev/null +++ b/oci/tests/test_guessed_endpoint_healthcheck.py @@ -0,0 +1,87 @@ +"""A web address guessed from the compose file may belong to a service that does +not speak HTTP. Updates, Modify and recovery must accept its answer instead of +waiting for an HTTP reply that never comes.""" + +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'remote')) + +import oci_instance_transaction as transaction + +INSTALLER = ROOT / 'remote/install_oci.sh' +GUESSED = {'first_run': {'endpoints': [{'label': 'Web UI', 'scheme': 'http', 'port': 3306, 'path': '/', + 'source': 'compose-first-tcp-port-fallback'}]}} +DECLARED = {'first_run': {'endpoints': [{'label': 'Web UI', 'scheme': 'http', 'port': 8080, 'path': '/'}]}} + + +def probe_function(): + source = INSTALLER.read_text() + start = source.index('healthcheck_probe() {') + end = source.index('\n}\n', start) + 3 + return source[start:end] + + +class GuessedEndpointCheckTests(unittest.TestCase): + def test_only_a_guessed_endpoint_accepts_other_protocols(self): + self.assertTrue(transaction.effective_healthcheck(GUESSED)['accept_non_http']) + self.assertNotIn('accept_non_http', transaction.effective_healthcheck(DECLARED)) + + def run_probe(self, exit_code, code='000', accept_non_http='true', any_status='true'): + with tempfile.TemporaryDirectory() as directory: + fake = Path(directory) / 'curl' + fake.write_text(f'#!/bin/sh\nprintf %s {code}\nexit {exit_code}\n') + fake.chmod(0o755) + script = (probe_function() + '\nCURL_ARGS=()\nHC_URL=http://192.0.2.1:3306/\n' + f'HC_ANY_STATUS={any_status}\nHC_ACCEPT_NON_HTTP={accept_non_http}\nhealthcheck_probe\n') + env = {**os.environ, 'PATH': f'{directory}:{os.environ["PATH"]}'} + return subprocess.run(['bash', '-c', script], env=env, capture_output=True).returncode + + def test_installer_probe_accepts_a_non_http_answer_only_when_guessed(self): + for status in transaction.NON_HTTP_ANSWER: + with self.subTest(status=status): + self.assertEqual(self.run_probe(status), 0) + self.assertNotEqual(self.run_probe(status, accept_non_http='false'), 0) + # A closed port, a timeout or a server error still fail. + for status, code in ((7, '000'), (28, '000'), (0, '503')): + with self.subTest(status=status, code=code): + self.assertNotEqual(self.run_probe(status, code), 0) + self.assertEqual(self.run_probe(0, '200'), 0) + self.assertEqual(self.run_probe(0, any_status='false', accept_non_http='false'), 0) + + def test_recovery_check_accepts_a_database_answer(self): + check = transaction.effective_healthcheck(GUESSED) + check.update(timeout_seconds=6, stability_seconds=0) + template = {'proxmox': {'installer_profile': {'startup_healthcheck': check}}} + + def run(*args): + if args[:2] == ('pct', 'status'): + return b'status: running\n' + if args[:2] == ('pct', 'config'): + return b'' + if args[0] == 'lxc-info': + return b'192.0.2.10\n' + raise AssertionError(args) + + with patch.object(transaction, 'run', side_effect=run), \ + patch.object(transaction, 'probe_http', return_value=(b'000', 1)), \ + patch.object(transaction, 'log'): + self.assertEqual(transaction.healthcheck(113, template), 'http://192.0.2.10:3306/') + declared = transaction.effective_healthcheck(DECLARED) + declared.update(timeout_seconds=4, stability_seconds=0) + template = {'proxmox': {'installer_profile': {'startup_healthcheck': declared}}} + with patch.object(transaction, 'run', side_effect=run), \ + patch.object(transaction, 'probe_http', return_value=(b'000', 1)), \ + patch.object(transaction, 'log'), patch.object(transaction.time, 'sleep'): + with self.assertRaises(ValueError): + transaction.healthcheck(113, template) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_instance_reconcile.py b/oci/tests/test_instance_reconcile.py index b0fb25a1..59cd4a3e 100644 --- a/oci/tests/test_instance_reconcile.py +++ b/oci/tests/test_instance_reconcile.py @@ -1,4 +1,5 @@ """Regression tests for explicit adoption of external OCI resources.""" +import copy import json from pathlib import Path import sys @@ -155,5 +156,78 @@ class ProposalTests(unittest.TestCase): self.assertEqual(json.loads(history[0].read_text())['deployment']['mounts'], []) +class EnvironmentAdoptionTests(unittest.TestCase): + marker = '11111111-1111-1111-1111-111111111111' + + def record(self, env): + config = f'description: proxmenux-instance={self.marker}\nenv: {env}\n'.encode() + return { + 'vmid': 200, 'status': 'installed', 'installation_id': self.marker, + 'deployment': {'mounts': [], 'devices': [], 'environment': [ + {'name': 'APP_PASSWORD', 'value': 'first-secret', 'sensitive': True}]}, + 'observed': {'config': config.decode(), 'config_sha256': sha(config), + 'archive_path': '/unused.tar', 'resolved_registry_digest': 'sha256:test', + 'image': {'manifest_digest': 'sha256:test', 'defaults': {'Env': ['PATH=/bin']}}}, + } + + def propose(self, record, env): + config = f'description: proxmenux-instance={self.marker}\nenv: {env}\n'.encode() + with (patch.object(reconcile.instances, 'ROOT', Path('/inert')), + patch.object(reconcile.instances, 'identity', return_value=self.marker), + patch.object(reconcile.instances, 'location', return_value=Path(__file__)), + patch.object(reconcile.gpu_devices, 'capture', return_value={}), + patch.object(reconcile.host_mounts, 'capture_sources', return_value={}), + patch.object(reconcile.transaction, 'preflight')): + return reconcile.propose(record, config) + + def test_a_password_changed_in_proxmox_is_adopted_without_showing_it(self): + record = self.record('PATH=/bin\0APP_PASSWORD=first-secret') + proposal = self.propose(record, 'PATH=/bin\0APP_PASSWORD=my-own-secret\0TZ=Europe/Madrid') + environment = proposal['candidate']['deployment']['environment'] + self.assertEqual(environment, [ + {'name': 'APP_PASSWORD', 'value': 'my-own-secret', 'sensitive': True}, + {'name': 'TZ', 'value': 'Europe/Madrid', 'sensitive': True}]) + self.assertEqual(proposal['details'], ['env APP_PASSWORD: value changed in Proxmox', + 'env TZ: added in Proxmox']) + self.assertNotIn('my-own-secret', str(proposal['details'])) + self.assertEqual(record['deployment']['environment'][0]['value'], 'first-secret') + + def test_reordered_variables_need_no_decision(self): + record = self.record('PATH=/bin\0APP_PASSWORD=first-secret') + proposal = self.propose(record, 'APP_PASSWORD=first-secret\0PATH=/bin') + self.assertEqual(proposal['details'], []) + + def test_removing_an_image_default_is_refused(self): + record = self.record('PATH=/bin\0APP_PASSWORD=first-secret') + with self.assertRaisesRegex(ValueError, 'env'): + self.propose(record, 'APP_PASSWORD=first-secret') + + def test_removing_a_saved_variable_removes_it_from_the_contract(self): + record = self.record('PATH=/bin\0APP_PASSWORD=first-secret') + proposal = self.propose(record, 'PATH=/bin') + self.assertEqual(proposal['candidate']['deployment']['environment'], []) + self.assertEqual(proposal['details'], ['env APP_PASSWORD: removed in Proxmox']) + + def test_other_changes_next_to_the_variables_are_still_refused(self): + record = self.record('PATH=/bin') + config = (f'description: proxmenux-instance={self.marker}\nenv: PATH=/bin\0A=1\n' + 'net0: name=eth0,bridge=vmbr1\n').encode() + with (patch.object(reconcile.instances, 'identity', return_value=self.marker), + self.assertRaisesRegex(ValueError, 'net0')): + reconcile.propose(record, config) + + +class OverlapMessageTests(unittest.TestCase): + def test_the_refusal_names_both_paths(self): + from test_modify_review import installation + saved, config = installation() + candidate = copy.deepcopy(saved) + volume = {'type': 'managed-volume', 'source': 'local-lvm', 'size_gb': 1, 'backup': True, 'read_only': False} + candidate['deployment']['mounts'] = [dict(volume, container_path='/source'), + dict(volume, container_path='/source/archivos')] + with self.assertRaisesRegex(ValueError, '/source/archivos - /source$'): + reconcile.transaction.preflight(saved, candidate, config) + + if __name__ == '__main__': unittest.main() diff --git a/oci/tests/test_manage_sync_notice.py b/oci/tests/test_manage_sync_notice.py new file mode 100644 index 00000000..ede909b5 --- /dev/null +++ b/oci/tests/test_manage_sync_notice.py @@ -0,0 +1,34 @@ +"""Opening Manage shows the sync notice only while some configuration is still unread.""" +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'src')) +sys.path.insert(0, str(ROOT / 'remote')) + +from proxmenux_oci import management +import oci_carried_record as carried + + +class ManageSyncNoticeTests(unittest.TestCase): + def test_notice_only_while_an_installed_application_has_no_cache(self): + rows = [{'vmid': 101, 'status': 'installed', 'pending': False}, + {'vmid': 102, 'status': 'installed', 'pending': True}, + {'vmid': 103, 'status': 'failed', 'pending': False}] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + with patch.object(carried.instances, 'ROOT', root), \ + patch.object(management, 'saved_inventory', return_value=rows): + self.assertTrue(management._records_unread(ROOT)) + cache = carried.instances.location(root, 101).parent / carried.CONFIG_CACHE + cache.parent.mkdir(parents=True) + cache.write_text('{}') + # The pending and failed applications are never cached by the sync. + self.assertFalse(management._records_unread(ROOT)) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_menu_performance.py b/oci/tests/test_menu_performance.py new file mode 100644 index 00000000..94981ceb --- /dev/null +++ b/oci/tests/test_menu_performance.py @@ -0,0 +1,109 @@ +"""Menu speedups avoid unused probes, retaining confirmation and capacity checks.""" +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import Mock,patch +ROOT=Path(__file__).resolve().parents[1] +sys.path[:0]=[str(ROOT/'src'),str(ROOT/'remote')] +from proxmenux_oci import management as menu +import oci_instances as instances +import oci_instance_reconcile as reconcile +import oci_remove as remover +import oci_work_backup as backup + + +class UpdateEntryTests(unittest.TestCase): + def run_update(self,approved,ready): + events=[] + record={'status':'installed','vmid':110,'deployment':{},'template':{}} + row={'vmid':110,'reason':'matched','stack':False,'pending':False,'status':'installed'} + ui=Mock();ui.unattended=True + ui.review.side_effect=lambda *a,**kw:events.append('review') or approved + with patch.object(menu,'carry_records',return_value={}) as carry, \ + patch.object(menu,'check_selected',return_value=row), \ + patch.object(menu,'_selected_configuration',return_value=b'current config'), \ + patch.object(instances,'read',return_value=record),patch.object(reconcile,'propose',return_value=None), \ + patch.object(menu,'_work_backup_ready',side_effect=lambda *a:events.append('backup') or ready), \ + patch.object(menu,'_run_lifecycle',side_effect=lambda *a:events.append('lifecycle') or True): + result=menu.manage_instance(ROOT,ui,row,action='update') + carry.assert_called_once_with(ROOT,vmids=[110],verify=True) + return result,events + + def test_declining_update_does_not_probe_disks_or_storages(self): + result,events=self.run_update(False,False) + self.assertFalse(result);self.assertEqual(events,['review']) + + def test_accepted_update_checks_backup_space_before_starting(self): + result,events=self.run_update(True,True) + self.assertTrue(result);self.assertEqual(events,['review','backup','lifecycle']) + + def test_insufficient_space_still_prevents_the_update(self): + result,events=self.run_update(True,False) + self.assertFalse(result);self.assertEqual(events,['review','backup']) + + +class BackupDestinationTests(unittest.TestCase): + def status(self,local_free,saved_free=None,only_needed=True): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);saved= root/'nas';saved.mkdir() + record={'deployment':{backup.KEY:'nas'}} + with patch.object(backup,'application',return_value=(110,[110,111])), \ + patch.object(instances,'read',return_value=record), \ + patch.object(backup,'required',return_value=100) as required, \ + patch.object(backup,'available',side_effect=lambda p:saved_free if Path(p)==saved else local_free), \ + patch.object(backup,'storage_path',return_value=saved if saved_free is not None else None) as path, \ + patch.object(backup,'file_storages',return_value=[{'storage':'enough','free':200},{'storage':'small','free':50}]) as alternatives: + result=backup.status(root,110,only_needed=only_needed) + required.assert_called_once_with([110,111]) + return result,path.call_count,alternatives.call_count + + def test_local_room_avoids_saved_storage_and_alternative_queries(self): + result,saved,alternatives=self.status(200,saved_free=1000) + self.assertTrue(result['fits']);self.assertEqual((saved,alternatives),(0,0)) + + def test_saved_destination_that_fits_avoids_alternative_queries(self): + result,saved,alternatives=self.status(50,saved_free=200) + self.assertTrue(result['saved_fits']);self.assertEqual((saved,alternatives),(1,0)) + + def test_when_neither_fits_all_available_destinations_are_checked(self): + result,saved,alternatives=self.status(50,saved_free=40) + self.assertFalse(result['fits']);self.assertFalse(result['saved_fits']) + self.assertEqual(result['candidates'],[{'storage':'enough','free':200}]) + self.assertEqual(alternatives,1) + + def test_missing_saved_storage_still_offers_alternatives(self): + result,saved,alternatives=self.status(50) + self.assertEqual(result['candidates'],[{'storage':'enough','free':200}]) + self.assertEqual(alternatives,1) + + def test_complete_status_keeps_legacy_fields_and_all_candidates(self): + result,saved,alternatives=self.status(200,saved_free=300,only_needed=False) + self.assertTrue(result['fits']);self.assertTrue(result['saved_fits']) + self.assertEqual((saved,alternatives),(1,1)) + self.assertEqual(result['candidates'],[{'storage':'enough','free':200}]) + + +class RemovalPreviewTests(unittest.TestCase): + def preview(self,reader): + record={'vmid':110,'deployment':{},'template':{'catalog_ui':{'title':'Example'}}} + with patch.object(remover,'members_of',return_value=(110,record,[110])), \ + patch.object(instances,'read',return_value=record), \ + patch.object(menu,'_selected_configuration',side_effect=reader), \ + patch.object(remover,'guest_config',side_effect=AssertionError('Duplicate pct process')), \ + patch.object(remover,'host_directories',return_value=[]), \ + patch.object(remover,'private_bridge',return_value=None): + return menu._removal_summary(ROOT,110) + + def test_preview_reuses_verified_config_and_keeps_volume_and_host_path_details(self): + config=b'hostname: example\nrootfs: local:subvol-110-disk-0,size=8G\nmp0: /mnt/media,mp=/media,backup=0\n' + text=self.preview(lambda v:config) + self.assertIn('8G',text);self.assertIn('/mnt/media',text) + self.assertIn('not targeted for removal',text) + + def test_missing_guest_can_still_be_reviewed_without_a_second_probe(self): + text=self.preview(lambda v:(_ for _ in ()).throw(RuntimeError('guest missing'))) + self.assertIn('CT 110',text);self.assertIn('rootfs -',text) + + +if __name__=='__main__':unittest.main() diff --git a/oci/tests/test_modify_image_reuse.py b/oci/tests/test_modify_image_reuse.py new file mode 100644 index 00000000..01d475c0 --- /dev/null +++ b/oci/tests/test_modify_image_reuse.py @@ -0,0 +1,338 @@ +"""Unchanged OCI images permit native resource edits or verified local reuse.""" +import copy +import hashlib +import json +from pathlib import Path +import re +import stat +import sys +import tempfile +import unittest +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT/'remote')) +import oci_carried_record +import oci_instances as instances +import oci_resource_modify as resources +import oci_update_current as update + +DIGEST = 'sha256:'+'a'*64 +NEW = 'sha256:'+'b'*64 +IDENTITY = '11111111-1111-4111-8111-111111111111' +CONFIG = (f'arch: amd64\ncores: 2\ndescription: proxmenux-instance={IDENTITY}\n' + 'memory: 512\nrootfs: local-zfs:subvol-115-disk-0,size=8G\n' + 'lxc.environment.runtime: A=one\nlxc.environment.runtime: B=two\n').encode() + + +def record(): + return {'schema_version': 1, 'vmid': 115, 'installation_id': IDENTITY, 'status': 'installed', + 'template': {'id': 'example-app', 'catalog_ui': {'title': 'Example'}, + 'container_contract': {'image': {'reference': 'example/app:latest'}}, + 'proxmox': {'installer_profile': {'startup_healthcheck': {'type': 'running'}}}}, + 'deployment': {'vmid': 115, 'hostname': 'example', 'template_storage': 'local', + 'resources': {'cores': 2, 'memory_mb': 512, 'swap_mb': 512}, + 'network': {'bridge': 'vmbr0', 'ipv4': 'dhcp'}, 'mounts': [], 'devices': [], + 'environment': [{'name': 'PASSWORD', 'value': 'private-test-value', 'sensitive': True}]}, + 'observed': {'config': CONFIG.decode(), 'config_sha256': resources.sha(CONFIG), + 'image': {'manifest_digest': DIGEST, 'architecture': 'amd64', 'os': 'linux'}, + 'resolved_registry_digest': DIGEST, 'archive_path': '/missing/deleted-image.tar'}} + + +class ResourceModifyTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name)/'instances' + self.configs = Path(self.temp.name)/'configs' + self.configs.mkdir() + self.file = self.configs/'115.conf' + self.file.write_bytes(CONFIG) + self.saved = record() + self.candidate = copy.deepcopy(self.saved) + self.candidate['deployment']['resources'].update(cores=3, memory_mb=768) + self.params = {'cores': '3', 'memory': '768'} + instances.write(instances.location(self.root, 115), self.saved) + self.calls = [] + self.native_failure = None + def run(*args): + self.calls.append(args) + self.assertEqual(args[:3], ('pct', 'set', '115')) + expected = args[args.index('--digest')+1] + self.assertEqual(expected, hashlib.sha1(self.file.read_bytes()).hexdigest()) + if self.native_failure: + raise self.native_failure + text = self.file.read_text() + for index in range(5, len(args), 2): + text = re.sub(r'^'+args[index][2:]+r': .*$', args[index][2:]+': '+args[index+1], text, flags=re.M) + self.file.write_text(text) + return b'' + def observe(*args): + result = copy.deepcopy(self.saved['observed']) + result.update(config=self.file.read_text(), config_sha256=resources.sha(self.file.read_bytes())) + return result + patches = [patch.object(resources, 'CONFIG_DIR', self.configs), + patch.object(instances, 'command', side_effect=lambda *args: self.file.read_bytes()), + patch.object(resources.transaction, 'run', side_effect=run), + patch.object(resources.transaction, 'open_log'), patch.object(resources.transaction, 'log'), + patch.object(instances, 'observe', side_effect=observe), + patch.object(oci_carried_record, 'carry', return_value='carried')] + for p in patches: + p.start() + self.addCleanup(p.stop) + + def apply(self): + return resources.apply(self.root, self.saved, self.candidate, CONFIG, self.params) + + def test_resource_only_change_needs_no_archive_and_preserves_every_other_field(self): + directory = self.apply() + after = instances.read(self.root, 115) + self.assertEqual(after['deployment']['resources'], {'cores': 3, 'memory_mb': 768, 'swap_mb': 512}) + for key in ('network', 'mounts', 'devices', 'environment'): + self.assertEqual(after['deployment'][key], self.saved['deployment'][key]) + self.assertEqual(after['observed']['image'], self.saved['observed']['image']) + self.assertEqual(after['observed']['archive_path'], '/missing/deleted-image.tar') + self.assertEqual(after['observed']['config_sha256'], resources.sha(self.file.read_bytes())) + self.assertNotIn('pending_transaction', after) + self.assertEqual(len(self.calls), 1) + self.assertEqual(stat.S_IMODE((directory/'before.json').stat().st_mode), 0o600) + self.assertEqual(json.loads((directory/'before.json').read_text())['record'], self.saved) + self.assertNotIn('private-test-value', str(self.calls)) + + def test_network_environment_template_and_extra_device_edits_use_the_normal_installer(self): + for mutate in ( + lambda c: c['deployment']['network'].update(ipv4='192.0.2.2/24'), + lambda c: c['deployment']['environment'][0].update(value='changed'), + lambda c: c['template']['container_contract']['image'].update(reference='example/app:other'), + lambda c: c['deployment']['devices'].append({'host_path': '/dev/ttyACM0'}), + lambda c: c['deployment']['resources'].update(swap_mb=1024), + ): + candidate = copy.deepcopy(self.candidate) + mutate(candidate) + self.assertIsNone(resources.changes(self.saved, candidate, CONFIG)) + + def test_invalid_values_and_unchanged_proposal_do_not_use_the_native_path(self): + for value in (True, 0, -1, 64, 128.5, '768'): + candidate = copy.deepcopy(self.saved) + candidate['deployment']['resources']['memory_mb'] = value + self.assertIsNone(resources.changes(self.saved, candidate, CONFIG)) + self.assertIsNone(resources.changes(self.saved, copy.deepcopy(self.saved), CONFIG)) + + def test_cpu_limit_profile_stays_a_cpu_limit(self): + config = CONFIG.replace(b'cores: 2', b'cpulimit: 2') + self.assertEqual(resources.changes(self.saved, self.candidate, config), {'cpulimit': '3', 'memory': '768'}) + + def test_native_failure_leaves_saved_record_and_config_unchanged(self): + self.native_failure = RuntimeError('native API rejected edit') + with self.assertRaisesRegex(RuntimeError, 'rejected'): + self.apply() + self.assertEqual(self.file.read_bytes(), CONFIG) + self.assertEqual(instances.read(self.root, 115), self.saved) + + def test_observation_failure_rolls_back_resource_values(self): + with patch.object(instances, 'observe', side_effect=RuntimeError('observation failed')): + with self.assertRaisesRegex(RuntimeError, 'observation failed'): + self.apply() + self.assertEqual(self.file.read_bytes(), CONFIG) + self.assertEqual(instances.read(self.root, 115), self.saved) + self.assertEqual(len(self.calls), 2) + + def test_record_write_failure_rolls_back_resources(self): + real_write = instances.write + failed = False + def write(path, value): + nonlocal failed + if Path(path) == instances.location(self.root, 115) and not failed: + failed = True + raise OSError('record could not be saved') + real_write(path, value) + with patch.object(instances, 'write', side_effect=write): + with self.assertRaisesRegex(OSError, 'could not be saved'): + self.apply() + self.assertEqual(self.file.read_bytes(), CONFIG) + self.assertEqual(instances.read(self.root, 115), self.saved) + + def test_configuration_drift_is_rejected_before_native_set(self): + self.file.write_bytes(CONFIG.replace(b'memory: 512', b'memory: 1024')) + with self.assertRaisesRegex(ValueError, 'changed'): + self.apply() + self.assertEqual(self.calls, []) + + def test_repeated_environment_entries_are_all_preserved_by_the_guard(self): + changed = CONFIG.replace(b'cores: 2', b'cores: 3').replace(b'memory: 512', b'memory: 768') + self.assertTrue(resources.expected_config(CONFIG, changed, self.params)) + self.assertFalse(resources.expected_config(CONFIG, changed.replace(b'A=one', b'A=foreign'), self.params)) + + def test_cpu_change_preserves_homepage_dns_timezone_and_cpu_priority(self): + extra = b'nameserver: 10.0.1.1\ntimezone: Europe/Madrid\ncpuunits: 100\n' + config = CONFIG + extra + self.file.write_bytes(config) + self.saved['observed'].update(config=config.decode(), config_sha256=resources.sha(config)) + self.saved['deployment']['resources']['cpu_units'] = 100 + self.candidate = copy.deepcopy(self.saved) + self.candidate['deployment']['resources']['cores'] = 3 + instances.write(instances.location(self.root,115),self.saved) + resources.apply(self.root,self.saved,self.candidate,config,{'cores':'3'}) + self.assertEqual(self.file.read_bytes(), config.replace(b'cores: 2',b'cores: 3')) + self.assertEqual(instances.read(self.root,115)['deployment']['resources']['cpu_units'],100) + + def test_a_concurrent_unrelated_change_is_not_overwritten_during_rollback(self): + def observe(*args): + self.file.write_bytes(self.file.read_bytes().replace(b'A=one', b'A=foreign')) + raise RuntimeError('concurrent edit') + with patch.object(instances, 'observe', side_effect=observe), patch.object(resources, 'msg_warn') as warn: + with self.assertRaisesRegex(RuntimeError, 'concurrent edit'): + self.apply() + self.assertIn(b'A=foreign', self.file.read_bytes()) + self.assertEqual(len(self.calls), 1) + warn.assert_called_once() + self.assertEqual(instances.read(self.root, 115), self.saved) + + def test_a_carried_record_error_does_not_undo_the_successful_edit(self): + with patch.object(oci_carried_record, 'carry', side_effect=RuntimeError('copy unavailable')): + self.apply() + self.assertEqual(instances.read(self.root, 115)['deployment']['resources']['memory_mb'], 768) + self.assertEqual(len(self.calls), 1) + + +class CachedArchiveTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name)/'original-installation.tar' + self.path.write_bytes(b'fixture verified by stub') + self.desired = record() + self.image = {'manifest_digest': DIGEST} + self.candidate = dict(self.image, registry_digest=DIGEST) + + def resolve(self, paths, image=None): + def run(args, error, capture=False): + self.assertNotEqual(args[0], 'skopeo', 'No layers should be downloaded') + return json.dumps(self.candidate) if '-c' in args else b'' + with patch.object(update, 'run_quiet', side_effect=run), \ + patch.object(update, 'image_from_archive', return_value=image or self.image), \ + patch.object(instances, 'command') as command: + result = update.resolve_archive(self.desired, b'arch: amd64\n', local_archives=paths) + command.assert_not_called() + return result + + def test_original_installation_archive_is_reused_without_update_cache_lookup(self): + self.assertEqual(self.resolve([self.path]), (self.path, DIGEST)) + + def test_invalid_original_cache_is_kept_and_a_second_matching_archive_is_reused(self): + other = self.path.with_name('other.tar') + other.write_bytes(b'second fixture') + def image(path): + return {'manifest_digest': NEW if str(path) == str(self.path) else DIGEST} + with patch.object(update, 'run_quiet', side_effect=lambda args, *a, **kw: json.dumps(self.candidate) if '-c' in args else b''), \ + patch.object(update, 'image_from_archive', side_effect=image), \ + patch.object(instances, 'command') as command: + result = update.resolve_archive(self.desired, b'arch: amd64\n', local_archives=[self.path, other]) + self.assertEqual(result, (other, DIGEST)) + self.assertTrue(self.path.exists()) + command.assert_not_called() + + def test_a_new_digest_downloads_once_instead_of_using_an_old_archive(self): + candidate = {'manifest_digest': NEW, 'registry_digest': NEW} + cached = self.path.with_name('update.tar') + copies = [] + def run(args, *a, **kwargs): + if '-c' in args: + return json.dumps(candidate) + if args[0] == 'skopeo': + copies.append(args) + Path(args[-1].removeprefix('oci-archive:')).write_bytes(b'new verified fixture') + return b'' + def image(path): + return {'manifest_digest': DIGEST if str(path) == str(self.path) else NEW} + with patch.object(update, 'run_quiet', side_effect=run), \ + patch.object(update, 'image_from_archive', side_effect=image), \ + patch.object(instances, 'command', return_value=str(cached).encode()), \ + patch.object(update.work_backup, 'available', return_value=1024**4), \ + patch.object(update.transaction, 'log'): + result = update.resolve_archive(self.desired, b'arch: amd64\n', local_archives=[self.path]) + self.assertEqual(result, (cached, NEW)) + self.assertEqual(len(copies), 1) + self.assertIn('docker://example/app@'+NEW, copies[0]) + self.assertEqual(self.path.read_bytes(), b'fixture verified by stub') + + def test_a_broken_or_symlink_original_cache_does_not_hide_a_valid_update_cache(self): + cached = self.path.with_name('update.tar') + cached.write_bytes(b'valid update fixture') + link = self.path.with_name('link.tar') + link.symlink_to(self.path) + verified = [] + def run(args, *a, **kwargs): + if '-c' in args: + return json.dumps(self.candidate) + self.assertNotEqual(args[0], 'skopeo') + verified.append(Path(args[-1])) + if Path(args[-1]) == self.path: + raise RuntimeError('corrupt archive') + return b'' + with patch.object(update, 'run_quiet', side_effect=run), \ + patch.object(update, 'image_from_archive', return_value=self.image), \ + patch.object(instances, 'command', return_value=str(cached).encode()): + result = update.resolve_archive(self.desired, b'arch: amd64\n', local_archives=[link, self.path]) + self.assertEqual(result, (cached, DIGEST)) + self.assertNotIn(link, verified) + self.assertTrue(link.is_symlink()) + self.assertTrue(self.path.exists()) + + +class UpdateRoutingTests(unittest.TestCase): + def run_update(self, new_image=False, resource_edit=True, reject_recreation=False): + saved = record() + candidate = copy.deepcopy(saved) + candidate['deployment']['resources']['memory_mb'] = 768 + if not resource_edit: + candidate['deployment']['environment'][0]['value'] = 'changed' + candidate['template']['container_contract']['image']['reference'] = 'example/app:other' + proposal = {'operation': 'recreate', 'candidate': candidate} + import contextlib, oci_operation_notice + with patch.object(instances, 'locked', return_value=contextlib.nullcontext()), \ + patch.object(instances, 'read', return_value=saved), \ + patch.object(instances, 'command', return_value=CONFIG), \ + patch.object(update.transaction, 'candidate_contract', return_value=candidate), \ + patch.object(update.transaction, 'external_changes', return_value={}), \ + patch.object(update.transaction, 'preflight', side_effect=ValueError('advanced Proxmox settings') if reject_recreation else None), \ + patch.object(update, 'resolve_archive', return_value=(Path('/new.tar') if new_image or not resource_edit else None, NEW if new_image else DIGEST)) as resolve, \ + patch.object(resources, 'apply') as apply_resources, \ + patch.object(update.transaction, 'apply') as apply_image, \ + patch.object(oci_operation_notice, 'operation', return_value=contextlib.nullcontext()): + update.update(115, proposal=proposal) + return resolve, apply_resources, apply_image + + def test_unchanged_image_routes_cpu_ram_edits_to_the_existing_guest(self): + resolve, native, image = self.run_update() + resolve.assert_not_called() + native.assert_called_once() + image.assert_not_called() + + def test_a_new_image_for_other_configuration_changes_uses_verified_recreation(self): + _resolve, native, image = self.run_update(new_image=True,resource_edit=False) + native.assert_not_called() + image.assert_called_once() + + def test_cpu_edits_do_not_implicitly_upgrade_the_image(self): + resolve,native,image=self.run_update(new_image=True) + resolve.assert_not_called() + native.assert_called_once() + image.assert_not_called() + + def test_homepage_resource_edit_does_not_run_the_recreation_preflight(self): + resolve,native,image=self.run_update(reject_recreation=True) + resolve.assert_not_called() + native.assert_called_once() + image.assert_not_called() + + def test_other_modifications_offer_the_recorded_archive_to_the_resolver(self): + resolve, native, image = self.run_update(resource_edit=False) + self.assertIsNone(resolve.call_args.args[2]) + self.assertEqual(resolve.call_args.kwargs['local_archives'], ['/missing/deleted-image.tar']) + native.assert_not_called() + image.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_modify_latency.py b/oci/tests/test_modify_latency.py new file mode 100644 index 00000000..f08359a5 --- /dev/null +++ b/oci/tests/test_modify_latency.py @@ -0,0 +1,103 @@ +"""Opening Modify avoids backup probes while preserving operation safeguards.""" +from pathlib import Path +import sys +import unittest +from unittest.mock import Mock,patch +ROOT=Path(__file__).resolve().parents[1] +sys.path[:0]=[str(ROOT/'src'),str(ROOT/'remote')] +from proxmenux_oci import management as menu,recreation +import oci_carried_record as carried +import oci_instances as instances +import oci_instance_reconcile as reconcile +from proxmenux_oci.ui import UserCancelled + + +def record(): + return {'vmid': 115, 'installation_id': 'test-installation', 'status': 'installed', + 'observed': {'config_sha256': 'native-config-fingerprint'}, + 'template': {'id': 'example-app', 'catalog_ui': {'title': 'Example'}, + 'container_contract': {'image': {'reference': 'example/app:latest'}}, + 'proxmox': {'installer_profile': {}}}, + 'deployment': {'vmid': 115, 'hostname': 'example', 'resources': {'cores': 2, 'memory_mb': 512}, + 'network': {'bridge': 'vmbr0', 'ipv4': 'dhcp', 'gateway': None}, + 'rootfs': {'storage': 'local-zfs', 'size_gb': 8}, 'mounts': [ + {'container_path': '/data', 'source': 'local-zfs', + 'type': 'managed-volume', 'size_gb': 8, 'backup': True}], + 'devices': [], 'security': {'unprivileged': True}, + 'environment': [{'name': 'PASSWORD', 'value': 'private-test-secret', + 'sensitive': True}]}} + + +class SelectedConfigTests(unittest.TestCase): + def test_valid_source_keyed_cache_avoids_another_pct_process(self): + with patch.object(carried,'configuration',create=True,return_value='description: current\n') as reader, \ + patch.object(instances,'command') as command: + self.assertEqual(menu._selected_configuration(115),b'description: current\n') + reader.assert_called_once_with(instances.ROOT,115) + command.assert_not_called() + + def test_old_or_unavailable_cache_falls_back_to_pct(self): + for reader in (None,Mock(return_value=None),Mock(return_value={}),Mock(side_effect=OSError('cache unavailable'))): + with self.subTest(reader=reader),patch.object(carried,'configuration',reader,create=True), \ + patch.object(instances,'command',return_value=b'live config') as command: + self.assertEqual(menu._selected_configuration(115),b'live config') + command.assert_called_once_with('pct','config','115') + + def test_identity_check_is_still_performed_on_the_selected_config(self): + r=record();r['installation_id']='11111111-1111-4111-8111-111111111111' + import contextlib + with patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(instances,'read',return_value=r), \ + patch.object(menu,'_selected_configuration',return_value=b'description: another container\n'): + self.assertEqual(menu.check_selected(ROOT,{'vmid':115})['reason'],'identity-unconfirmed') + + +class EditorLatencyTests(unittest.TestCase): + def run_editor(self,proposal=None,backup_ready=True,stale=False,adoption_error=False,approved=True,declined=False): + events=[];saved=record() + row={'vmid':115,'reason':'matched','stack':False,'pending':False,'status':'installed'} + def edit(*a,**kw): + events.append('editor') + if declined:return None + if proposal is None:raise UserCancelled() + return proposal + def backup(*a,**kw):events.append('backup');return backup_ready + ui=Mock();ui.unattended=True;ui.review.return_value=approved + with patch.object(menu,'carry_records',return_value={115:'stale'} if stale else {}) as verify, \ + patch.object(menu,'check_selected',return_value=row), \ + patch.object(menu,'_selected_configuration',return_value=b'current config'), \ + patch.object(instances,'read',return_value=saved), \ + patch.object(reconcile,'propose',side_effect=ValueError('external changes') if adoption_error else None,return_value=None), \ + patch.object(recreation,'edit_recreation',side_effect=edit), \ + patch.object(menu,'_work_backup_ready',side_effect=backup), \ + patch.object(menu,'_run_lifecycle',side_effect=lambda *a:events.append('lifecycle') or True): + try:result=menu.manage_instance(ROOT,ui,row,action='modify') + except UserCancelled:result=False + verify.assert_called_once_with(ROOT,vmids=[115],verify=True) + return result,events + + def test_open_and_cancel_has_no_backup_or_storage_probes(self): + result,events=self.run_editor() + self.assertFalse(result);self.assertEqual(events,['editor']) + + def test_declining_the_review_has_no_backup_or_storage_probes(self): + # The review is part of the Modify menu: declining it and leaving the + # menu returns no proposal, so no probe or operation follows. + result,events=self.run_editor(declined=True) + self.assertFalse(result);self.assertEqual(events,['editor']) + + def test_confirmed_edit_keeps_backup_check_before_the_lifecycle(self): + result,events=self.run_editor({'operation':'recreate','candidate':record()}) + self.assertTrue(result);self.assertEqual(events,['editor','backup','lifecycle']) + + def test_missing_backup_space_still_prevents_the_lifecycle(self): + result,events=self.run_editor({'operation':'recreate','candidate':record()},backup_ready=False) + self.assertFalse(result);self.assertEqual(events,['editor','backup']) + + def test_restored_or_conflicting_instance_cannot_enter_editor(self): + for options in ({'stale':True},{'adoption_error':True}): + result,events=self.run_editor(**options) + self.assertFalse(result);self.assertEqual(events,[]) + + +if __name__=='__main__':unittest.main() diff --git a/oci/tests/test_modify_menu.py b/oci/tests/test_modify_menu.py new file mode 100644 index 00000000..0f80a0a7 --- /dev/null +++ b/oci/tests/test_modify_menu.py @@ -0,0 +1,223 @@ +"""Selective OCI edits stay in a draft until reviewed, then use one transaction.""" +import copy +import json +from pathlib import Path +import stat +import sys +import unittest +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'src')) +sys.path.insert(0, str(ROOT / 'remote')) + +from proxmenux_oci import management, recreation +from proxmenux_oci.ui import BackRequested, UserCancelled + + +def record(): + return {'vmid': 115, 'installation_id': 'test-installation', 'status': 'installed', + 'observed': {'config_sha256': 'native-config-fingerprint'}, + 'template': {'id': 'example-app', 'catalog_ui': {'title': 'Example'}, + 'container_contract': {'image': {'reference': 'example/app:latest'}}, + 'proxmox': {'installer_profile': {}}}, + 'deployment': {'vmid': 115, 'hostname': 'example', 'resources': {'cores': 2, 'memory_mb': 512}, + 'network': {'bridge': 'vmbr0', 'ipv4': 'dhcp', 'gateway': None}, + 'rootfs': {'storage': 'local-zfs', 'size_gb': 8}, 'mounts': [ + {'container_path': '/data', 'source': 'local-zfs', + 'type': 'managed-volume', 'size_gb': 8, 'backup': True}], + 'devices': [], 'security': {'unprivileged': True}, + 'environment': [{'name': 'PASSWORD', 'value': 'private-test-secret', + 'sensitive': True}]}} + + +class MenuUI: + def __init__(self, choices, answers=(), reviews=(True,), confirms=()): + self.choices, self.answers = iter(choices), iter(answers) + self.reviews, self.confirms = iter(reviews), iter(confirms) + self.events = [] + self.back_enabled = False + + def choose(self, text, options, default=None, **kwargs): + self.events.append(('menu', text, dict(options))) + value = next(self.choices) + if value is not None: + assert value in dict(options), value + return value + + def ask(self, text, default=None, **kwargs): + self.events.append(('ask', text, default)) + value = next(self.answers) + if isinstance(value, Exception): + raise value + return value + + def confirm(self, text, default=False): + self.events.append(('confirm', text)) + return next(self.confirms) + + def review(self, text, *args, **kwargs): + self.events.append(('review', text)) + return next(self.reviews) + + def message(self, text, *args): + self.events.append(('message', text)) + + +@patch('proxmenux_oci.i18n.language', return_value='en') +class ModifyMenuTests(unittest.TestCase): + def test_ram_only_does_not_ask_about_cpu_network_paths_devices_or_catalog(self, _language): + original = record() + saved = copy.deepcopy(original) + ui = MenuUI(['memory', 'apply'], ['768']) + with patch.object(recreation, 'refresh_template') as refresh: + result = recreation.edit_recreation(original, ui) + self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 2, 'memory_mb': 768}) + self.assertEqual([e[1] for e in ui.events if e[0] == 'ask'], ['RAM in MiB']) + self.assertEqual(result['base_config_sha256'], 'native-config-fingerprint') + self.assertEqual(original, saved) + refresh.assert_not_called() + self.assertFalse(ui.back_enabled) + + def test_several_sections_and_revisits_keep_the_current_draft(self, _language): + ui = MenuUI(['memory', 'cores', 'memory', 'apply'], ['768', '3', '1024']) + result = recreation.edit_recreation(record(), ui) + self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 1024}) + self.assertEqual([e[2] for e in ui.events if e[0] == 'ask'], ['512', '2', '768']) + self.assertEqual(sum(e[0] == 'review' for e in ui.events), 1) + + def test_declining_review_returns_to_menu_without_losing_edits(self, _language): + ui = MenuUI(['cores', 'apply', 'memory', 'apply'], ['3', '768'], reviews=[False, True]) + result = recreation.edit_recreation(record(), ui) + self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 768}) + self.assertEqual(sum(e[0] == 'review' for e in ui.events), 2) + + def test_unchanged_apply_does_not_propose_recreating_the_guest(self, _language): + ui = MenuUI(['memory', 'apply'], ['512']) + self.assertIsNone(recreation.edit_recreation(record(), ui)) + self.assertFalse(any(e[0] == 'review' for e in ui.events)) + self.assertIn(('message', 'Nothing was changed.'), ui.events) + + def test_discard_leaves_the_saved_record_untouched(self, _language): + original = record() + saved = copy.deepcopy(original) + ui = MenuUI(['memory', 'discard'], ['768'], confirms=[True]) + self.assertIsNone(recreation.edit_recreation(original, ui)) + self.assertEqual(original, saved) + self.assertFalse(any(e[0] == 'review' for e in ui.events)) + + def test_declining_discard_preserves_the_draft(self, _language): + ui = MenuUI(['memory', None, 'apply'], ['768'], confirms=[False]) + result = recreation.edit_recreation(record(), ui) + self.assertEqual(result['candidate']['deployment']['resources']['memory_mb'], 768) + + def test_cancel_without_changes_asks_nothing_else(self, _language): + ui = MenuUI([None]) + self.assertIsNone(recreation.edit_recreation(record(), ui)) + self.assertEqual(len(ui.events), 1) + + def test_invalid_ram_is_retried_without_restarting_other_sections(self, _language): + ui = MenuUI(['cores', 'memory', 'apply'], ['3', 'invalid', '64', '768']) + result = recreation.edit_recreation(record(), ui) + self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 768}) + self.assertEqual(sum(e[0] == 'message' for e in ui.events), 2) + self.assertEqual(sum(e[:2] == ('ask', 'Cores') for e in ui.events), 1) + + def test_back_from_first_field_returns_to_menu_and_keeps_other_edits(self, _language): + ui = MenuUI(['cores', 'memory', 'apply'], ['3', BackRequested()]) + result = recreation.edit_recreation(record(), ui) + self.assertEqual(result['candidate']['deployment']['resources'], {'cores': 3, 'memory_mb': 512}) + self.assertFalse(ui.back_enabled) + + def test_cancelling_a_partly_edited_section_rolls_back_only_that_section(self, _language): + real_edit = recreation.edit_setting + def edit(candidate, ui, setting): + if setting == 'environment': + candidate['deployment']['environment'][0]['value'] = 'partial-value' + raise UserCancelled() + real_edit(candidate, ui, setting) + ui = MenuUI(['cores', 'environment', 'apply'], ['3']) + with patch.object(recreation, 'edit_setting', side_effect=edit): + result = recreation.edit_recreation(record(), ui) + self.assertEqual(result['candidate']['deployment']['resources']['cores'], 3) + self.assertEqual(result['candidate']['deployment']['environment'], record()['deployment']['environment']) + + def test_menu_and_review_do_not_expose_a_saved_secret(self, _language): + ui = MenuUI(['cores', 'apply'], ['3']) + recreation.edit_recreation(record(), ui) + self.assertNotIn('private-test-secret', json.dumps(ui.events)) + + def test_extra_paths_counts_only_paths_added_by_hand(self, _language): + candidate = record() + self.assertTrue(dict(recreation.modification_options(candidate))['paths'].rstrip().endswith(' 0')) + candidate['deployment']['mounts'].append({'container_path': '/media-extra', 'type': 'host-bind', + 'source': '/mnt/media', 'custom': True}) + self.assertTrue(dict(recreation.modification_options(candidate))['paths'].rstrip().endswith(' 1')) + + def test_optional_categories_follow_the_application_profile(self, _language): + candidate = record() + options = dict(recreation.modification_options(candidate)) + self.assertTrue({'cores', 'memory', 'network', 'paths', 'devices', 'environment', 'catalog', 'apply'} <= options.keys()) + self.assertFalse({'shares', 'acceleration', 'detector'} & options.keys()) + candidate['template']['proxmox']['installer_profile'] = { + 'shared_directories': {'base_path': '/shares'}, 'object_detector': {'devices': []}, + 'hardware_acceleration': {'profiles': [{'id': 'intel', 'label': 'Intel GPU'}]}} + candidate['deployment']['hardware_profile'] = 'intel' + options = dict(recreation.modification_options(candidate)) + self.assertTrue({'shares', 'acceleration', 'detector'} <= options.keys()) + self.assertIn('Intel GPU', options['acceleration']) + + +class ModifyDispatchTests(unittest.TestCase): + def call_manager(self, proposal, lifecycle, ui=None, offer=None): + import oci_instances + import oci_instance_reconcile + saved = record() + row = {'vmid': 115, 'reason': 'matched', 'stack': False, 'pending': False, 'status': 'installed'} + with patch.object(management.images, 'offer_removal', offer or Mock()), \ + patch.object(management, 'carry_records', return_value={}), \ + patch.object(management, 'check_selected', return_value=row), \ + patch.object(management, '_work_backup_ready', return_value=True), \ + patch.object(oci_instances, 'read', return_value=saved), \ + patch.object(oci_instances, 'command', return_value='hostname: example'), \ + patch.object(oci_instance_reconcile, 'propose', return_value=None), \ + patch.object(recreation, 'edit_recreation', return_value=proposal), \ + patch.object(management, '_run_lifecycle', side_effect=lifecycle): + return management.manage_instance(ROOT, ui or Mock(), row, action='modify') + + def test_cancel_does_not_start_a_lifecycle_transaction(self): + lifecycle = Mock() + self.assertFalse(self.call_manager(None, lifecycle)) + lifecycle.assert_not_called() + + def test_apply_sends_one_private_proposal_to_the_existing_backend(self): + proposal = {'operation': 'recreate', 'candidate': record(), 'base_config_sha256': 'fingerprint'} + seen = [] + def lifecycle(command, title): + path = Path(command[command.index('--proposal') + 1]) + seen.append(json.loads(path.read_text())) + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600) + self.assertNotIn('private-test-secret', ' '.join(command)) + self.assertIn('oci_update_current.py', command[1]) + self.assertIn('--acknowledge-external-data', command) + return True + self.assertTrue(self.call_manager(proposal, lifecycle)) + self.assertEqual(seen, [proposal]) + + def test_the_image_question_only_follows_a_recreated_container(self): + proposal = {'operation': 'recreate', 'candidate': record(), 'base_config_sha256': 'fingerprint'} + ui = Mock(unattended=False) + for recreated in (False, True): + with self.subTest(recreated=recreated): + def lifecycle(command, title): + if recreated: + Path(management.os.environ['OCI_RECREATED_MARKER']).touch() + return True + offer = Mock() + self.assertTrue(self.call_manager(proposal, lifecycle, ui, offer)) + self.assertEqual(offer.called, recreated) + self.assertNotIn('OCI_RECREATED_MARKER', management.os.environ) + + +if __name__ == '__main__': + unittest.main() diff --git a/oci/tests/test_modify_review.py b/oci/tests/test_modify_review.py new file mode 100644 index 00000000..e9d4e41d --- /dev/null +++ b/oci/tests/test_modify_review.py @@ -0,0 +1,219 @@ +"""Maintainer review: fallback, complete mount edits and retained unused disks.""" +import contextlib +import copy +from pathlib import Path +import re +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'remote')) +import oci_instances as instances +import oci_instance_transaction as transaction +import oci_native_modify as native +import oci_update_current as update +import oci_operation_notice +from test_modify_image_reuse import record, CONFIG + + +def installation(): + result=record() + result['deployment'].update(rootfs={'storage':'local-zfs','size_gb':8}, onboot=True, + security={'unprivileged':True}) + result['observed']['image']['defaults']={} + config=CONFIG+b'unprivileged: 1\nonboot: 1\nnet0: name=eth0,bridge=vmbr0,ip=dhcp,hwaddr=AA:BB:CC:DD:EE:FF\n' + result['observed'].update(config=config.decode(), config_sha256=transaction.sha(config)) + return result,config + + +class FallbackTests(unittest.TestCase): + def test_all_installer_adaptations_fall_back_before_any_native_write(self): + saved,config=installation() + def nvidia(c):c['deployment']['devices']=[{'kind':'nvidia-runtime','host_path':'/dev/nvidia0'}] + def groups(c):c['deployment']['devices']=[{'host_path':'/dev/dri/renderD128','append_host_device_gid_to_environment':'GROUPS'}] + def permissions(c): + c['deployment']['devices']=[{'host_path':'/dev/dri/renderD128'}] + c['deployment']['device_permissions']={'uid':1000} + mutations=[nvidia,groups,permissions, + lambda c:c['deployment'].update(completion_notes=['Installer note']), + lambda c:c['deployment'].update(post_start_configurations=[{'id':'adapter'}]), + lambda c:c['template']['catalog_ui'].update(title='Refreshed catalog template'), + lambda c:c['deployment'].update(mounts=[{'type':'host-bind','source':'/host/file','container_path':'/config/file','backup':False,'size_gb':None}])] + for mutation in mutations: + with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as folder: + candidate=copy.deepcopy(saved);mutation(candidate) + root=Path(folder);instances.write(instances.location(root,115),saved) + with patch.object(instances,'ROOT',root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(instances,'command',return_value=config), \ + patch.object(transaction,'external_changes',return_value={}),patch.object(transaction,'preflight'), \ + patch.object(transaction,'is_file_bind',side_effect=lambda m:m.get('source')=='/host/file'), \ + patch.object(native,'apply',side_effect=AssertionError('partial native apply')), \ + patch.object(update,'resolve_archive',return_value=(Path('/cached.tar'),'sha256:resolved')) as resolve, \ + patch.object(transaction,'apply') as recreate, \ + patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()): + update.update(115,proposal={'operation':'recreate','candidate':candidate}) + recreate.assert_called_once();resolve.assert_called_once() + self.assertEqual(resolve.call_args.kwargs['local_archives'],[saved['observed']['archive_path']]) + + def test_native_validation_failure_never_falls_back_to_recreation(self): + saved,config=installation();candidate=copy.deepcopy(saved) + candidate['deployment']['environment']=[{'name':'INVALID NAME','value':'value'}] + with tempfile.TemporaryDirectory() as folder: + root=Path(folder);instances.write(instances.location(root,115),saved) + with patch.object(instances,'ROOT',root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(instances,'command',return_value=config), \ + patch.object(transaction,'preflight'),patch.object(transaction,'external_changes',return_value={}), \ + patch.object(transaction,'apply') as recreate,patch.object(update,'resolve_archive') as resolve, \ + patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()): + with self.assertRaises(ValueError):update.update(115,proposal={'operation':'recreate','candidate':candidate}) + recreate.assert_not_called();resolve.assert_not_called() + + def test_native_api_failure_never_falls_back_after_starting_the_edit(self): + saved,config=installation();candidate=copy.deepcopy(saved) + candidate['deployment']['environment']=[{'name':'VALID','value':'value'}] + with tempfile.TemporaryDirectory() as folder: + root=Path(folder);instances.write(instances.location(root,115),saved) + with patch.object(instances,'ROOT',root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(instances,'command',return_value=config), \ + patch.object(transaction,'preflight'),patch.object(transaction,'external_changes',return_value={}), \ + patch.object(transaction,'apply') as recreate,patch.object(update,'resolve_archive') as resolve, \ + patch.object(native,'apply',side_effect=RuntimeError('native API failed')), \ + patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()): + with self.assertRaisesRegex(RuntimeError,'native API failed'): + update.update(115,proposal={'operation':'recreate','candidate':candidate}) + recreate.assert_not_called();resolve.assert_not_called() + + +class MountTests(unittest.TestCase): + def setUp(self): + self.record,self.config=installation() + self.mount={'type':'host-bind','source':'/mnt/old','container_path':'/media', + 'size_gb':None,'backup':False,'read_only':False,'create_if_missing':False} + self.record['deployment']['mounts']=[self.mount] + self.config+=b'mp3: /mnt/old,mp=/media,backup=0,ro=0,replicate=0\n' + self.record['observed'].update(config=self.config.decode(),config_sha256=transaction.sha(self.config)) + self.candidate=copy.deepcopy(self.record) + + def test_existing_bind_source_and_flags_are_applied_with_other_options_preserved(self): + self.candidate['deployment']['mounts'][0].update(source='/mnt/new',read_only=True) + with patch.object(Path,'is_dir',return_value=True): + plan=native.plan(self.record,self.candidate,self.config) + self.assertEqual(plan['values'],{'mp3':'/mnt/new,mp=/media,backup=0,ro=1,replicate=0'}) + self.assertTrue(plan['restart']);self.assertEqual(plan['delete'],[]) + + def test_missing_replacement_directory_is_rejected_unless_creation_was_requested(self): + self.candidate['deployment']['mounts'][0]['source']='/mnt/new' + with patch.object(Path,'is_dir',return_value=False): + with self.assertRaisesRegex(ValueError,'directory does not exist'): + native.plan(self.record,self.candidate,self.config) + self.candidate['deployment']['mounts'][0]['create_if_missing']=True + self.assertIn('/mnt/new,',native.plan(self.record,self.candidate,self.config)['values']['mp3']) + + def test_verification_detects_a_source_that_was_not_applied(self): + operation={'values':{'mp3':'/mnt/new,mp=/media,backup=0,ro=0'},'delete':[]} + with self.assertRaisesRegex(ValueError,'could not apply'):native.verify_values(self.config,operation) + native.verify_values(self.config.replace(b'/mnt/old',b'/mnt/new'),operation) + + def test_unknown_mount_options_and_permission_grants_use_installer(self): + for fields in ({'grant_access':True},{'mount_options':['noexec']},{'type':'managed-volume','source':'local-zfs','size_gb':2,'backup':True}): + candidate=copy.deepcopy(self.candidate);candidate['deployment']['mounts'][0].update(fields) + self.assertIsNone(native.changes(self.record,candidate,self.config)) + + def test_recursive_bind_adaptations_use_installer(self): + self.candidate['deployment']['mounts'][0]['read_only']=True + derived=b'lxc.mount.entry: /mnt/old media none rbind,rslave,create=dir 0 0\n' + self.assertIsNone(native.changes(self.record,self.candidate,self.config+derived)) + + def test_storage_and_disk_size_changes_keep_existing_migration_guard(self): + self.record['deployment']['mounts'][0].update(type='managed-volume',source='local-zfs',size_gb=1,backup=True) + self.config=self.config.replace(b'/mnt/old,mp=/media,backup=0',b'local-zfs:subvol-115-disk-1,mp=/media,backup=1') + self.record['observed']['config']=self.config.decode() + for fields in ({'size_gb':2},{'source':'other-storage'}): + candidate=copy.deepcopy(self.record);candidate['deployment']['mounts'][0].update(fields) + self.assertIsNone(native.changes(self.record,candidate,self.config)) + with self.assertRaisesRegex(ValueError,'migration'):transaction.preflight(self.record,candidate,self.config) + + +class DetachedPreflightTests(unittest.TestCase): + def test_removing_a_mount_then_updating_accepts_the_recorded_unused_disk(self): + saved,config=installation() + saved['deployment']['mounts']=[{'type':'managed-volume','source':'local-zfs','size_gb':1, + 'container_path':'/extra','backup':True,'read_only':False}] + config+=b'mp4: local-zfs:subvol-115-disk-1,mp=/extra,backup=1,size=1G\n' + saved['observed']['config']=config.decode() + candidate=copy.deepcopy(saved);candidate['deployment']['mounts']=[] + self.assertEqual(native.plan(saved,candidate,config)['delete'],['mp4']) + detached=config.replace(b'mp4: local-zfs:subvol-115-disk-1,mp=/extra,backup=1,size=1G\n',b'unused0: local-zfs:subvol-115-disk-1\n') + candidate['observed'].update(config=detached.decode(),config_sha256=transaction.sha(detached)) + cfg,_,_=transaction.preflight(candidate,copy.deepcopy(candidate),detached) + self.assertIn('unused0',cfg) + + def test_unrecorded_detached_disks_are_not_adopted_silently(self): + saved,config=installation() + with self.assertRaisesRegex(ValueError,'disks do not match|discard those changes'): + transaction.preflight(saved,copy.deepcopy(saved),config+b'unused0: local-zfs:subvol-115-disk-1\n') + + def test_invalid_duplicate_or_mounted_unused_disks_are_rejected(self): + for extra in (b'unused0: /etc/passwd\n',b'unused0: local-zfs:subvol-115-disk-0\n', + b'unused0: local-zfs:subvol-115-disk-1\nunused1: local-zfs:subvol-115-disk-1\n'): + saved,config=installation();config+=extra;saved['observed']['config']=config.decode() + with self.assertRaisesRegex(ValueError,'disks do not match'):transaction.preflight(saved,copy.deepcopy(saved),config) + + +class DetachedTransferTests(unittest.TestCase): + def setUp(self): + saved,config=installation() + self.configs={115:config+b'unused0: local-zfs:subvol-115-disk-1\nunused2: local-zfs:subvol-115-disk-2\n', + 900:b'description: proxmenux-transaction=test-tx\nrootfs: local-zfs:subvol-900-disk-0\n'} + self.state={'vmid':115,'stage':900,'id':'test-tx','record':saved, + 'retained_unused':transaction.unused_volumes(self.configs[115])} + self.calls=[] + def owned(vmid,marker): + self.assertIn(marker.encode(),self.configs[vmid]);return self.configs[vmid] + def run(*args): + self.calls.append(args);self.assertEqual(args[:2],('pct','move-volume')) + source,key,target=int(args[2]),args[3],int(args[5]);self.assertEqual(key,args[7]) + line=next(line for line in self.configs[source].splitlines(keepends=True) if line.startswith((key+': ').encode())) + self.configs[source]=self.configs[source].replace(line,b'') + self.configs[target]+=line.replace(('subvol-'+str(source)+'-').encode(),('subvol-'+str(target)+'-').encode()) + for p in (patch.object(transaction,'owned',side_effect=owned),patch.object(transaction,'run',side_effect=run)): + p.start();self.addCleanup(p.stop) + + def test_unused_disks_are_parked_and_returned_without_freeing_any_volume(self): + transaction.move_retained_unused(self.state,True) + self.assertEqual(transaction.unused_volumes(self.configs[115]),{}) + self.assertEqual(set(transaction.unused_volumes(self.configs[900])),{'unused0','unused2'}) + transaction.move_retained_unused(self.state,False) + self.assertEqual(transaction.unused_volumes(self.configs[115]),self.state['retained_unused']) + self.assertEqual(transaction.unused_volumes(self.configs[900]),{}) + self.assertEqual(len(self.calls),4) + + def test_an_interrupted_transfer_can_be_retried_in_both_directions(self): + original=transaction.run.side_effect + def interrupt(*args): + original(*args);raise RuntimeError('interrupted after reassignment') + with patch.object(transaction,'run',side_effect=interrupt): + with self.assertRaises(RuntimeError):transaction.move_retained_unused(self.state,True) + transaction.move_retained_unused(self.state,True) + with patch.object(transaction,'run',side_effect=interrupt): + with self.assertRaises(RuntimeError):transaction.move_retained_unused(self.state,False) + transaction.move_retained_unused(self.state,False) + self.assertEqual(transaction.unused_volumes(self.configs[115]),self.state['retained_unused']) + + def test_unrelated_unused_disks_are_not_moved(self): + self.configs[115]+=b'unused3: local-zfs:subvol-115-disk-3\n' + with self.assertRaises(ValueError):transaction.move_retained_unused(self.state,True) + self.assertEqual(self.calls,[]) + + def test_a_missing_disk_or_target_collision_blocks_the_transfer(self): + del self.state['retained_unused']['unused2'] + self.configs[115]=self.configs[115].replace(b'unused2: local-zfs:subvol-115-disk-2\n',b'') + self.configs[900]+=b'unused0: local-zfs:subvol-900-disk-9\n' + with self.assertRaises(ValueError):transaction.move_retained_unused(self.state,True) + self.configs[115]=self.configs[115].replace(b'unused0: local-zfs:subvol-115-disk-1\n',b'') + self.configs[900]=self.configs[900].replace(b'unused0: local-zfs:subvol-900-disk-9\n',b'') + with self.assertRaises(ValueError):transaction.move_retained_unused(self.state,False) + + +if __name__=='__main__':unittest.main() diff --git a/oci/tests/test_modify_review_transaction.py b/oci/tests/test_modify_review_transaction.py new file mode 100644 index 00000000..08bf79d3 --- /dev/null +++ b/oci/tests/test_modify_review_transaction.py @@ -0,0 +1,148 @@ +"""Exercise the actual update and recovery orchestration with a simulated PVE API. + +Unused volumes must leave the old CT before destroy/restore and return before +validation/commit, including an interruption during volume reassignment. +""" +import copy +import json +from pathlib import Path +import re +import socket +import sys +import tempfile +import unittest +from unittest.mock import patch + +sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'remote')) +import oci_instances as instances +import oci_instance_transaction as transaction +from test_modify_review import installation +from test_modify_image_reuse import NEW + + +class UpdateRetainedDiskTests(unittest.TestCase): + def setUp(self): + temp=tempfile.TemporaryDirectory();self.addCleanup(temp.cleanup) + self.root=Path(temp.name)/'instances' + self.archive=Path(temp.name)/'image.tar';self.archive.write_bytes(b'verified fixture') + self.backup=Path(temp.name)/'backup.tar.zst';self.backup.write_bytes(b'verified backup') + self.saved,config=installation() + self.base_config=config + config+=b'unused0: local-zfs:subvol-115-disk-1\nunused2: local-zfs:subvol-115-disk-2\n' + self.saved['observed'].update(config=config.decode(),config_sha256=transaction.sha(config)) + self.saved['observed']['archive_path']=str(self.archive) + self.image=copy.deepcopy(self.saved['observed']['image']);self.image['manifest_digest']=NEW + self.configs={115:config};self.commands=[];self.fail_install=False;self.interrupt_return=False + instances.write(instances.location(self.root,115),self.saved) + def config_line(vmid,key,value): + before=self.configs[vmid] + result=b''.join(line for line in before.splitlines(keepends=True) if not line.startswith((key+': ').encode())) + self.configs[vmid]=result+(key+': '+value+'\n').encode() + def run(*args): + self.commands.append(args) + if args[0]=='zstd': + self.assertEqual(args[1:],('-t',str(self.backup))) + return b'' + if args[:2]==('pvesh','get'): + if args[2]=='/cluster/nextid':return b'900' + return json.dumps([{'vmid':vmid,'type':'lxc','node':socket.gethostname().split('.')[0]} + for vmid in self.configs]).encode() if args[2]=='/cluster/resources' else b'[]' + self.assertEqual(args[0],'pct',args) + action,vmid=args[1],int(args[2]) + if action=='config':return self.configs[vmid] + if action=='status':return b'status: running' + if action in ('start','shutdown','stop'):return b'' + if action=='create': + marker=args[args.index('--description')+1] + self.configs[vmid]=f'description: {marker}\nrootfs: local-zfs:subvol-{vmid}-disk-0\n'.encode() + elif action=='destroy': + self.assertFalse(transaction.unused_volumes(self.configs[vmid]),'would destroy retained data') + self.assertFalse(transaction.mounts(self.configs[vmid]),'would destroy attached data') + del self.configs[vmid] + elif action=='set': + for index in range(3,len(args),2):config_line(vmid,args[index][2:],args[index+1]) + elif action=='move-volume': + key,target,target_key=args[3],int(args[5]),args[7] + volume=transaction.unused_volumes(self.configs[vmid])[key] + line=(key+': '+volume+'\n').encode() + self.configs[vmid]=self.configs[vmid].replace(line,b'') + volume=volume.replace('subvol-'+str(vmid)+'-','subvol-'+str(target)+'-') + config_line(target,target_key,volume) + if self.interrupt_return and target==115: + self.interrupt_return=False + raise RuntimeError('interrupted return after PVE reassignment') + elif action=='restore': + # vzdump does not contain detached disks; restore skips unusedN. + self.assertFalse(transaction.unused_volumes(self.configs.get(vmid,b''))) + self.configs[vmid]=self.base_config + else:self.fail('Unexpected PVE command: '+str(args)) + return b'' + def install(root,journal,state,progress=None): + if self.fail_install:raise RuntimeError('installer failed after old rootfs was removed') + self.configs[state['vmid']]=self.base_config + def observe(vmid,identity,archive,digest,image=None): + observed=copy.deepcopy(self.saved['observed']) + observed.update(config=self.configs[vmid].decode(),config_sha256=transaction.sha(self.configs[vmid]), + image=copy.deepcopy(image or self.image),resolved_registry_digest=digest) + return observed + original_exists=Path.exists + def exists(path): + match=re.fullmatch(r'/etc/pve/lxc/([0-9]+)\.conf',str(path)) + return int(match[1]) in self.configs if match else original_exists(path) + patches=[patch.object(transaction,'run',side_effect=run),patch.object(transaction,'open_log'), + patch.object(transaction,'log'),patch.object(transaction,'stop'), + patch.object(transaction,'check_archive'),patch.object(transaction,'image_from_archive',return_value=self.image), + patch.object(transaction,'verified_backup',return_value=self.backup), + patch.object(transaction,'install_candidate',side_effect=install), + patch.object(transaction,'healthcheck',return_value='running'), + patch.object(transaction,'candidate_host_sources',return_value={}), + patch.object(transaction,'prune_backups'),patch.object(transaction.image_cache,'prune',return_value=[]), + patch.object(transaction,'cleanup_restored_format_dirs'),patch.object(transaction.runtime_settings,'restore'), + patch.object(transaction.work_backup,'locate',side_effect=lambda directory,*a:directory), + patch.object(instances,'observe',side_effect=observe),patch.object(Path,'exists',exists)] + for p in patches:p.start();self.addCleanup(p.stop) + self.addCleanup(transaction._run.update,journal=None) + + def apply(self,**kwargs):return transaction.apply(self.root,115,self.archive,'update',registry_digest=NEW,**kwargs) + + def journal(self):return next(instances.location(self.root,115).parent.glob('transactions/*/transaction.json')) + + def assert_retained(self): + self.assertEqual(transaction.unused_volumes(self.configs[115]),{ + 'unused0':'local-zfs:subvol-115-disk-1','unused2':'local-zfs:subvol-115-disk-2'}) + self.assertNotIn(900,self.configs) + self.assertFalse(any(args[:2]==('pct','set') and '--delete' in args for args in self.commands)) + + def test_update_keeps_detached_disks_and_the_next_update_preflight_accepts_them(self): + self.apply();self.assert_retained() + saved=instances.read(self.root,115) + self.assertEqual(saved['status'],'installed') + self.assertEqual(saved['observed']['image']['manifest_digest'],NEW) + transaction.preflight(saved,copy.deepcopy(saved),self.configs[115]) + self.assertEqual(json.loads(self.journal().read_text())['phase'],'committed') + + def test_failed_install_restores_both_backup_and_detached_disks(self): + self.fail_install=True + with self.assertRaisesRegex(RuntimeError,'installer failed'):self.apply() + transaction.recover(self.root,self.journal()) + self.assert_retained() + self.assertEqual(instances.read(self.root,115)['observed']['image'],self.saved['observed']['image']) + self.assertEqual(json.loads(self.journal().read_text())['phase'],'rolled-back') + + def test_recovery_after_parking_data_returns_the_original_unused_disks(self): + with self.assertRaisesRegex(RuntimeError,'Lab interruption'):self.apply(interrupt_after='data-parked') + transaction.recover(self.root,self.journal());self.assert_retained() + + def test_recovery_handles_an_interruption_while_returning_unused_disks(self): + self.interrupt_return=True + with self.assertRaisesRegex(RuntimeError,'interrupted return'):self.apply() + transaction.recover(self.root,self.journal());self.assert_retained() + + def test_failed_runtime_check_recovers_disks_already_returned_to_the_candidate(self): + with patch.object(transaction.gpu_devices,'validate_runtime',side_effect=[RuntimeError('invalid runtime'), None]): + with self.assertRaisesRegex(RuntimeError,'invalid runtime'):self.apply() + transaction.recover(self.root,self.journal()) + self.assert_retained() + + +if __name__=='__main__':unittest.main() diff --git a/oci/tests/test_native_modify.py b/oci/tests/test_native_modify.py new file mode 100644 index 00000000..cf42493e --- /dev/null +++ b/oci/tests/test_native_modify.py @@ -0,0 +1,262 @@ +"""Native runtime edits keep rootfs, defaults and secrets; failures roll back.""" +import contextlib +import copy +import json +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch, Mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'remote')) +import oci_native_modify as native +import oci_instances as instances +import oci_update_current as update +import oci_operation_notice +from test_modify_image_reuse import record, CONFIG + + +class NativePlanTests(unittest.TestCase): + def setUp(self): + self.record = record() + self.record['observed']['image']['defaults'] = {'Env': ['A=image-default']} + self.record['deployment']['environment'] = [{'name': 'A', 'value': 'one'}] + self.config = CONFIG+b'net0: name=eth0,bridge=vmbr0,ip=dhcp,hwaddr=AA:BB:CC:DD:EE:FF,tag=20,ip6=auto\n' + self.candidate = copy.deepcopy(self.record) + p = patch.object(native.transaction, 'preflight') + self.preflight = p.start(); self.addCleanup(p.stop) + + def test_environment_preserves_image_defaults_and_other_runtime_values(self): + self.candidate['deployment']['environment'] = [{'name': 'SECRET', 'value': "a' $x \" ; = value", 'sensitive': True}] + result = native.plan(self.record, self.candidate, self.config) + env = dict(x.split('=', 1) for x in result['values']['env'].split('\0')) + self.assertEqual(env, {'A': 'image-default', 'B': 'two', 'SECRET': "a' $x \" ; = value"}) + self.assertTrue(result['restart']) + + def test_remove_environment_without_default_restores_image_environment(self): + self.record['observed']['image']['defaults'] = {} + self.candidate['deployment']['environment'] = [] + self.assertEqual(native.plan(self.record, self.candidate, self.config)['values']['env'], 'B=two') + + def test_public_pct_environment_and_native_lines_have_the_same_values(self): + raw=b'lxc.environment.runtime: A=one\nlxc.environment.runtime: B=two\n' + public=b'env: A=one\0B=two\n' + self.assertEqual(native.environment(raw),native.environment(public)) + self.assertEqual(native.environment(public),{'A':'one','B':'two'}) + + def test_duplicate_invalid_and_control_character_values_are_refused(self): + for items in ([{'name':'X','value':'a'},{'name':'X','value':'b'}], + [{'name':'X','value':'a\nb'}],[{'name':'BAD NAME','value':'a'}]): + self.candidate['deployment']['environment'] = items + with self.assertRaises(ValueError): native.plan(self.record, self.candidate, self.config) + + def test_network_preserves_mac_vlan_ipv6_and_interface_identity(self): + self.candidate['deployment']['network'].update(ipv4='192.0.2.2/24', gateway='192.0.2.1') + with patch.object(Path, 'is_dir', return_value=True): + result = native.plan(self.record, self.candidate, self.config) + fields = dict(x.split('=',1) for x in result['values']['net0'].split(',')) + self.assertEqual(fields['hwaddr'], 'AA:BB:CC:DD:EE:FF') + self.assertEqual(fields['tag'], '20'); self.assertEqual(fields['ip6'], 'auto') + self.assertEqual(fields['ip'], '192.0.2.2/24'); self.assertEqual(fields['gw'], '192.0.2.1') + + def test_invalid_network_is_rejected_before_stopping(self): + for value in ('nonsense', '2001:db8::1/64'): + self.candidate['deployment']['network']['ipv4'] = value + with patch.object(Path, 'is_dir', return_value=True), self.assertRaises(ValueError): + native.plan(self.record,self.candidate,self.config) + + def test_new_managed_disk_uses_native_allocation_and_existing_disk_is_untouched(self): + self.candidate['deployment']['mounts'] = [{'type':'managed-volume','container_path':'/extra', + 'source':'local-zfs','size_gb':1,'backup':True,'read_only':False}] + result = native.plan(self.record,self.candidate,self.config) + self.assertEqual(result['values'], {'mp0':'local-zfs:1,mp=/extra,backup=1,ro=0'}) + self.assertNotIn('rootfs', result['values']) + + def test_removing_mount_detaches_it_without_freeing_its_volume(self): + mount={'type':'managed-volume','container_path':'/extra','source':'local-zfs','size_gb':1,'backup':True} + self.record['deployment']['mounts']=[mount] + result=native.plan(self.record,self.candidate,self.config+b'mp4: local-zfs:subvol-115-disk-1,mp=/extra,backup=1,size=1G\n') + self.assertEqual(result['delete'],['mp4']) + + def test_image_or_installer_adapter_changes_do_not_take_native_path(self): + for key in ('image', 'adapter'): + candidate=copy.deepcopy(self.record) + if key=='image':candidate['template']['container_contract']['image']['reference']='new/image' + else:candidate['deployment']['post_start_configurations']=[{'id':'rootfs-adapter'}] + self.assertIsNone(native.changes(self.record,candidate,self.config)) + + def test_secrets_use_stdin_never_arguments_or_logs(self): + secret="super-secret with 'quotes' and $shell" + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);(root/'115.conf').write_bytes(self.config) + with patch.object(native.resources,'CONFIG_DIR',root), \ + patch.object(instances,'command',return_value=self.config), \ + patch.object(native.transaction,'log') as log, \ + patch.object(native.subprocess,'run',return_value=Mock(returncode=0)) as run: + native.set_config(115,self.record['installation_id'],self.config,{'env':'SECRET='+secret+'\0B=two'}) + self.assertNotIn(secret,str(run.call_args.args)) + self.assertNotIn(secret,str(log.call_args_list)) + self.assertIn(secret,json.loads(run.call_args.kwargs['input'])['env']) + + def test_native_api_failure_does_not_echo_secret_stderr(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);(root/'115.conf').write_bytes(self.config) + with patch.object(native.resources,'CONFIG_DIR',root),patch.object(instances,'command',return_value=self.config), \ + patch.object(native.subprocess,'run',return_value=Mock(returncode=1,stderr=b'secret')): + with self.assertRaisesRegex(RuntimeError,'Proxmox could not apply') as error: + native.set_config(115,self.record['installation_id'],self.config,{'env':'SECRET=secret'}) + self.assertNotIn('secret',str(error.exception)) + + def test_verifies_all_environment_entries_and_native_values(self): + native.verify_values(CONFIG,{'values':{'env':'A=one\0B=two','cores':'2'},'delete':[]}) + for values in ({'env':'A=wrong\0B=two'},{'cores':'3'}): + with self.assertRaises(ValueError):native.verify_values(CONFIG,{'values':values,'delete':[]}) + with self.assertRaises(ValueError):native.verify_values(CONFIG,{'values':{},'delete':['env']}) + + def test_native_property_canonicalization_keeps_modes_and_default_flags(self): + cfg=CONFIG+b'dev0: path=/dev/fuse,mode=660\n' + native.verify_values(cfg,{'values':{'dev0':'path=/dev/fuse,mode=0660,uid=0,gid=0,deny-write=0'},'delete':[]}) + + +class NativeApplyTests(unittest.TestCase): + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup) + self.root=Path(self.temp.name) + self.record=record();self.candidate=copy.deepcopy(self.record) + self.candidate['deployment']['environment']=[{'name':'A','value':'changed'}] + self.config=CONFIG + self.state={'config':CONFIG,'running':True} + self.commands=[] + def run(*args): + self.commands.append(args) + if args[:2]==('pct','status'):return b'status: running' if self.state['running'] else b'status: stopped' + if args[:2]==('pct','start'):self.state['running']=True + return b'' + def stop(vmid):self.state['running']=False + def setter(vmid,identity,expected,values,delete=()): + self.assertEqual(expected,self.state['config']) + text=self.state['config'].decode() + if 'env' in values or 'env' in delete: + public='env: ' in text + text=''.join(x for x in text.splitlines(keepends=True) if not x.startswith(('lxc.environment.runtime: ','env: '))) + if public and values.get('env'):text+='env: '+values['env']+'\n' + else: + for item in values.get('env','').split('\0'): + if item:text+='lxc.environment.runtime: '+item+'\n' + self.state['config']=text.encode() + def observe(*args): + observed=copy.deepcopy(self.record['observed']) + observed.update(config=self.state['config'].decode(),config_sha256=native.sha(self.state['config'])) + return observed + patches=[patch.object(native,'plan',return_value={'values':{'env':'A=changed\0B=two'},'delete':[],'restart':True,'changed':['environment']}), + patch.object(native,'set_config',side_effect=setter),patch.object(instances,'command',side_effect=lambda *a:self.state['config']), + patch.object(native.transaction,'run',side_effect=run),patch.object(native.transaction,'stop',side_effect=stop), + patch.object(native.transaction,'healthcheck'),patch.object(native.transaction,'preflight'), + patch.object(native.transaction,'freeze_host_sources',return_value=({},{})), + patch.object(native.transaction,'open_log'),patch.object(native.transaction,'log'), + patch.object(instances,'observe',side_effect=observe),patch('oci_carried_record.carry')] + for p in patches:p.start();self.addCleanup(p.stop) + instances.write(instances.location(self.root,115),self.record) + + def apply(self):return native.apply(self.root,self.record,self.candidate,self.config) + + def test_runtime_edit_restarts_existing_guest_and_keeps_rootfs_image_and_data(self): + folder=self.apply() + saved=instances.read(self.root,115) + self.assertEqual(saved['observed']['image'],self.record['observed']['image']) + self.assertIn(b'rootfs: local-zfs:subvol-115-disk-0',self.state['config']) + self.assertEqual(saved['deployment'],self.candidate['deployment']) + self.assertTrue(self.state['running']) + self.assertEqual(json.loads((folder/'before.json').read_text())['phase'],'committed') + self.assertFalse(any(c[:2] in (('pct','create'),('pct','destroy')) for c in self.commands)) + + def test_failed_healthcheck_rolls_back_and_starts_original_service(self): + with patch.object(native.transaction,'healthcheck',side_effect=[RuntimeError('not healthy'),None]): + with self.assertRaisesRegex(RuntimeError,'not healthy'):self.apply() + self.assertEqual(self.state['config'],CONFIG) + self.assertTrue(self.state['running']) + self.assertEqual(instances.read(self.root,115)['deployment'],self.record['deployment']) + + def test_health_failure_restores_public_nul_separated_environment(self): + self.config=CONFIG.replace(b'lxc.environment.runtime: A=one\nlxc.environment.runtime: B=two\n',b'env: A=one\0B=two\n') + self.state['config']=self.config + self.record['observed'].update(config=self.config.decode(),config_sha256=native.sha(self.config)) + instances.write(instances.location(self.root,115),self.record) + with patch.object(native.transaction,'healthcheck',side_effect=[RuntimeError('not healthy'),None]): + with self.assertRaises(RuntimeError):self.apply() + self.assertEqual(native.environment(self.state['config']),{'A':'one','B':'two'}) + self.assertTrue(self.state['running']) + + def test_stopped_guest_stays_stopped(self): + self.state['running']=False + self.apply() + self.assertFalse(self.state['running']) + self.assertNotIn(('pct','start','115'),self.commands) + + def test_interrupted_edit_is_recovered_through_transaction_dispatch(self): + folder=self.apply() + path=folder/'before.json';state=json.loads(path.read_text());state['phase']='configured' + instances.write(path,state) + pending=copy.deepcopy(self.record) + pending.update(status='updating',transaction_id=folder.name,pending_transaction=str(path)) + instances.write(instances.location(self.root,115),pending) + native.transaction.recover(self.root,path) + self.assertEqual(self.state['config'],CONFIG) + saved=instances.read(self.root,115) + self.assertEqual(saved['status'],'installed');self.assertNotIn('pending_transaction',saved) + self.assertEqual(json.loads(path.read_text())['phase'],'rolled-back') + + def test_recovery_refuses_a_concurrent_environment_edit(self): + folder=self.apply();path=folder/'before.json';state=json.loads(path.read_text());state['phase']='configured' + instances.write(path,state) + pending=copy.deepcopy(self.record) + pending.update(status='updating',transaction_id=folder.name,pending_transaction=str(path)) + instances.write(instances.location(self.root,115),pending) + self.state['config']=self.state['config'].replace(b'A=changed',b'A=concurrent') + with self.assertRaisesRegex(ValueError,'changed'):native.transaction.recover(self.root,path) + self.assertIn(b'A=concurrent',self.state['config']) + + def test_concurrent_change_before_write_is_not_overwritten(self): + def stop(vmid): + self.state['running']=False + self.state['config']+=b'timezone: host\n' + with patch.object(native.transaction,'stop',side_effect=stop): + with self.assertRaisesRegex(ValueError,'changed'):self.apply() + self.assertIn(b'timezone: host',self.state['config']) + self.assertTrue(self.state['running']) + + def test_router_does_not_resolve_download_or_recreate_for_environment_edit(self): + with patch.object(instances,'ROOT',self.root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(native.transaction,'external_changes',return_value={}), \ + patch.object(update,'resolve_archive',side_effect=AssertionError('registry path')), \ + patch.object(native.transaction,'apply',side_effect=AssertionError('recreation')), \ + patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()): + update.update(115,proposal={'operation':'recreate','candidate':self.candidate}) + self.assertEqual(instances.read(self.root,115)['deployment']['environment'],self.candidate['deployment']['environment']) + + def test_unsupported_same_image_adaptation_uses_the_existing_recreation_flow(self): + self.candidate['deployment']['post_start_configurations']=[{'id':'unsupported-adapter'}] + with patch.object(instances,'ROOT',self.root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(native.transaction,'external_changes',return_value={}), \ + patch.object(update,'resolve_archive',return_value=(Path('/verified.tar'),'sha256:test')) as resolve, \ + patch.object(native.transaction,'apply') as recreate, \ + patch.object(native,'apply',side_effect=AssertionError('partial native edit')), \ + patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()): + update.update(115,proposal={'operation':'recreate','candidate':self.candidate}) + resolve.assert_called_once() + recreate.assert_called_once() + self.assertEqual(recreate.call_args.args[3],'recreate') + + def test_optional_backup_does_not_force_recreation(self): + with patch.object(instances,'ROOT',self.root),patch.object(instances,'locked',return_value=contextlib.nullcontext()), \ + patch.object(native.transaction,'external_changes',return_value={}), \ + patch.object(update,'resolve_archive',side_effect=AssertionError('registry path')), \ + patch.object(update,'backup_native_settings') as backup, \ + patch.object(oci_operation_notice,'operation',return_value=contextlib.nullcontext()): + update.update(115,proposal={'operation':'recreate','candidate':self.candidate},keep_backup='test-storage') + backup.assert_called_once_with(115,'test-storage') + + +if __name__=='__main__':unittest.main() diff --git a/oci/tests/test_readonly_volume_preparation.py b/oci/tests/test_readonly_volume_preparation.py new file mode 100644 index 00000000..37f67011 --- /dev/null +++ b/oci/tests/test_readonly_volume_preparation.py @@ -0,0 +1,187 @@ +"""Run the installer's volume preparation loop against filesystem fixtures. + +Write doubles reject every mutation of the protected directory, including +lost+found removal. Writable paths still run real rm/chmod; chown is recorded +without requiring root. These tests never invoke Proxmox or touch a container. +""" + +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +INSTALLER = ROOT / "remote/install_oci.sh" + + +def preparation_loop(): + source = INSTALLER.read_text() + start = source.index(" if (( failed == 0 )) && [[ $(jq -r '.stack_managed") + end = source.index("\n if (( failed == 0 && tls_count == 1 )); then", start) + return source[start:end] + + +def preparation(target): + return { + "container_path": target, + "remove_lost_found": True, + "owner_strategy": "mapped-application-user", + "mode": "0750", + } + + +class ReadOnlyVolumePreparationTests(unittest.TestCase): + def run_case(self, mounts, *, target="/run/secrets", read_only=True, + preparations=None): + preparations = preparations if preparations is not None else [preparation(target)] + with tempfile.TemporaryDirectory() as directory: + fixture = Path(directory) + rootfs = fixture / "rootfs" + for item in preparations: + path = rootfs / item["container_path"].lstrip("/") + path.mkdir(parents=True, exist_ok=True) + (path / "lost+found").mkdir(exist_ok=True) + protected = rootfs / target.lstrip("/") + protected.mkdir(parents=True, exist_ok=True) + sentinel = protected / "sentinel" + sentinel.write_bytes(b"existing secret\n") + sentinel.chmod(0o600) + protected.chmod(0o700) + before = sentinel.stat() + template = {"proxmox": {"installer_profile": { + "volume_preparations": preparations, + }}} + (fixture / "template.json").write_text(json.dumps(template)) + (fixture / "deployment.json").write_text(json.dumps({"mounts": mounts})) + script = r'''set -Eeuo pipefail +rootfs="$FIXTURE/rootfs" +TEMPLATE_FILE="$FIXTURE/template.json" +DEPLOYMENT_FILE="$FIXTURE/deployment.json" +HOST_BIND_UID=110001; HOST_BIND_GID=110001 +HOST_ROOT_UID=100000; HOST_ROOT_GID=100000 +failed=0; volumes_prepared=0 +oci_log() { printf 'log %s\n' "$*"; } +write_op() { + local operation=$1 + shift + local path=${!#} + printf 'WRITE %s %s\n' "$operation" "$path" + if [[ -n $PROTECTED && ( $path == "$PROTECTED" || $path == "$PROTECTED/"* ) ]]; then + printf 'Read-only file system\n' >&2 + return 1 + fi + case "$operation" in + rm|chmod) command "$operation" "$@" ;; + chown) return 0 ;; + esac +} +chown() { write_op chown "$@"; } +chmod() { write_op chmod "$@"; } +rm() { write_op rm "$@"; } +''' + preparation_loop() + r''' +printf 'failed=%s prepared=%s\n' "$failed" "$volumes_prepared" +test "$failed" = 0 +''' + result = subprocess.run( + ["bash", "-c", script], capture_output=True, text=True, + env=dict(os.environ, FIXTURE=str(fixture), + PROTECTED=str(protected) if read_only else ""), + timeout=15, + ) + self.assertEqual(sentinel.read_bytes(), b"existing secret\n") + after = sentinel.stat() + self.assertEqual( + (before.st_ino, before.st_uid, before.st_gid, before.st_mode, before.st_mtime_ns), + (after.st_ino, after.st_uid, after.st_gid, after.st_mode, after.st_mtime_ns), + ) + if read_only: + self.assertEqual(protected.stat().st_mode & 0o777, 0o700) + self.assertTrue((protected / "lost+found").is_dir()) + elif "prepared=1" in result.stdout: + self.assertEqual(protected.stat().st_mode & 0o777, 0o750) + self.assertFalse((protected / "lost+found").exists()) + return result + + def assert_skipped(self, result): + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + self.assertNotIn("WRITE ", result.stdout) + self.assertIn("Skipping preparation of read-only volume", result.stdout) + self.assertIn("prepared=0", result.stdout) + + def assert_prepared(self, result): + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + for operation in ("rm", "chown", "chmod"): + self.assertIn(f"WRITE {operation} ", result.stdout) + self.assertIn("prepared=1", result.stdout) + + def test_read_only_managed_and_bind_volumes_receive_no_writes(self): + for mount_type in ("managed-volume", "host-bind"): + with self.subTest(mount_type=mount_type): + self.assert_skipped(self.run_case([ + {"container_path": "/run/secrets", "type": mount_type, "read_only": True}, + ])) + + def test_read_only_parent_protects_nested_preparation(self): + self.assert_skipped(self.run_case([ + {"container_path": "/run", "read_only": True}, + ])) + + def test_writable_child_overrides_read_only_parent_regardless_of_order(self): + mounts = [ + {"container_path": "/run", "read_only": True}, + {"container_path": "/run/secrets", "read_only": False}, + ] + for ordered in (mounts, mounts[::-1]): + with self.subTest(mounts=ordered): + self.assert_prepared(self.run_case(ordered, read_only=False)) + + def test_child_without_read_only_flag_overrides_read_only_parent(self): + self.assert_prepared(self.run_case([ + {"container_path": "/run", "read_only": True}, + {"container_path": "/run/secrets"}, + ], read_only=False)) + + def test_read_only_child_overrides_writable_parent_regardless_of_order(self): + mounts = [ + {"container_path": "/run/secrets", "read_only": True}, + {"container_path": "/run", "read_only": False}, + ] + for ordered in (mounts, mounts[::-1]): + with self.subTest(mounts=ordered): + self.assert_skipped(self.run_case(ordered)) + + def test_writable_volume_is_still_prepared(self): + self.assert_prepared(self.run_case([ + {"container_path": "/data", "read_only": False}, + ], target="/data", read_only=False)) + + def test_missing_read_only_flag_defaults_to_writable(self): + self.assert_prepared(self.run_case([ + {"container_path": "/data"}, + ], target="/data", read_only=False)) + + def test_unrelated_mount_with_similar_prefix_does_not_skip_preparation(self): + self.assert_prepared(self.run_case([ + {"container_path": "/run/secret", "read_only": True}, + ], read_only=False)) + + def test_directory_outside_mounts_is_still_prepared(self): + self.assert_prepared(self.run_case([], read_only=False)) + + def test_skipping_read_only_volume_does_not_skip_following_writable_volume(self): + result = self.run_case([ + {"container_path": "/run/secrets", "read_only": True}, + {"container_path": "/data", "read_only": False}, + ], preparations=[preparation("/run/secrets"), preparation("/data")]) + self.assert_prepared(result) + self.assertIn("Skipping preparation of read-only volume: /run/secrets", result.stdout) + for line in result.stdout.splitlines(): + if line.startswith("WRITE "): + self.assertTrue(line.endswith("/data") or line.endswith("/data/lost+found"), line) + + +if __name__ == "__main__": + unittest.main() diff --git a/oci/tests/test_runtime_home_contract.py b/oci/tests/test_runtime_home_contract.py index c14e8e26..25e9ca9e 100644 --- a/oci/tests/test_runtime_home_contract.py +++ b/oci/tests/test_runtime_home_contract.py @@ -31,14 +31,14 @@ class RuntimeHomeResolutionTests(unittest.TestCase): ) self.assertEqual(oci_runtime.home_directory(passwd, 1001), "/srv/app") - def test_unknown_or_unsafe_home_is_not_invented(self): + def test_unsafe_record_is_rejected_but_unknown_uid_defaults_to_root(self): passwd = self.passwd( "root:x:0:0:root:relative-home:/bin/sh\n" "app:x:1001:1001:App user::/sbin/nologin\n" ) self.assertIsNone(oci_runtime.home_directory(passwd, 0)) self.assertIsNone(oci_runtime.home_directory(passwd, 1001)) - self.assertIsNone(oci_runtime.home_directory(passwd, 1234)) + self.assertEqual(oci_runtime.home_directory(passwd, 1234), "/") class RuntimeHomeInstallerContractTests(unittest.TestCase): diff --git a/oci/tests/test_runtime_home_hardening.py b/oci/tests/test_runtime_home_hardening.py new file mode 100644 index 00000000..6418709d --- /dev/null +++ b/oci/tests/test_runtime_home_hardening.py @@ -0,0 +1,260 @@ +"""Exercise HOME resolution without reading image symlinks or changing guests.""" + +from contextlib import redirect_stderr, redirect_stdout +import io +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "remote")) +import oci_runtime + + +class HomeHardeningTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + (self.root / "etc").mkdir() + self.passwd = self.root / "etc/passwd" + self.passwd.write_text("root:x:0:0:root:/root:/bin/sh\n", encoding="utf-8") + + def cli(self, uid="1234"): + output, error = io.StringIO(), io.StringIO() + with patch.object(sys, "argv", ["oci_runtime.py", "--home", str(self.passwd), uid]), \ + redirect_stdout(output), redirect_stderr(error): + status = oci_runtime.main() + return status, output.getvalue(), error.getvalue() + + def assert_rejected_without_read(self): + with patch.object(os, "fdopen", side_effect=AssertionError("unsafe file read")): + status, output, _ = self.cli() + self.assertEqual(status, 1) + self.assertEqual(output, "") + + def test_unknown_uid_uses_root_only_after_reading_a_regular_database(self): + self.assertEqual(self.cli(), (0, "/\n", "")) + + def test_empty_regular_database_uses_root(self): + self.passwd.write_text("") + self.assertEqual(self.cli(), (0, "/\n", "")) + + def test_final_symlink_is_rejected_without_reading_its_target(self): + target = self.root / "outside" + target.write_text("app:x:1234:1234::/host-secret:/bin/sh\n") + self.passwd.unlink() + self.passwd.symlink_to(target) + self.assert_rejected_without_read() + self.assertEqual(target.read_text(), "app:x:1234:1234::/host-secret:/bin/sh\n") + + def test_symlinked_etc_is_rejected_without_reading_its_target(self): + directory = self.root / "etc" + directory.rename(self.root / "outside-etc") + directory.symlink_to(self.root / "outside-etc", target_is_directory=True) + self.assert_rejected_without_read() + self.assertEqual(self.cli("0")[:2], (1, "")) + + def test_pinned_etc_does_not_follow_a_later_directory_replacement(self): + original_open = os.open + outside = self.root / "outside" + outside.mkdir() + (outside / "passwd").write_text("root:x:0:0::/outside:/bin/sh\n") + replaced = [] + def replace(path, flags, *args, **kwargs): + if str(path) == "passwd": + (self.root / "etc").rename(self.root / "original-etc") + (self.root / "etc").symlink_to(outside, target_is_directory=True) + replaced.append(True) + return original_open(path, flags, *args, **kwargs) + with patch.object(os, "open", side_effect=replace): + self.assertEqual(self.cli("0"), (0, "/root\n", "")) + self.assertEqual(replaced, [True]) + + def test_directory_is_rejected_without_reading(self): + self.passwd.unlink() + self.passwd.mkdir() + self.assert_rejected_without_read() + + def test_fifo_is_rejected_without_reading_or_waiting_for_a_writer(self): + self.passwd.unlink() + os.mkfifo(self.passwd) + done = subprocess.run( + [sys.executable, "-B", str(ROOT / "remote/oci_runtime.py"), "--home", str(self.passwd), "0"], + capture_output=True, text=True, timeout=3, + ) + self.assertEqual(done.returncode, 1) + self.assertEqual(done.stdout, "") + self.assert_rejected_without_read() + + def test_missing_database_does_not_masquerade_as_unknown_uid(self): + self.passwd.unlink() + self.assert_rejected_without_read() + + def test_permission_failure_does_not_masquerade_as_unknown_uid(self): + with patch.object(os, "open", side_effect=PermissionError("denied")): + self.assert_rejected_without_read() + + def test_invalid_encoding_does_not_produce_a_fallback(self): + self.passwd.write_bytes(b"\xff\xfe") + status, output, _ = self.cli() + self.assertEqual((status, output), (1, "")) + + def test_partial_matching_record_does_not_produce_a_fallback(self): + self.passwd.write_text("app:x:1234:1234\n") + self.assertEqual(self.cli()[:2], (1, "")) + + def test_read_error_closes_both_descriptors_without_a_fallback(self): + descriptors = [] + original_open = os.open + def opened(*args, **kwargs): + fd = original_open(*args, **kwargs) + descriptors.append(fd) + return fd + with patch.object(os, "open", side_effect=opened), \ + patch.object(os, "fdopen", side_effect=OSError("read failed")): + self.assertEqual(self.cli()[:2], (1, "")) + self.assertEqual(len(descriptors), 2) + for fd in descriptors: + with self.assertRaises(OSError): + os.fstat(fd) + + def test_matching_unsafe_home_is_not_replaced_with_root(self): + for value in ("", "relative", "/bad\tvalue", "/bad\x00value", "/bad\x7fvalue"): + with self.subTest(value=repr(value)): + self.passwd.write_text(f"app:x:1234:1234::{value}:/bin/sh\n") + self.assertEqual(self.cli()[:2], (1, "")) + + def test_invalid_uid_does_not_open_the_database(self): + for uid in ("-1", "nobody"): + with self.subTest(uid=uid), patch.object(os, "open") as opened: + self.assertEqual(self.cli(uid)[:2], (1, "")) + opened.assert_not_called() + + def test_symlink_replacement_between_check_and_open_is_not_read(self): + original_open = os.open + outside = self.root / "outside" + outside.write_text("app:x:1234:1234::/outside:/bin/sh\n") + def replace(path, flags, *args, **kwargs): + if str(path) == "passwd": + self.passwd.unlink() + self.passwd.symlink_to(outside) + return original_open(path, flags, *args, **kwargs) + with patch.object(os, "open", side_effect=replace): + self.assert_rejected_without_read() + + def test_fifo_replacement_between_check_and_open_is_not_read(self): + original_open = os.open + def replace(path, flags, *args, **kwargs): + if str(path) == "passwd": + self.passwd.unlink() + os.mkfifo(self.passwd) + return original_open(path, flags, *args, **kwargs) + with patch.object(os, "open", side_effect=replace): + self.assert_rejected_without_read() + + +class HomeInstallerExecutionTests(unittest.TestCase): + """Run the real shell HOME function and Python CLI with fake mount boundaries.""" + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + (self.root / "etc").mkdir() + self.passwd = self.root / "etc/passwd" + self.passwd.write_text("root:x:0:0:root:/root:/bin/sh\napp:x:1001:1001::/srv/app:/bin/sh\n") + self.conf = self.root / "guest.conf" + self.trace = self.root / "trace" + self.source = (ROOT / "remote/install_oci.sh").read_text() + + def run_home(self, config="unrelated: keep\n", unmount_status=0, mount_status=0): + self.conf.write_text(config) + self.trace.write_text("") + functions = [] + for name in ("ensure_runtime_home", "set_runtime_environment"): + found = re.search(rf"^{name}\(\) \{{\n.*?^\}}\n", self.source, re.M | re.S) + self.assertIsNotNone(found, name) + functions.append(found.group(0)) + script = r''' +set -euo pipefail +CONF=$1 +FIXTURE_ROOT=$2 +TRACE=$3 +OCI_RUNTIME_RESOLVER=$4 +PYTHON=$5 +UNMOUNT_STATUS=$6 +MOUNT_STATUS=$7 +OCI_LOG="$FIXTURE_ROOT/resolver.log" +VMID=123456789 +translate() { printf '%s' "$1"; } +die() { printf '%s\n' "$*" >&2; exit 1; } +mount_ct_rootfs() { + printf 'mount\n' >> "$TRACE" + [ "$MOUNT_STATUS" = 0 ] || die 'mount failed' + rootfs=$FIXTURE_ROOT + passwd_file="$rootfs/etc/passwd" +} +oci_quiet() { printf '%s\n' "$*" >> "$TRACE"; return "$UNMOUNT_STATUS"; } +python3() { printf 'resolve\n' >> "$TRACE"; command "$PYTHON" -B "$@"; } +''' + done = subprocess.run( + ["bash", "-c", script + "\n".join(functions) + "\nensure_runtime_home\nprintf 'continued\\n'\n", + "bash", str(self.conf), str(self.root), str(self.trace), str(ROOT / "remote/oci_runtime.py"), + sys.executable, str(unmount_status), str(mount_status)], + capture_output=True, text=True, timeout=5, + ) + return done, self.conf.read_text(), self.trace.read_text().splitlines() + + def test_explicit_home_including_empty_never_mounts_or_resolves(self): + for home in ("/custom", ""): + config = f"unrelated: keep\nlxc.environment.runtime: HOME={home}\n" + done, after, trace = self.run_home(config) + self.assertEqual((done.returncode, after, trace), (0, config, [])) + + def test_default_and_effective_uid_write_the_resolved_home_after_unmount(self): + for uid, expected in ((None, "/root"), (1001, "/srv/app"), (9999, "/")): + config = "unrelated: keep\n" + (f"lxc.init.uid: {uid}\n" if uid is not None else "") + done, after, trace = self.run_home(config) + self.assertEqual(done.returncode, 0, done.stderr) + self.assertEqual(after, config + f"lxc.environment.runtime: HOME={expected}\n") + self.assertEqual(trace, ["mount", "resolve", "pct unmount 123456789"]) + + def test_invalid_uid_leaves_config_unchanged_without_mount(self): + config = "lxc.init.uid: not-a-uid\n" + done, after, trace = self.run_home(config) + self.assertEqual((done.returncode, after, trace), (0, config, [])) + + def test_rejected_input_unmounts_and_preserves_config(self): + self.passwd.unlink() + self.passwd.symlink_to(self.root / "missing") + config = "unrelated: keep\n" + done, after, trace = self.run_home(config) + self.assertEqual((done.returncode, after), (0, config)) + self.assertEqual(trace, ["mount", "resolve", "pct unmount 123456789"]) + + def test_failed_unmount_stops_before_config_write_for_success_and_failure(self): + for valid in (True, False): + if not valid: + self.passwd.unlink() + config = "unrelated: keep\n" + done, after, trace = self.run_home(config, unmount_status=1) + self.assertNotEqual(done.returncode, 0) + self.assertEqual(after, config) + self.assertNotIn("continued", done.stdout) + self.assertEqual(trace, ["mount", "resolve", "pct unmount 123456789"]) + + def test_failed_mount_never_reads_or_writes(self): + config = "unrelated: keep\n" + done, after, trace = self.run_home(config, mount_status=1) + self.assertNotEqual(done.returncode, 0) + self.assertEqual(after, config) + self.assertEqual(trace, ["mount"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/oci/tests/test_shared_directories.py b/oci/tests/test_shared_directories.py index d8204835..55545374 100644 --- a/oci/tests/test_shared_directories.py +++ b/oci/tests/test_shared_directories.py @@ -43,15 +43,18 @@ class ScriptedUI: def password(self, text, required=True): return self._answer(text, "") - def choose(self, text, options, default=None): + def choose(self, text, options, default=None, **_kwargs): return self._answer(text, default) def confirm(self, text, default=False): return self._answer(text, default) - def message(self, text): + def message(self, text, *_args): pass + def review(self, *_args, **_kwargs): + return True + @patch("proxmenux_oci.i18n.language", return_value="en") @patch("proxmenux_oci.installer.host.storages", side_effect=storages) @@ -223,14 +226,15 @@ class SharedDirectoriesTests(unittest.TestCase): patch("proxmenux_oci.recreation.edit_acceleration"), \ patch("proxmenux_oci.recreation.edit_environment"): ui = ScriptedUI({"Add a directory to share": [True], NAME: ["photos"], + "Choose a setting. Changes are applied only after the final review.": ["shares", "apply"], "Where to store /shares/photos": ["host-bind"], "Host path for /shares/photos": ["/tank/photos"]}) proposal = edit_recreation(record, ui) mounts = [m["container_path"] for m in proposal["candidate"]["deployment"]["mounts"]] self.assertEqual(mounts, ["/config", "/shares/media", "/shares/photos"]) - untouched = edit_recreation(record, ScriptedUI()) - self.assertEqual([m["container_path"] for m in untouched["candidate"]["deployment"]["mounts"]], - ["/config", "/shares/media"]) + untouched = edit_recreation(record, ScriptedUI({ + "Choose a setting. Changes are applied only after the final review.": ["discard"]})) + self.assertIsNone(untouched) self.assertEqual([m["container_path"] for m in plan["mounts"]], ["/config", "/shares/media"]) diff --git a/scripts/global/remove-banner-pve-v3.sh b/scripts/global/remove-banner-pve-v3.sh index 4ddf7af2..92df1028 100644 --- a/scripts/global/remove-banner-pve-v3.sh +++ b/scripts/global/remove-banner-pve-v3.sh @@ -283,6 +283,10 @@ reload_services() { systemctl is-active --quiet pvedaemon 2>/dev/null && { systemctl reload pvedaemon 2>/dev/null || true } + # Proxmox Backup Server serves the same toolkit from its own proxy. + systemctl is-active --quiet proxmox-backup-proxy 2>/dev/null && { + systemctl reload proxmox-backup-proxy 2>/dev/null || systemctl restart proxmox-backup-proxy 2>/dev/null || true + } } main() { @@ -316,9 +320,12 @@ EOFAPT # Main function to remove subscription banner remove_subscription_banner_v3() { local pve_version - pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown") - - msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying minimal banner patch")" + if command -v pveversion >/dev/null 2>&1; then + pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || echo "unknown") + msg_info "$(translate "Detected Proxmox VE") ${pve_version} - $(translate "applying minimal banner patch")" + else + msg_info "$(translate "Detected Proxmox Backup Server") - $(translate "applying minimal banner patch")" + fi diff --git a/scripts/global/repository-functions.sh b/scripts/global/repository-functions.sh index d6114085..7a2f3b86 100644 --- a/scripts/global/repository-functions.sh +++ b/scripts/global/repository-functions.sh @@ -62,3 +62,48 @@ ensure_repositories() { fi return 0 } + +# The no-subscription switch of a Proxmox Backup Server: the same policy and +# API as on PVE, offered only when the host has no active subscription. +ensure_pbs_repositories() { + local suite decision refresh_status + if [[ "$(pmx_platform_field supported)" != true ]]; then + msg_error "$(translate 'This operation supports PBS 3 on Debian 12 and PBS 4 on Debian 13.')" + return 1 + fi + suite=$(pmx_platform_field codename) + decision=$(repository_policy plan "$suite" pbs) || return 1 + case "$decision" in + preserve) return 0 ;; + offer) ;; + *) msg_error "$(translate 'Repository policy returned an unexpected result.')"; return 1 ;; + esac + if ! declare -F hybrid_yesno >/dev/null || { [[ ! -t 0 ]] && ! { declare -F is_web_mode >/dev/null && is_web_mode; }; }; then + msg_error "$(translate 'No active subscription and no usable PBS repository. Noninteractive mode cannot change APT sources; configure them in Administration > Repositories.')" + return 1 + fi + if ! hybrid_yesno "$(translate 'Proxmox repository')" \ + "$(translate 'This Proxmox Backup Server has no active subscription, switch to the no-subscription repository? The inaccessible Enterprise PBS source will be disabled.')" 14 90; then + msg_error "$(translate 'Repository switch declined; no APT source changed.')" + return 1 + fi + decision=$(repository_policy apply "$suite" pbs) || return 1 + [[ "$decision" == changed || "$decision" == preserve ]] || return 1 + if [[ "$decision" == changed ]]; then + if declare -F pmx_record_execution >/dev/null; then + local journal_function="${PMX_JOURNAL_FUNCTION:-}" journal_version="${PMX_JOURNAL_VERSION:-}" + pmx_journal_context "ensure_pbs_repositories" "" "${PMX_JOURNAL_SOURCE:-}" + pmx_record_execution "Switch to the PBS no-subscription repository" \ + "proxmox-backup-debug api set /nodes/localhost/apt/repositories --handle no-subscription" + PMX_JOURNAL_FUNCTION="$journal_function" PMX_JOURNAL_VERSION="$journal_version" + fi + if apt-get update; then + return 0 + else + refresh_status=$? + msg_error "$(translate '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.')" + return "$refresh_status" + fi + fi + return 0 +} diff --git a/scripts/global/repository_policy.py b/scripts/global/repository_policy.py index 7e0dca70..95f40d68 100644 --- a/scripts/global/repository_policy.py +++ b/scripts/global/repository_policy.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -"""Proxmox repository policy via its own parsed APT repository API. +"""Proxmox VE and Proxmox Backup Server repository policy via their own parsed APT +repository API. Only `notfound` or `expired` permits an offered switch. Never print raw subscription or repository API responses: they can contain subscription keys or credentials. @@ -12,6 +13,19 @@ import sys ENDPOINT = '/nodes/localhost/apt/repositories' PVE_CHANNELS = {'pve-enterprise', 'pve-no-subscription', 'pve-test', 'pvetest'} +# Proxmox VE and Proxmox Backup Server share the repository API; each has its +# own command line, channels and place in the web interface. +PRODUCTS = { + 'pve': {'api': ['pvesh'], 'name': 'Proxmox VE', 'short': 'PVE', + 'channels': PVE_CHANNELS, 'enterprise': 'pve-enterprise', 'public': 'pve-no-subscription', + 'test': {'pve-no-subscription', 'pve-test', 'pvetest'}, + 'ui': 'Node > Updates > Repositories', 'ceph': True}, + 'pbs': {'api': ['proxmox-backup-debug', 'api'], 'name': 'Proxmox Backup Server', 'short': 'PBS', + 'channels': {'pbs-enterprise', 'pbs-no-subscription', 'pbs-test', 'pbstest'}, + 'enterprise': 'pbs-enterprise', 'public': 'pbs-no-subscription', + 'test': {'pbs-no-subscription', 'pbs-test', 'pbstest'}, + 'ui': 'Administration > Repositories', 'ceph': False}, +} def run(args): @@ -21,7 +35,16 @@ def run(args): raise ValueError(f'Unable to query or change Proxmox repository state ({args[0]}). Check the service and retry.') from exc -def subscription_status(): +def subscription_status(product='pve'): + if product == 'pbs': + # Only the status field is read; the answer also carries the server ID. + try: + status = json.loads(run(['proxmox-backup-manager', 'subscription', 'get', '--output-format', 'json'])).get('status') + 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.') + 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. output = run(['pvesubscription', 'get']) @@ -31,9 +54,10 @@ def subscription_status(): return statuses[0] -def repository_state(suite): +def repository_state(suite, product='pve'): + p = PRODUCTS[product] try: - data = json.loads(run(['pvesh', 'get', ENDPOINT, '--output-format', 'json'])) + data = json.loads(run([*p['api'], 'get', ENDPOINT, '--output-format', 'json'])) if not isinstance(data, dict) or not isinstance(data['files'], list) or not isinstance(data['errors'], list) or not isinstance(data['digest'], str) or not isinstance(data['standard-repos'], list): raise ValueError() if data['errors']: @@ -86,13 +110,14 @@ def repository_state(suite): entries.append((file['path'], index, row)) return data, entries except (KeyError, TypeError, ValueError, IndexError) as exc: - raise ValueError('Proxmox APT repository inventory is invalid or reports parse errors. Fix sources in Node > Updates > Repositories; no repository changed.') from exc + raise ValueError(f"Proxmox APT repository inventory is invalid or reports parse errors. Fix sources in {p['ui']}; no repository changed.") from exc -def evaluate(suite, apply=False): +def evaluate(suite, apply=False, product='pve'): if suite not in ('bookworm', 'trixie'): raise ValueError('Unsupported Proxmox suite; no repository changed.') - data, entries = repository_state(suite) + p = PRODUCTS[product] + data, entries = repository_state(suite, product) pve = [] ceph = [] debian = False @@ -102,40 +127,40 @@ def evaluate(suite, apply=False): if not row['Enabled'] or 'deb' not in row['Types']: continue components = set(row['Components']) - if components & PVE_CHANNELS: + if components & p['channels']: if suite not in row['Suites']: - wrong_pve.append(f"{' '.join(sorted(components & PVE_CHANNELS))} in {path} (suite {' '.join(row['Suites'])})") + wrong_pve.append(f"{' '.join(sorted(components & p['channels']))} in {path} (suite {' '.join(row['Suites'])})") pve.append((path, index, row)) - if 'main' in components and suite in row['Suites'] and not components & PVE_CHANNELS: + if 'main' in components and suite in row['Suites'] and not components & p['channels']: debian = True - if 'enterprise' in components and any('/ceph-' in uri for uri in row['URIs']): + if p['ceph'] and 'enterprise' in components and any('/ceph-' in uri for uri in row['URIs']): if suite not in row['Suites']: wrong_ceph.append(f"{path} (suite {' '.join(row['Suites'])})") 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 Proxmox VE on {suite}, but these Proxmox VE repositories are for another suite: " - f"{'; '.join(wrong_pve)}. Disable or remove them in Node > Updates > Repositories and run the update again; no repository changed.") + 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.") 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.") - if any(set(row['Components']) & {'pve-no-subscription', 'pve-test', 'pvetest'} for _, _, row in pve): + if any(set(row['Components']) & p['test'] for _, _, row in pve): return 'preserve' # Entitlement lookup is only needed when considering a switch. Existing # public/test channels are the operator's choice, even if lookup fails. - status = subscription_status() # fail closed before any write + status = subscription_status(product) # fail closed before any write if status == 'active': if not pve: - raise ValueError('Host has an active subscription but no active PVE repository. Configure its Enterprise source in Node > Updates > Repositories; no repository changed.') + raise ValueError(f"Host has an active subscription but no active {p['short']} repository. Configure its Enterprise source in {p['ui']}; no repository changed.") return 'preserve' if not debian: raise ValueError('No active Debian base repository for this suite; configure it in the Proxmox repository UI before continuing.') - # No active PVE source or only inaccessible Enterprise. A mixed stanza + # 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 'pve-enterprise' in item[2]['Components']] + ceph + disable = [item for item in pve if p['enterprise'] in item[2]['Components']] + ceph for _, _, row in disable: - expected = {'pve-enterprise'} if 'pve-enterprise' in row['Components'] else {'enterprise'} + 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.') handles = [r.get('handle') for r in data['standard-repos'] if r.get('handle') == 'no-subscription'] @@ -149,9 +174,9 @@ def evaluate(suite, apply=False): try: for path, index, original in disable: next(file for file in expected if file['path'] == path)['repositories'][index]['Enabled'] = False - run(['pvesh', 'create', ENDPOINT, '--path', path, '--index', str(index), + run([*p['api'], 'create', ENDPOINT, '--path', path, '--index', str(index), '--enabled', '0', '--digest', data['digest']]) - refreshed, current = repository_state(suite) + refreshed, current = repository_state(suite, product) # Per-file digests change when Proxmox serializes the disabled entry. inventory = lambda files: sorted( ({key: value for key, value in file.items() if key != 'digest'} for file in files), @@ -159,23 +184,25 @@ def evaluate(suite, apply=False): if inventory(refreshed['files']) != inventory(expected): raise ValueError('Could not verify the expected repository inventory after Enterprise disable; possible concurrent edit. Inspect the Proxmox repository UI before retrying.') data = refreshed - run(['pvesh', 'set', ENDPOINT, '--handle', 'no-subscription', '--digest', data['digest']]) - _, current = repository_state(suite) + run([*p['api'], 'set', ENDPOINT, '--handle', 'no-subscription', '--digest', data['digest']]) + _, current = repository_state(suite, product) if not any(row['Enabled'] and 'deb' in row['Types'] and suite in row['Suites'] - and 'pve-no-subscription' in row['Components'] for _, _, row in current) \ + and p['public'] in row['Components'] for _, _, row in current) \ or any(p == path and i == index and row['Enabled'] 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('Could not complete or verify the repository switch; changes may be partial and repository state is unknown. Inspect Node > Updates > Repositories before retrying; no automatic rollback was attempted.') from 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 return 'changed' def main(): try: - if len(sys.argv) != 3 or sys.argv[1] not in ('plan', 'apply'): - raise ValueError('Usage: repository_policy.py plan|apply bookworm|trixie') - print(evaluate(sys.argv[2], apply=sys.argv[1] == 'apply')) + 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): + raise ValueError('Usage: repository_policy.py plan|apply bookworm|trixie [pve|pbs]') + 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) return 1 diff --git a/scripts/global/update-pbs.sh b/scripts/global/update-pbs.sh new file mode 100644 index 00000000..54574676 --- /dev/null +++ b/scripts/global/update-pbs.sh @@ -0,0 +1,188 @@ +#!/bin/bash +# ========================================================== +# Proxmox Backup Server Update Script (post-install flows) +# ========================================================== + +LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" +BASE_DIR="/usr/local/share/proxmenux" +UTILS_FILE="$BASE_DIR/utils.sh" +APT_ENV="env DEBIAN_FRONTEND=noninteractive LC_ALL=C LANG=C" + +if [[ -f "$UTILS_FILE" ]]; then + source "$UTILS_FILE" +fi +if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then + source "$LOCAL_SCRIPTS/global/pmx_journal.sh" +fi +source "$LOCAL_SCRIPTS/global/common-functions.sh" +source "$LOCAL_SCRIPTS/global/repository-functions.sh" + +load_language +initialize_cache + +update_pbs() { + local FUNC_VERSION="1.0" + pmx_journal_context "update_pbs" "$FUNC_VERSION" + + if ! pmx_is_pbs || [[ "$(pmx_platform_field supported)" != true ]]; then + msg_error "$(translate 'This operation supports PBS 3 on Debian 12 and PBS 4 on Debian 13.')" + return 1 + fi + + local start_time + start_time=$(date +%s) + local log_file="/var/log/proxmox-update-$(date +%Y%m%d-%H%M%S).log" + local screen_capture="/tmp/proxmenux_screen_capture_$$.txt" + local OS_CODENAME + OS_CODENAME=$(pmx_platform_field codename) + + { + echo "=== ProxMenux — Proxmox Backup Server update ===" + echo "Started: $(date -Iseconds)" + echo "Host: $(hostname)" + echo "Running: proxmox-backup-server $(dpkg-query -W -f='${Version}' proxmox-backup-server 2>/dev/null)" + } > "$log_file" + + if ! ensure_pbs_repositories; then + msg_error "$(translate "Failed to configure Proxmox Backup Server repositories")" + rm -f "$screen_capture" + return 1 + fi + + 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")" + return 1 + fi + + local update_output update_exit_code + update_output=$(apt-get -o APT::Update::Error-Mode=any update 2>&1) + update_exit_code=$? + { + echo + echo "--- apt-get update (exit $update_exit_code) ---" + printf '%s\n' "$update_output" + } >> "$log_file" + if [ $update_exit_code -ne 0 ]; then + msg_error "$(translate "APT refresh failed. Check the PBS subscription and repositories in Administration > Repositories. Log:") $log_file" + return 1 + fi + msg_ok "$(translate "Package lists updated successfully")" | tee -a "$screen_capture" + + local current_version available_version + current_version=$(dpkg-query -W -f='${Version}' proxmox-backup-server 2>/dev/null) + available_version=$(apt-cache policy proxmox-backup-server 2>/dev/null | awk '/Candidate:/ {print $2; exit}') + + local upgradable upgradable_raw upgradable_list security_updates + upgradable_raw=$($APT_ENV apt list --upgradable 2>/dev/null \ + | sed '1d' \ + | sed '/^\s*$/d') + upgradable=$(printf '%s' "$upgradable_raw" | grep -c . ) + security_updates=$(printf '%s\n' "$upgradable_raw" | grep -ci '\-security') + upgradable_list=$(printf '%s\n' "$upgradable_raw" | pmx_format_upgradable) + + { + echo + echo "--- Packages to upgrade ($upgradable) ---" + [ "$upgradable" -gt 0 ] && printf '%s\n' "$upgradable_list" + } >> "$log_file" + + local menu_text + menu_text="$(translate "System Update Information")\n\n" + menu_text+="$(translate "Current PBS Version"): $current_version\n" + if [ -n "$available_version" ] && [ "$available_version" != "$current_version" ]; then + menu_text+="$(translate "Available PBS Version"): $available_version\n" + fi + menu_text+="\n$(translate "Package Updates Available"): $upgradable\n" + menu_text+="$(translate "Security Updates"): $security_updates\n\n" + + if [ "$upgradable" -eq 0 ]; then + menu_text+="$(translate "System is already up to date")" + whiptail --title "$(translate "Update Status")" --msgbox "$menu_text" 15 70 + clear + show_proxmenux_logo + msg_title "$(translate "$SCRIPT_TITLE")" + cat "$screen_capture" + msg_ok "$(translate "System is already up to date. No update needed.")" + echo -e "\nSystem is already up to date." >> "$log_file" + rm -f "$screen_capture" + return 0 + fi + + menu_text+="$(translate "Packages to be upgraded"):\n$upgradable_list\n\n" + menu_text+="$(translate "Do you want to proceed with the system update?")" + local proceed=0 + whiptail --title "$(translate "Proxmox Backup Server Update")" --scrolltext --yesno "$menu_text" 24 78 || proceed=1 + + clear + show_proxmenux_logo + msg_title "$(translate "$SCRIPT_TITLE")" + cat "$screen_capture" + + if [ $proceed -ne 0 ]; then + msg_info2 "$(translate "Update cancelled by user")" + rm -f "$screen_capture" + return 0 + fi + + pmx_record_execution "Upgrade Proxmox Backup Server packages" "apt -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold full-upgrade" + local dpkg_mark upgrade_exit_code + dpkg_mark=$(date '+%Y-%m-%d %H:%M:%S') + echo -e "\n--- apt full-upgrade ---" >> "$log_file" + DEBIAN_FRONTEND=noninteractive apt -y \ + -o Dpkg::Options::='--force-confdef' \ + -o Dpkg::Options::='--force-confold' \ + full-upgrade 2> >(tee -a "$log_file" >&2) + upgrade_exit_code=$? + echo -e + { + echo + echo "--- Packages changed (exit $upgrade_exit_code) ---" + pmx_dpkg_changes_since "$dpkg_mark" + } >> "$log_file" + + clear + show_proxmenux_logo + msg_title "$(translate "$SCRIPT_TITLE")" + cat "$screen_capture" + rm -f "$screen_capture" + + if [ $upgrade_exit_code -ne 0 ]; then + 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 + + pmx_record_execution "Remove unused packages" "apt-get -y autoremove" + apt-get -y autoremove > /dev/null 2>&1 || true + apt-get -y autoclean > /dev/null 2>&1 || true + msg_ok "$(translate "Cleanup finished")" + + local end_time duration minutes seconds + end_time=$(date +%s) + duration=$((end_time - start_time)) + minutes=$((duration / 60)) + seconds=$((duration % 60)) + + echo -e "${TAB}${BGN}$(translate "====== PBS UPDATE COMPLETED ======")${CL}" + echo -e "${TAB}${GN}⏱️ $(translate "Duration")${CL}: ${BL}${minutes}m ${seconds}s${CL}" + echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$log_file${CL}" + echo -e "${TAB}${GN}📦 $(translate "Packages upgraded")${CL}: ${BL}$upgradable${CL}" + echo -e "${TAB}${GN}🗄️ $(translate "Proxmox Backup Server")${CL}: ${BL}$(dpkg-query -W -f='${Version}' proxmox-backup-server 2>/dev/null) (Debian $OS_CODENAME)${CL}" + + { + echo + echo "Finished: $(date -Iseconds) — ${minutes}m ${seconds}s" + echo "Running: proxmox-backup-server $(dpkg-query -W -f='${Version}' proxmox-backup-server 2>/dev/null)" + } >> "$log_file" + + if [ -f /var/run/reboot-required ] || [ "$(uname -r)" != "$(ls -1 /boot/vmlinuz-* 2>/dev/null | sed 's|/boot/vmlinuz-||' | sort -V | tail -1)" ]; then + msg_warn "$(translate "A newer kernel is installed. Reboot before the next backup window to use it.")" + fi + msg_ok "$(translate "Proxmox Backup Server update completed.")" +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + update_pbs +fi diff --git a/scripts/global/update-pve8.sh b/scripts/global/update-pve8.sh index 3db8cfd9..34f36267 100644 --- a/scripts/global/update-pve8.sh +++ b/scripts/global/update-pve8.sh @@ -188,7 +188,8 @@ EOF pmx_record_execution "Remove unused packages" "apt-get -y autoremove" apt-get -y autoremove > /dev/null 2>&1 || true apt-get -y autoclean > /dev/null 2>&1 || true - return 0 + # Not an error, but the system was not updated. + return 3 elif [[ $MENU_RESULT -eq 2 ]]; then msg_ok "$(translate "System is already up to date. No update needed.")" echo -e "\nSystem is already up to date." >> "$log_file" @@ -280,6 +281,12 @@ EOF tput ed tput cnorm msg_ok "$(translate "System upgrade completed")" + else + tput rc + tput ed + tput cnorm + msg_error "$(translate "Proxmox VE 8 package upgrade failed. Review the APT output before retrying.")" + return 1 fi diff --git a/scripts/gpu_tpu/add_gpu_vm.sh b/scripts/gpu_tpu/add_gpu_vm.sh index bbbb194a..655418b4 100644 --- a/scripts/gpu_tpu/add_gpu_vm.sh +++ b/scripts/gpu_tpu/add_gpu_vm.sh @@ -1121,6 +1121,7 @@ analyze_iommu_group() { local group_dir="/sys/kernel/iommu_groups/${IOMMU_GROUP}/devices" local display_lines="" local extra_devices=0 + local other_slot_devices=0 for dev_path in "${group_dir}/"*; do [[ -e "$dev_path" ]] || continue @@ -1149,8 +1150,16 @@ analyze_iommu_group() { display_lines+=" • ${dev} ${dev_name} [${dev_driver}]\n" [[ "$dev" != "$pci_full" ]] && extra_devices=$((extra_devices + 1)) + [[ "${dev%.*}" != "${pci_full%.*}" ]] && other_slot_devices=$((other_slot_devices + 1)) done + if [[ $other_slot_devices -gt 0 ]]; then + local box_h=$(( 13 + ${#IOMMU_DEVICES[@]} )) + (( box_h > 24 )) && box_h=24 + _pmx_msgbox "$(translate 'IOMMU Group') ${IOMMU_GROUP}" \ + "\n$(translate 'These devices share the IOMMU group of the selected GPU and will be assigned to the VM with it:')\n\n${display_lines}\n$(translate 'The host loses access to them. If the group includes a network or storage controller the host needs, cancel at the summary.')" \ + "$box_h" 82 + fi } # Returns 0 if the BDF at $1 is a real PCI audio device (class 04xx). diff --git a/scripts/gpu_tpu/nvidia_installer.sh b/scripts/gpu_tpu/nvidia_installer.sh index 0a364451..d927f34a 100644 --- a/scripts/gpu_tpu/nvidia_installer.sh +++ b/scripts/gpu_tpu/nvidia_installer.sh @@ -203,8 +203,14 @@ detect_driver_status() { if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then CURRENT_DRIVER_INSTALLED=true - # Register the installed driver version in components_status.json - update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}' + # Register the installed driver version in components_status.json, + # keeping the patched flag while the recorded version is unchanged. + local patched_json='{"patched":false}' + if [[ "$(jq -r '.nvidia_driver.version // empty' "$COMPONENTS_STATUS_FILE" 2>/dev/null)" == "$CURRENT_DRIVER_VERSION" ]] \ + && is_current_nvidia_patched; then + patched_json='{"patched":true}' + fi + update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" "$patched_json" fi fi fi diff --git a/scripts/help_info_menu.sh b/scripts/help_info_menu.sh index 770400ca..2808a0cd 100644 --- a/scripts/help_info_menu.sh +++ b/scripts/help_info_menu.sh @@ -95,6 +95,7 @@ show_system_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Check for Esc key press if [[ "$user_input" == $'\x1b' ]]; then @@ -184,17 +185,24 @@ show_vm_ct_commands() { echo -e " 8) ${GREEN}pct config ${NC} - $(translate 'Show container configuration. Use the correct ')" echo -e " 9) ${GREEN}qm destroy ${NC} - $(translate 'Delete a VM (irreversible). Use the correct ')" echo -e "10) ${GREEN}pct destroy ${NC} - $(translate 'Delete a CT (irreversible). Use the correct ')" - echo -e "11) ${GN}[Only with menu] Show CT users for permission mapping${NC} - $(translate 'root and real users only')" - echo -e "12) ${GREEN}pct exec -- getent passwd | column -t -s :${NC} - $(translate 'Show CT users in table format')" - echo -e "13) ${GREEN}pct exec -- ps aux --sort=-%mem | head${NC} - $(translate 'Top memory processes in CT')" - echo -e "14) ${GREEN}cat /etc/pve/qemu-server/.conf${NC} - $(translate 'View raw VM configuration file')" - echo -e "15) ${GREEN}cat /etc/pve/lxc/.conf${NC} - $(translate 'View raw CT configuration file')" - echo -e "16) ${GREEN}nano /etc/pve/qemu-server/.conf${NC} - $(translate 'Edit raw VM configuration file')" - echo -e "17) ${GREEN}nano /etc/pve/lxc/.conf${NC} - $(translate 'Edit raw CT configuration file')" + echo -e "11) ${GREEN}qm unlock ${NC} - $(translate 'Remove the lock left on a VM by an interrupted task')" + echo -e "12) ${GREEN}pct unlock ${NC} - $(translate 'Remove the lock left on a container by an interrupted task')" + # Translated label: pad it so its description lines up with 14-19. + local users_label pad + users_label="$(translate 'Show CT users and their UID/GID on the host')" + pad=$((51 - ${#users_label})); (( pad < 1 )) && pad=1 + echo -e "13) ${GN}${users_label}${NC}$(printf '%*s' "$pad" '')- $(translate 'root and real users only')" + echo -e "14) ${GREEN}pct exec -- getent passwd | column -t -s :${NC} - $(translate 'Show CT users in table format')" + echo -e "15) ${GREEN}pct exec -- ps aux --sort=-%mem | head${NC} - $(translate 'Top memory processes in CT')" + echo -e "16) ${GREEN}cat /etc/pve/qemu-server/.conf${NC} - $(translate 'View raw VM configuration file')" + echo -e "17) ${GREEN}cat /etc/pve/lxc/.conf${NC} - $(translate 'View raw CT configuration file')" + echo -e "18) ${GREEN}nano /etc/pve/qemu-server/.conf${NC} - $(translate 'Edit raw VM configuration file')" + echo -e "19) ${GREEN}nano /etc/pve/lxc/.conf${NC} - $(translate 'Edit raw CT configuration file')" echo -e " ${DEF}0) $(translate ' Back to previous menu or Esc + Enter')" echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Check for Esc key press if [[ "$user_input" == $'\x1b' ]]; then @@ -212,7 +220,9 @@ show_vm_ct_commands() { 8) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"; read -r id; cmd="pct config $id" ;; 9) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r id; cmd="qm destroy $id" ;; 10) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"; read -r id; cmd="pct destroy $id" ;; - 11) + 11) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}"; read -r id; cmd="qm unlock $id" ;; + 12) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}"; read -r id; cmd="pct unlock $id" ;; + 13) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}" read -r id @@ -236,34 +246,34 @@ show_vm_ct_commands() { done ;; - 12) + 14) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}" read -r id cmd="pct exec $id -- getent passwd | column -t -s :" ;; - 13) + 15) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}" read -r id cmd="pct exec $id -- ps aux --sort=-%mem | head" ;; - 14) + 16) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}" read -r id cmd="cat /etc/pve/qemu-server/$id.conf" ;; - 15) + 17) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}" read -r id cmd="cat /etc/pve/lxc/$id.conf" ;; - 16) + 18) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}" read -r id cmd="nano /etc/pve/qemu-server/$id.conf" ;; - 17) + 19) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter CT ID: ')${CL}" read -r id cmd="nano /etc/pve/lxc/$id.conf" @@ -272,8 +282,10 @@ show_vm_ct_commands() { *) cmd="$user_input" ;; esac - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -308,7 +320,7 @@ show_storage_commands() { echo -e "16) ${GREEN}cat /etc/pve/storage.cfg${NC} - $(translate 'Show Proxmox storage configuration')" echo -e "17) ${GREEN}pvesm status${NC} - $(translate 'Show status of all storage pools')" echo -e "18) ${GREEN}pvesm list ${NC} - $(translate 'List content of specific storage')" - echo -e "19) ${GREEN}pvesm scan ${NC} - $(translate 'Scan storage for new content')" + echo -e "19) ${GREEN}pvesm scan nfs ${NC} - $(translate 'Discover NFS exports on a server')" echo -e "20) ${GREEN}qm importdisk ${NC} - $(translate 'Import disk image to VM')" echo -e "21) ${GREEN}qm set - ${NC} - $(translate 'Add physical disk to VM via') passthrough" echo -e "22) ${GREEN}qemu-img convert -O ${NC} - $(translate 'Convert disk image format')" @@ -324,6 +336,7 @@ show_storage_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Esc key exit if [[ "$user_input" == $'\x1b' ]]; then @@ -358,9 +371,9 @@ show_storage_commands() { cmd="pvesm list $store" ;; 19) - echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter storage ID: ')${CL}" - read -r store - cmd="pvesm scan $store" + echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter NFS server IP or hostname:') ${CL}" + read -r server + cmd="pvesm scan nfs $server" ;; 20) echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter VM ID: ')${CL}" @@ -550,8 +563,10 @@ show_storage_commands() { *) cmd="$user_input" ;; esac - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -588,6 +603,7 @@ show_network_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" if [[ "$user_input" == $'\x1b' ]]; then break @@ -630,8 +646,10 @@ show_network_commands() { *) cmd="$user_input" ;; esac - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -652,12 +670,13 @@ show_update_commands() { echo -e " 1) ${GREEN}apt update && apt upgrade -y${NC} - $(translate 'Update and upgrade all system packages')" echo -e " 2) ${GREEN}apt dist-upgrade -y${NC} - $(translate 'Full system upgrade, including dependencies')" echo -e " 3) ${GREEN}pveupdate${NC} - $(translate 'Update Proxmox package lists')" - echo -e " 4) ${GREEN}pveupgrade${NC} - $(translate 'Show available Proxmox upgrades')" + echo -e " 4) ${GREEN}pveupgrade${NC} - $(translate 'Start the interactive Proxmox package upgrade')" echo -e " 5) ${GREEN}apt autoremove --purge${NC} - $(translate 'Remove unused packages and their config')" echo -e " ${DEF}0) $(translate ' Back to previous menu or Esc + Enter')" echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Check for Esc key press if [[ "$user_input" == $'\x1b' ]]; then @@ -674,8 +693,10 @@ show_update_commands() { *) cmd="$user_input" ;; esac - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -698,7 +719,7 @@ show_gpu_commands() { echo -e " 4) ${GREEN}dmesg -T | grep -Ei 'DMAR|IOMMU|vfio|pcie'${NC} - $(translate 'Inspect passthrough/kernel events')" echo -e " 5) ${GREEN}find /sys/kernel/iommu_groups -type l${NC} - $(translate 'List IOMMU group mapping')" echo -e " 6) ${GREEN}lsmod | grep -E 'vfio|nvidia|amdgpu|apex'${NC} - $(translate 'Check loaded GPU/TPU modules')" - echo -e " 7) ${GREEN}grep -R \"vfio-pci|blacklist\" /etc/modprobe.d${NC} - $(translate 'Review passthrough config files')" + echo -e " 7) ${GREEN}grep -R \"vfio-pci|blacklist\" /etc/modprobe.d${NC} - $(translate 'Review passthrough config files')" echo -e " 8) ${GREEN}nvidia-smi${NC} - $(translate 'Check NVIDIA driver and devices')" echo -e " 9) ${GREEN}qm config | grep 'hostpci|bios'${NC} - [T] $(translate 'Check VM passthrough settings')" echo -e "10) ${GREEN}pct config | grep 'dev|lxc.cgroup2'${NC} - [T] $(translate 'Check LXC GPU/TPU mapping')" @@ -713,6 +734,7 @@ show_gpu_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" if [[ "$user_input" == $'\x1b' ]]; then break @@ -794,8 +816,10 @@ show_gpu_commands() { continue fi - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -830,6 +854,7 @@ show_zfs_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Detect ESC if [[ "$user_input" == $'\x1b' ]]; then @@ -883,8 +908,10 @@ show_zfs_commands() { *) cmd="$user_input" ;; esac - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -916,6 +943,7 @@ show_backup_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Check for Esc key press if [[ "$user_input" == $'\x1b' ]]; then @@ -967,8 +995,10 @@ show_backup_commands() { *) cmd="$user_input" ;; esac - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp @@ -1001,6 +1031,7 @@ show_tools_commands() { echo echo -en "${TAB}${BOLD}${YW}${HOLD}$(translate 'Enter a number, or write or paste a command: ') ${CL}" read -r user_input + cmd="" # Check for Esc key press if [[ "$user_input" == $'\x1b' ]]; then @@ -1063,8 +1094,10 @@ show_tools_commands() { fi fi - echo -e "\n${GREEN}> $cmd${NC}\n" - bash -c "$cmd" + if [[ -n "$cmd" ]]; then + echo -e "\n${GREEN}> $cmd${NC}\n" + bash -c "$cmd" + fi echo msg_success "$(translate 'Press ENTER to continue...')" read -r tmp diff --git a/scripts/menus/config_menu.sh b/scripts/menus/config_menu.sh index 547b2244..ae9695e3 100644 --- a/scripts/menus/config_menu.sh +++ b/scripts/menus/config_menu.sh @@ -744,7 +744,7 @@ show_version_info() { fi local translated_status=$(translate "$status") case "$status" in - "installed"|"already_installed"|"created"|"already_exists"|"upgraded"|"updated") + "installed"|"already_installed"|"installed_from_github"|"created"|"already_exists"|"upgraded"|"updated") info_message+="✓ $component: $translated_status\n" ;; *) @@ -851,6 +851,9 @@ uninstall_proxmenu() { else pmx_edit_file /etc/motd '/This system is optimised by: ProxMenux/d' fi + + # The change journal above writes under BASE_DIR again. + rm -rf "$BASE_DIR" echo "100" ; echo "Uninstallation complete!" sleep 1 diff --git a/scripts/menus/menu_post_install.sh b/scripts/menus/menu_post_install.sh index 447583e7..d4e63cda 100644 --- a/scripts/menus/menu_post_install.sh +++ b/scripts/menus/menu_post_install.sh @@ -28,6 +28,12 @@ fi load_language initialize_cache +PMX_ON_PBS=false +if [[ -f "$LOCAL_SCRIPTS/global/platform-functions.sh" ]]; then + source "$LOCAL_SCRIPTS/global/platform-functions.sh" + pmx_is_pbs && PMX_ON_PBS=true +fi + # ========================================================== confirm_and_run() { local name="$1" @@ -55,8 +61,13 @@ confirm_automated_script() { local script_info="" - 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" + 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" + 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 "Install and configure") \Z4Log2RAM\Z0 $(translate "(only on SSD/NVMe) to protect your disk")\n" @@ -229,6 +240,12 @@ declare -a COMMUNITY_SCRIPTS=( "Proxmox VE Post Install|Helper-Scripts|bash -c \"\$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pve-install.sh); msg_success \\\"\$(translate 'Press ENTER to continue...')\\\"; read -r _\"" "Proxmox VE Microcode|Helper-Scripts|bash -c \"\$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/microcode.sh); msg_success \\\"\$(translate 'Press ENTER to continue...')\\\"; read -r _\"" ) +if [[ "$PMX_ON_PBS" == true ]]; then + COMMUNITY_SCRIPTS=( + "Proxmox Backup Server Post Install|Helper-Scripts|bash -c \"\$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pbs-install.sh); msg_success \\\"\$(translate 'Press ENTER to continue...')\\\"; read -r _\"" + "Proxmox Backup Server Microcode|Helper-Scripts|bash -c \"\$(wget -qLO - https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/pbs-microcode.sh); msg_success \\\"\$(translate 'Press ENTER to continue...')\\\"; read -r _\"" + ) +fi # ========================================================== @@ -271,6 +288,12 @@ post_install_label() { "Proxmox VE Microcode") echo "Proxmox VE Microcode" ;; + "Proxmox Backup Server Post Install") + echo "Proxmox Backup Server Post Install" + ;; + "Proxmox Backup Server Microcode") + echo "Proxmox Backup Server Microcode" + ;; "Community Scripts") translate "Community Scripts" ;; diff --git a/scripts/menus/pbs_menu.sh b/scripts/menus/pbs_menu.sh index 86f30ca2..99a8d76c 100644 --- a/scripts/menus/pbs_menu.sh +++ b/scripts/menus/pbs_menu.sh @@ -2,8 +2,8 @@ # ========================================================== # ProxMenux - Proxmox Backup Server menu # ========================================================== -# PBS exposes compatible host tools; PVE guest/repository optimizations -# stay in the PVE menu. +# PBS exposes compatible host tools and its own post-install profile; +# PVE guest optimizations stay in the PVE menu. # ========================================================== BASE_DIR="/usr/local/share/proxmenux" LOCAL_SCRIPTS="$BASE_DIR/scripts" @@ -31,20 +31,22 @@ pbs_show() { while true; do choice=$(dialog --clear --backtitle "ProxMenux" --title "Proxmox Backup Server" \ - --menu "$(translate 'Select an option:')" 18 75 8 \ - 1 "$(translate 'PBS version and packages')" \ - 2 "$(translate 'PBS datastores')" \ - 3 "$(translate 'Recent PBS tasks')" \ - 4 "$(translate 'System Utilities Installer')" \ - 5 "$(translate 'Update PBS using configured repositories')" \ + --menu "$(translate 'Select an option:')" 19 75 9 \ + 1 "$(translate "Settings post-install Proxmox")" \ + 2 "$(translate 'PBS version and packages')" \ + 3 "$(translate 'PBS datastores')" \ + 4 "$(translate 'Recent PBS tasks')" \ + 5 "$(translate 'System Utilities Installer')" \ + 6 "$(translate 'Update PBS using configured repositories')" \ s "$(translate 'Settings')" \ 0 "$(translate 'Exit')" 2>&1 >/dev/tty) || break case "$choice" in - 1) pbs_show "$(translate 'PBS version and packages')" proxmox-backup-manager versions --verbose ;; - 2) pbs_show "$(translate 'PBS datastores')" proxmox-backup-manager datastore list ;; - 3) pbs_show "$(translate 'Recent PBS tasks')" proxmox-backup-manager task list --all true --limit 20 ;; - 4) bash "$LOCAL_SCRIPTS/utilities/system_utils.sh" ;; - 5) bash "$LOCAL_SCRIPTS/utilities/pbs_update.sh" ;; + 1) exec bash "$LOCAL_SCRIPTS/menus/menu_post_install.sh" ;; + 2) pbs_show "$(translate 'PBS version and packages')" proxmox-backup-manager versions --verbose ;; + 3) pbs_show "$(translate 'PBS datastores')" proxmox-backup-manager datastore list ;; + 4) pbs_show "$(translate 'Recent PBS tasks')" proxmox-backup-manager task list --all true --limit 20 ;; + 5) bash "$LOCAL_SCRIPTS/utilities/system_utils.sh" ;; + 6) bash "$LOCAL_SCRIPTS/utilities/pbs_update.sh" ;; s) exec bash "$LOCAL_SCRIPTS/menus/config_menu.sh" ;; 0) break ;; esac diff --git a/scripts/post_install/auto_post_install.sh b/scripts/post_install/auto_post_install.sh index 25e6289f..83f84a82 100644 --- a/scripts/post_install/auto_post_install.sh +++ b/scripts/post_install/auto_post_install.sh @@ -59,7 +59,12 @@ initialize_cache if [[ -f "$LOCAL_SCRIPTS/global/platform-functions.sh" ]]; then source "$LOCAL_SCRIPTS/global/platform-functions.sh" - pmx_require_pve || exit 1 +fi +# Proxmox Backup Server runs the same host optimizations; repositories, +# package upgrade and the subscription notice follow its own tooling. +PMX_ON_PBS=false +if declare -F pmx_is_pbs >/dev/null && pmx_is_pbs; then + PMX_ON_PBS=true fi # Global variables @@ -142,6 +147,10 @@ $(translate "Do you want to continue anyway?")" 13 70 apt_upgrade() { + if [[ "$PMX_ON_PBS" == true ]]; then + bash "$LOCAL_SCRIPTS/global/update-pbs.sh" + return + fi local pve_version pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1) @@ -171,6 +180,20 @@ apt_upgrade() { remove_subscription_banner() { local FUNC_VERSION="1.0" # 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" \ + --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." + return 1 + fi + if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"; then + msg_error "$(translate "Subscription banner removal failed")" + return 1 + fi + register_tool "subscription_banner" true "$FUNC_VERSION" + return 0 + fi local pve_version pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1) @@ -225,6 +248,17 @@ setup_proxmox_repositories() { msg_info2 "$(translate "Configuring Proxmox APT repositories...")" + if [[ "$PMX_ON_PBS" == true ]]; then + if ! ensure_pbs_repositories; then + msg_error "$(translate "Failed to configure Proxmox Backup Server repositories")" + register_tool "proxmox_repos" false "$FUNC_VERSION" + return 1 + fi + register_tool "proxmox_repos" true "$FUNC_VERSION" + msg_success "$(translate "Proxmox Backup Server APT repositories configured")" + return 0 + fi + if ! ensure_repositories; then msg_error "$(translate "Failed to configure Proxmox repositories")" register_tool "proxmox_repos" false "$FUNC_VERSION" @@ -524,7 +558,7 @@ force_apt_ipv4() { # ========================================================== apply_network_optimizations() { - local FUNC_VERSION="1.1" + local FUNC_VERSION="1.2" pmx_journal_context "apply_network_optimizations" "$FUNC_VERSION" # description: Tune TCP buffers, somaxconn, IPv4 hardening and disable rp_filter on fw bridges (PVE 9 compatible). msg_info "$(translate "Optimizing network settings...")" @@ -622,14 +656,16 @@ RemainAfterExit=yes WantedBy=multi-user.target EOF - pmx_write_file /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules <<'EOF' + pmx_remove_file /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules + + pmx_write_file /etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules <<'EOF' ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwbr*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k" ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwln*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k" ACTION=="add", SUBSYSTEM=="net", KERNEL=="fwpr*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k" ACTION=="add", SUBSYSTEM=="net", KERNEL=="tap*", RUN+="/usr/local/sbin/proxmenux-fwbr-tune %k" EOF - chmod 0644 /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules - chown root:root /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules + chmod 0644 /etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules + chown root:root /etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules systemctl daemon-reload >/dev/null 2>&1 || true udevadm control --reload-rules >/dev/null 2>&1 || true diff --git a/scripts/post_install/customizable_post_install.sh b/scripts/post_install/customizable_post_install.sh index ca60b752..81601230 100644 --- a/scripts/post_install/customizable_post_install.sh +++ b/scripts/post_install/customizable_post_install.sh @@ -53,7 +53,12 @@ initialize_cache if [[ -f "$LOCAL_SCRIPTS/global/platform-functions.sh" ]]; then source "$LOCAL_SCRIPTS/global/platform-functions.sh" - pmx_require_pve || exit 1 +fi +# On Proxmox Backup Server the menu lists only the options that apply to +# it; repositories, upgrade and the subscription notice use its tooling. +PMX_ON_PBS=false +if declare -F pmx_is_pbs >/dev/null && pmx_is_pbs; then + PMX_ON_PBS=true fi # Load shared global functions @@ -122,6 +127,17 @@ setup_proxmox_repositories() { msg_info2 "$(translate "Configuring Proxmox APT repositories...")" + if [[ "$PMX_ON_PBS" == true ]]; then + if ! ensure_pbs_repositories; then + msg_error "$(translate "Failed to configure Proxmox Backup Server repositories")" + register_tool "proxmox_repos" false "$FUNC_VERSION" + return 1 + fi + register_tool "proxmox_repos" true "$FUNC_VERSION" + msg_success "$(translate "Proxmox Backup Server APT repositories configured")" + return 0 + fi + if ! ensure_repositories; then msg_error "$(translate "Failed to configure Proxmox repositories")" register_tool "proxmox_repos" false "$FUNC_VERSION" @@ -248,6 +264,10 @@ EOF apt_upgrade() { + if [[ "$PMX_ON_PBS" == true ]]; then + bash "$LOCAL_SCRIPTS/global/update-pbs.sh" + return + fi local pve_version pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1) @@ -2376,6 +2396,14 @@ EOF remove_subscription_banner() { local FUNC_VERSION="1.0" # description: Patch the Proxmox web UI to suppress the "no valid subscription" dialog (PVE 8 + 9 variants supported). + if [[ "$PMX_ON_PBS" == true ]]; then + if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"; then + msg_error "$(translate "Subscription banner removal failed")" + return 1 + fi + register_tool "subscription_banner" true "$FUNC_VERSION" + return 0 + fi local pve_version pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1) @@ -3143,8 +3171,11 @@ EOF } - ensure_aliases - msg_ok "$(translate "Aliases added to .bashrc")" + # The aliases call Proxmox VE tools. + if [[ "$PMX_ON_PBS" != true ]]; then + ensure_aliases + msg_ok "$(translate "Aliases added to .bashrc")" + fi msg_success "$(translate "Figurine installation and configuration completed successfully.")" register_tool "figurine" true "$FUNC_VERSION" @@ -3549,7 +3580,8 @@ EOF SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MAILTO="" -*/5 * * * * root /usr/local/bin/log2ram-check.sh >/dev/null 2>&1 +# nice/ionice keep the check off the priority queue for scheduled tasks. +3-59/10 * * * * root nice -n 19 ionice -c 3 /usr/local/bin/log2ram-check.sh >/dev/null 2>&1 EOF chmod 0644 /etc/cron.d/log2ram-auto-sync chown root:root /etc/cron.d/log2ram-auto-sync @@ -3993,6 +4025,19 @@ main_menu() { "Optional|Install and configure Log2RAM|LOG2RAM" ) + if [[ "$PMX_ON_PBS" == true ]]; then + local -A pve_only=( + [GUESTAGENT]=1 [VFIO_IOMMU]=1 [OPENVSWITCH]=1 [VZDUMP]=1 [PVEAM]=1 + [CEPH]=1 [ENABLE_HA]=1 [OVHRTM]=1 [PIGZ]=1 + ) + local pbs_options=() option_key + for option in "${options[@]}"; do + option_key="${option##*|}" + [[ -n "${pve_only[$option_key]:-}" ]] || pbs_options+=("$option") + done + options=("${pbs_options[@]}") + fi + IFS=$'\n' sorted_options=($(for option in "${options[@]}"; do IFS='|' read -r category description function_name <<< "$option" printf "%d|%s|%s|%s\n" "${category_order[$category]:-999}" "$category" "$description" "$function_name" diff --git a/scripts/post_install/uninstall-tools.sh b/scripts/post_install/uninstall-tools.sh index 63b22fa1..91f90867 100644 --- a/scripts/post_install/uninstall-tools.sh +++ b/scripts/post_install/uninstall-tools.sh @@ -266,6 +266,12 @@ uninstall_subscription_banner() { fi fi + # Proxmox Backup Server ships the same widget toolkit without the PVE packages. + local -a ui_packages=(pve-manager proxmox-widget-toolkit libjs-extjs libpve-http-server-perl) + if [[ "$(dpkg-query -W -f='${Status}' proxmox-backup-server 2>/dev/null)" == "install ok installed" ]]; then + ui_packages=(proxmox-widget-toolkit libjs-extjs) + fi + if [[ "$restored" == false ]]; then msg_info "$(translate "Performing complete package reinstallation...")" @@ -274,7 +280,7 @@ uninstall_subscription_banner() { # Reinstall packages with force-confnew to restore original configs if apt-get --reinstall -o Dpkg::Options::="--force-confnew" install \ - pve-manager proxmox-widget-toolkit libjs-extjs libpve-http-server-perl -y >/dev/null 2>&1; then + "${ui_packages[@]}" -y >/dev/null 2>&1; then msg_ok "$(translate "Reinstalled Proxmox packages successfully")" restored=true else @@ -286,7 +292,7 @@ uninstall_subscription_banner() { # Second pass reinstallation to ensure everything is clean apt-get update >/dev/null 2>&1 - apt-get --reinstall install proxmox-widget-toolkit pve-manager libjs-extjs libpve-http-server-perl -y >/dev/null 2>&1 || true + apt-get --reinstall install "${ui_packages[@]}" -y >/dev/null 2>&1 || true fi msg_info "$(translate "Cleaning cached files...")" @@ -575,7 +581,7 @@ uninstall_system_limits() { # Remove systemd limits (restore defaults) for file in /etc/systemd/system.conf /etc/systemd/user.conf; do if [ -f "$file" ]; then - pmx_edit_file "$file" '/^DefaultLimitNOFILE=256000/d' + pmx_edit_file "$file" -e '/^DefaultLimitNOFILE=256000$/d' -e '/^DefaultLimitNOFILE=1048576$/d' fi done @@ -588,7 +594,7 @@ uninstall_system_limits() { # Remove ulimit from profile if [ -f /root/.profile ]; then - pmx_edit_file /root/.profile '/ulimit -n 256000/d' + pmx_edit_file /root/.profile -e '/^ulimit -n 256000$/d' -e '/^ulimit -n 1048576$/d' fi # Reload sysctl diff --git a/scripts/utilities/proxmox-upgrade-pve8-to-pve9-manual-guide.sh b/scripts/utilities/proxmox-upgrade-pve8-to-pve9-manual-guide.sh index 322f1bf7..eeb95d28 100644 --- a/scripts/utilities/proxmox-upgrade-pve8-to-pve9-manual-guide.sh +++ b/scripts/utilities/proxmox-upgrade-pve8-to-pve9-manual-guide.sh @@ -95,7 +95,7 @@ show_proxmox_upgrade_manual_guide() { show_command "2.1" \ "${YW}$(translate "If this node runs hyper-converged Ceph: ensure Ceph is 19.x (Squid) BEFORE upgrading PVE.")${CL}\n\n" \ "ceph --version" \ - "$(translate "If not 19.x, upgrade Ceph (Reef→Squid) first per the official guide:") ${BL}https://pve.proxmox.com/wiki/Ceph_Squid${CL}" \ + "$(translate "If not 19.x, upgrade Ceph (Reef→Squid) first per the official guide:") ${BL}https://pve.proxmox.com/wiki/Ceph_Reef_to_Squid${CL}" \ "\n" diff --git a/scripts/utilities/upgrade_pve8_to_pve9.sh b/scripts/utilities/upgrade_pve8_to_pve9.sh index 2ba2bade..6c0c7e89 100644 --- a/scripts/utilities/upgrade_pve8_to_pve9.sh +++ b/scripts/utilities/upgrade_pve8_to_pve9.sh @@ -36,8 +36,8 @@ # broken repo config). # - dist-upgrade with --force-confdef --force-confold in # unattended mode; user-driven dpkg prompts in interactive mode. -# - Post-upgrade: installs grub-efi-amd64 on EFI hosts, restarts -# pve-manager, re-runs pve8to9 to surface residual issues. +# - Post-upgrade: installs grub-efi-amd64 on EFI hosts and +# re-runs pve8to9 to surface residual issues. # - Reboot prompt at the end. # - Full log written to /var/log/pve8-a-pve9-.log. # ========================================================== @@ -117,7 +117,7 @@ ask_run_mode() { msg_ok "$(translate "Run mode: Unattended")" export DEBIAN_FRONTEND=noninteractive export APT_LISTCHANGES_FRONTEND=none - exit 0 + return 0 fi @@ -625,7 +625,14 @@ apt_upgrade() { exit 1 else - bash "$LOCAL_SCRIPTS/global/update-pve8.sh" + local update_rc=0 + bash "$LOCAL_SCRIPTS/global/update-pve8.sh" || update_rc=$? + if [[ $update_rc -eq 3 ]]; then + exit 0 + elif [[ $update_rc -ne 0 ]]; then + msg_error "$(translate "The Proxmox VE 8 update did not finish. The upgrade to Proxmox VE 9 was not started.")" + exit 1 + fi hash -r fi @@ -641,10 +648,6 @@ regenerate_pve_cache() { mkdir -p /var/lib/pve-manager chmod 755 /var/lib/pve-manager - systemctl restart pve-manager 2>/dev/null || true - - sleep 2 - timeout 10 pvesh get /nodes/localhost/apt/update 2>/dev/null || true msg_ok "$(translate "PVE cache regenerated")" @@ -1009,6 +1012,13 @@ else bash -lc "apt-get dist-upgrade" fi +# pve-manager 9 ships pve-enterprise.sources during the upgrade. +if [[ "$REPO_MODE" == "no-subscription" && -f /etc/apt/sources.list.d/pve-enterprise.sources ]] \ + && ! grep -qi '^Enabled:[[:space:]]*\(false\|no\)' /etc/apt/sources.list.d/pve-enterprise.sources; then + disable_enterprise_repo_if_present + msg_ok "$(translate "Enterprise Proxmox repository disabled")" +fi + # ========================================================== @@ -1029,7 +1039,7 @@ if [[ -d /sys/firmware/efi ]]; then run_step \ "Installing grub-efi-amd64" \ "grub-efi-amd64 installed or already present" \ - "apt-get install grub-efi-amd64 >/dev/null 2>&1" + "apt-get install -y grub-efi-amd64 >/dev/null 2>&1" fi fi diff --git a/scripts/utils.sh b/scripts/utils.sh index 60255e40..1deb1dd6 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -271,6 +271,18 @@ translate() { echo "$cached" return fi + # The catalog stores texts trimmed; keep the caller's surrounding spaces. + local lead="${text%%[![:space:]]*}" + local core="${text#"$lead"}" + local trail="${core##*[![:space:]]}" + core="${core%"$trail"}" + if [ -n "$core" ] && [ "$core" != "$text" ]; then + cached=$(jq -r --arg text "$core" '.[$text] // .[($text | gsub("\\\\n"; "\n"))] // empty' "$lang_file" 2>/dev/null) + if [ -n "$cached" ]; then + echo "${lead}${cached}${trail}" + return + fi + fi fi echo "$text" diff --git a/tests/lxc_updates/test_native_app_discovery_ui.cjs b/tests/lxc_updates/test_native_app_discovery_ui.cjs new file mode 100644 index 00000000..2ffa802e --- /dev/null +++ b/tests/lxc_updates/test_native_app_discovery_ui.cjs @@ -0,0 +1,199 @@ +// Execute the real panel, handlers and editor with inert hook/UI/API fixtures. +// This is component interaction evidence, not browser or Proxmox acceptance. +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const vm = require('node:vm') +const root = path.resolve(__dirname, '../../AppImage') +const ts = require(path.join(root, 'node_modules/typescript')) +const source = fs.readFileSync(path.join(root, 'components/lxc-app-panel.tsx'), 'utf8') +const compiled = ts.transpileModule(source, {compilerOptions: { + module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, jsx: ts.JsxEmit.ReactJSX, +}, reportDiagnostics: true}) +assert.equal(compiled.diagnostics.length, 0) + +const flatten = node => !node || typeof node !== 'object' ? [] : [node, ...( + [node.props?.children].flat(Infinity).flatMap(flatten) +)] +const text = node => node == null || node === false ? '' : typeof node !== 'object' + ? String(node) : [node.props?.children].flat(Infinity).map(text).join(' ') +const candidate = { + slug: 'native-' + 'a'.repeat(24), discovery_id: 'native-' + 'a'.repeat(24), + name: 'handmade', source: 'native_metadata', installed_version: null, + evidence: [{kind: 'installation_file', value: '/opt/handmade/server'}], default_ports: [], +} + +function fixture({nativeCandidate = candidate} = {}) { + let cursor = 0, tree, props, effects = [], states = [], refs = [], effectStates = [] + const calls = [], cachedWrites = [] + let native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false} + let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete' + let cacheGate, resolveCache, cachePending = false + const seed = {sidecar: {vmid: 101, apps: []}, suggestions: {extras: [], port_suggestions: []}} + const react = { + useState(initial) { + const index = cursor++ + if (!(index in states)) states[index] = typeof initial === 'function' ? initial() : initial + return [states[index], value => {states[index] = typeof value === 'function' ? value(states[index]) : value}] + }, + useRef(value) {const index = cursor++; return refs[index] ||= {current: value}}, + useMemo(fn) {cursor++; return fn()}, useCallback(fn) {cursor++; return fn}, + useEffect(fn, deps) { + const index = cursor++, prior = effectStates[index] + if (!prior || !deps || deps.some((value, i) => value !== prior.deps?.[i])) { + effects.push(() => {prior?.cleanup?.(); effectStates[index] = {deps, cleanup: fn()}}) + } + }, + } + const api = async (url, options = {}) => { + calls.push({url, ...options}) + if (url.endsWith('/native-discovery')) { + if (options.method !== 'POST' && cachePending) { + cachePending = false + const oldSnapshot = structuredClone(native) + await cacheGate + return oldSnapshot + } + if (options.method === 'POST') { + if (scanGate) await scanGate + native = {...native, status: scanStatus, candidates: [nativeCandidate]} + } + return structuredClone(native) + } + if (url.endsWith('/apps/suggestions')) return seed.suggestions + if (url.endsWith('/apps') && options.method === 'POST') { + if (saveGate) await saveGate + return {vmid: Number(url.split('/')[3]), apps: [{id: 'saved', ...JSON.parse(options.body), state: {}}]} + } + if (url.endsWith('/adguard-setup')) return {available: false} + return [] + } + const cache = { + getLxcAppsCached: () => null, fetchLxcApps: async () => seed, + subscribeLxcApps: () => () => {}, setLxcAppsCached: (...args) => cachedWrites.push(args), + } + const exports = {} + vm.runInNewContext(compiled.outputText, {exports, console, setTimeout, clearTimeout, + require: name => { + if (name === 'react') return react + if (name === 'react/jsx-runtime') return {jsx: (type, props) => ({type, props}), jsxs: (type, props) => ({type, props})} + if (name.includes('api-config')) return {fetchApi: api} + if (name.includes('lxc-apps-cache')) return cache + if (name.includes('i18n/provider')) return {useT: () => (key, values) => key + (values ? JSON.stringify(values) : '')} + if (name.includes('category-color')) return {useIsLightTheme: () => false, categoryChipStyle: () => ({})} + if (name.includes('category-label')) return {getCategoryLabel: value => value} + return new Proxy({}, {get: (_, key) => key}) + }, + }) + function render(next = props || {vmid: 101, initialData: seed}) { + props = next; cursor = 0; effects = [] + tree = exports.LxcAppPanel(props) + for (const effect of effects) effect() + return tree + } + const settle = async () => {for (let i = 0; i < 6; i++) await Promise.resolve(); render()} + const button = key => { + const match = flatten(tree).find(n => n.type === 'Button' && text(n).includes('vmLxc.appEditor.' + key)) + assert.ok(match, 'Missing button ' + key) + return match + } + return {render, settle, calls, cachedWrites, button, tree: () => tree, + setNative: value => {native = value}, + holdInitialCache: () => {cachePending = true; cacheGate = new Promise(resolve => {resolveCache = resolve})}, + resolveInitialCache: () => resolveCache(), + setScanStatus: value => {scanStatus = value}, + holdScan: () => {scanGate = new Promise(resolve => {holdScan = resolve})}, resolveScan: () => holdScan(), + holdSave: () => {saveGate = new Promise(resolve => {holdSave = resolve})}, resolveSave: () => holdSave(), + } +} + +async function main() { + const f = fixture() + f.render(); await f.settle() + assert.equal(f.calls.filter(c => c.method === 'POST').length, 0, 'mount and cache reads never scan') + await f.button('searchApplications').props.onClick(); f.render() + assert.equal(f.calls.filter(c => c.url.endsWith('/native-discovery') && c.method === 'POST').length, 1) + assert.ok(text(f.tree()).includes('nativeVersionUnknown'), 'unknown version remains visible') + assert.ok(text(f.tree()).includes('/opt/handmade/server')) + await f.button('registerButton').props.onClick(); f.render() + assert.ok(text(f.tree()).includes('saveButton'), 'candidate opens actual editor') + f.button('cancelButton').props.onClick(); f.render() + assert.equal(f.calls.filter(c => /\/apps$/.test(c.url) && c.method === 'POST').length, 0, 'cancel never registers') + await f.button('registerButton').props.onClick(); f.render() + await f.button('saveButton').props.onClick(); f.render() + const saved = f.calls.find(c => /\/apps$/.test(c.url) && c.method === 'POST') + const payload = JSON.parse(saved.body) + assert.equal(saved.url, '/api/vms/101/apps') + assert.equal(payload.discovery_id, candidate.discovery_id) + assert.ok(!payload.helper_slug, 'native identity never becomes a helper') + assert.equal(payload.installed_via, '') + assert.equal(payload.update_method, 'none') + assert.equal(payload.update_command, '') + assert.equal(payload.repo, '') + assert.equal(payload.upstream_type, '') + + const hestia = fixture({nativeCandidate: { + ...candidate, name: 'Hestia', default_ports: [8083], scheme: 'https', + }}) + hestia.render(); await hestia.settle() + await hestia.button('searchApplications').props.onClick(); hestia.render() + await hestia.button('registerButton').props.onClick(); hestia.render() + await hestia.button('saveButton').props.onClick(); hestia.render() + const hestiaPayload = JSON.parse(hestia.calls.find(c => /\/apps$/.test(c.url) && c.method === 'POST').body) + assert.deepEqual(hestiaPayload.ports, [{port: 8083, scheme: 'https', web_path: ''}], + 'a verified Hestia panel pre-fills only its fixed HTTPS endpoint') + const delayedCache = fixture() + delayedCache.holdInitialCache() + delayedCache.render(); await delayedCache.settle() + await delayedCache.button('searchApplications').props.onClick(); delayedCache.render() + assert.ok(text(delayedCache.tree()).includes('/opt/handmade/server')) + delayedCache.resolveInitialCache(); await delayedCache.settle() + assert.ok(text(delayedCache.tree()).includes('/opt/handmade/server'), + 'a delayed initial cache read must not erase a completed explicit search') + + const partial = fixture() + partial.render(); await partial.settle(); partial.setScanStatus('partial') + await partial.button('searchApplications').props.onClick(); partial.render() + assert.ok(text(partial.tree()).includes('nativeScanPartial')) + assert.ok(text(partial.tree()).includes('nativeEvidenceStale')) + const failed = fixture() + failed.render(); await failed.settle(); failed.setScanStatus('error') + await failed.button('searchApplications').props.onClick(); failed.render() + assert.ok(text(failed.tree()).includes('nativeScanFailed')) + assert.equal(failed.calls.filter(c => c.url.endsWith('/apps/suggestions') && c.method === 'POST').length, 0) + + const editorSwitch = fixture() + editorSwitch.render(); await editorSwitch.settle() + await editorSwitch.button('searchApplications').props.onClick(); editorSwitch.render() + await editorSwitch.button('registerButton').props.onClick(); editorSwitch.render() + editorSwitch.render({vmid: 102}) + assert.ok(!text(editorSwitch.tree()).includes('saveButton'), 'old editor must disappear in the very first render of another CT') + + const late = fixture() + late.render(); await late.settle(); late.holdScan() + const scan = late.button('searchApplications').props.onClick() + late.render({vmid: 102}); await late.settle() + late.resolveScan(); await scan; late.render() + assert.ok(!text(late.tree()).includes('/opt/handmade/server'), 'late A scan cannot appear in B') + assert.equal(late.cachedWrites.length, 0) + assert.equal(late.calls.filter(c => c.url.endsWith('/apps/suggestions') && c.method === 'POST').length, 0) + + const saveLate = fixture() + saveLate.render(); await saveLate.settle() + await saveLate.button('searchApplications').props.onClick(); saveLate.render() + await saveLate.button('registerButton').props.onClick(); saveLate.render() + saveLate.holdSave(); const saving = saveLate.button('saveButton').props.onClick() + const priorWrites = saveLate.cachedWrites.length + saveLate.render({vmid: 102}); await saveLate.settle() + saveLate.resolveSave(); await saving; saveLate.render() + assert.equal(saveLate.cachedWrites.length, priorWrites, 'late save response does not update new target') + assert.equal(saveLate.calls.filter(c => /\/vms\/102\/apps$/.test(c.url) && c.method === 'POST').length, 0) + + const oci = fixture() + oci.render({vmid: 101, oci: {instance: true}, initialData: {sidecar: {apps: []}, suggestions: {oci_instance: {template_id: 'image-app'}, extras: []}}}) + await oci.settle() + await oci.button('refreshData').props.onClick() + assert.equal(oci.calls.filter(c => c.url.endsWith('/native-discovery') && c.method === 'POST').length, 0, 'OCI refresh never native-scans') + console.log('PASS native discovery: real panel Find → editor → Cancel/Save, no automatic scan, safe payload, CT switching and OCI refresh (fixtures)') +} +main().catch(error => {console.error(error); process.exitCode = 1}) diff --git a/tests/lxc_updates/test_observed_state_ui.cjs b/tests/lxc_updates/test_observed_state_ui.cjs index 7b659bed..4f2de690 100644 --- a/tests/lxc_updates/test_observed_state_ui.cjs +++ b/tests/lxc_updates/test_observed_state_ui.cjs @@ -162,8 +162,10 @@ assert.equal(ui.osStatus({ ...recent, available: false, count: 0 }).osUpdateStat assert.equal(ui.osStatus({ ...recent, error: "probe failed" }).osUpdateStatusKnown, false) assert.equal(ui.aggregate({ app_watches: [{ update_available: true, name: "App" }] }).last_check, null, "an app badge cannot invent an OS check timestamp") -assert.equal(ui.badge(legacy).props.children[0].type, "Clock") -assert.equal(ui.badge(legacy).props.children[1], "vmLxc.updates.checkStale") +assert.equal(ui.badge(legacy).props.children[0].type, "Arrow", + "an old check keeps the same badge as a recent one") +assert.equal(ui.badge(legacy).props.children[1], 51) +assert.equal(ui.badge({ ...legacy, available: false, count: 0 }), null) assert.equal(ui.badge({ ...recent, available: true, count: 2 }).props.children[1], 2) assert.equal(ui.badge({ ...recent, available: false, count: 0 }), null) assert.equal(requests.length, 0) diff --git a/tests/test_repository_policy_pbs.py b/tests/test_repository_policy_pbs.py new file mode 100644 index 00000000..e0a71d90 --- /dev/null +++ b/tests/test_repository_policy_pbs.py @@ -0,0 +1,103 @@ +"""Fixture-only PBS policy checks: never call host proxmox-backup-* tools or edit host APT sources.""" +import importlib.util +import json +from pathlib import Path +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +POLICY = ROOT / 'scripts/global/repository_policy.py' + + +def repo(components, *, uri='https://enterprise.proxmox.com/debian/pbs', suite='trixie', enabled=True): + return {'Types': ['deb'], 'URIs': [uri], 'Suites': [suite], + 'Components': components.split(), 'Enabled': enabled} + + +class PBSRepositoryPolicyTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + spec = importlib.util.spec_from_file_location('repository_policy', POLICY) + cls.module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(cls.module) + + def setUp(self): + self.calls = [] + self.status = 'notfound' + self.repositories = { + 'files': [ + {'path': '/etc/apt/sources.list.d/pbs-enterprise.sources', 'repositories': [repo('pbs-enterprise')]}, + {'path': '/etc/apt/sources.list.d/debian.sources', + 'repositories': [repo('main contrib', uri='http://deb.debian.org/debian')]}, + ], + 'errors': [], 'digest': 'fixture-digest', + 'standard-repos': [{'handle': 'enterprise'}, {'handle': 'no-subscription'}, {'handle': 'test'}], + } + + def run_policy(self, apply=False): + def run(args): + self.calls.append(args) + if args[:3] == ['proxmox-backup-manager', 'subscription', 'get']: + return json.dumps({'status': self.status, 'serverid': 'FIXTURE-SECRET'}) + self.assertEqual(args[:2], ['proxmox-backup-debug', 'api']) + action = args[2] + if action == 'get': + return json.dumps(self.repositories) + self.assertEqual(args[args.index('--digest') + 1], self.repositories['digest']) + if action == 'create': + path = args[args.index('--path') + 1] + index = int(args[args.index('--index') + 1]) + next(f for f in self.repositories['files'] if f['path'] == path)['repositories'][index]['Enabled'] = False + elif action == 'set': + self.repositories['files'].append({'path': '/etc/apt/sources.list.d/proxmox.sources', + 'repositories': [repo('pbs-no-subscription', uri='http://download.proxmox.com/debian/pbs')]}) + self.repositories['digest'] += '+' + return '' + with patch.object(self.module, 'run', side_effect=run): + return self.module.evaluate('trixie', apply=apply, product='pbs') + + def writes(self): + return [c for c in self.calls if c[:2] == ['proxmox-backup-debug', 'api'] and c[2] in ('create', 'set')] + + def test_fresh_pbs_offers_switch_without_writing(self): + self.assertEqual(self.run_policy(), 'offer') + self.assertEqual(self.writes(), []) + self.assertFalse(any(c[0] in ('pvesh', 'pvesubscription') for c in self.calls)) + + def test_consented_switch_disables_pbs_enterprise_then_adds_no_subscription(self): + self.assertEqual(self.run_policy(apply=True), 'changed') + writes = self.writes() + self.assertEqual([c[2] for c in writes], ['create', 'set']) + self.assertEqual(writes[0][writes[0].index('--path') + 1], '/etc/apt/sources.list.d/pbs-enterprise.sources') + self.assertEqual(writes[0][writes[0].index('--enabled') + 1], '0') + self.assertEqual(writes[1][writes[1].index('--handle') + 1], 'no-subscription') + + def test_active_subscription_preserves_enterprise(self): + self.status = 'active' + self.assertEqual(self.run_policy(apply=True), 'preserve') + self.assertEqual(self.writes(), []) + + def test_existing_no_subscription_is_preserved_without_lookup(self): + self.repositories['files'][0]['repositories'] = [repo('pbs-no-subscription')] + self.assertEqual(self.run_policy(apply=True), 'preserve') + self.assertFalse(any(c[0] == 'proxmox-backup-manager' for c in self.calls)) + + def test_unrecognized_subscription_status_cannot_mutate(self): + self.status = 'invalid' + with self.assertRaisesRegex(ValueError, 'proxmox-backup-manager'): + self.run_policy(apply=True) + self.assertEqual(self.writes(), []) + + def test_pve_channels_do_not_count_as_pbs_sources(self): + self.repositories['files'][0]['repositories'] = [repo('pve-no-subscription')] + self.assertEqual(self.run_policy(), 'offer') + + def test_wrong_suite_points_to_pbs_interface(self): + self.repositories['files'][0]['repositories'] = [repo('pbs-enterprise', suite='bookworm')] + with self.assertRaisesRegex(ValueError, 'Administration > Repositories'): + self.run_policy() + self.assertEqual(self.writes(), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/web/CONTRIBUTING-TRANSLATIONS.md b/web/CONTRIBUTING-TRANSLATIONS.md index ce9250cb..bd668edc 100644 --- a/web/CONTRIBUTING-TRANSLATIONS.md +++ b/web/CONTRIBUTING-TRANSLATIONS.md @@ -5,7 +5,12 @@ serves every published page under locale-prefixed URLs: - `/en/` — English, the source of truth - `/es/` — Spanish +- `/de/` — German +- `/fr/` — French +- `/it/` — Italian +- `/pt/` — Portuguese - `/sk/` — Slovak, with English fallback where needed +- `/sv/` — Swedish We use [`next-intl`](https://next-intl.dev) for the i18n plumbing. Anyone can translate the docs without writing TypeScript: most of the work is @@ -148,6 +153,18 @@ Example: ### 4. Test locally +From the repository root, check the available catalogs without using a +translation provider: + +```bash +python3 .github/scripts/check_web_docs_catalogs.py +``` + +For a full locale-completion change, add `--require-complete` to also require +every English file and leaf. This validates schema, placeholders, rich-text +tags, literal code and rendering metadata; it does not replace linguistic +review. Partial translations can continue to use the English fallback. + ```bash cd web npm run dev @@ -311,7 +328,10 @@ registers a renderer for them. If you want to add a language that isn't in the project yet: 1. Create `messages//common.json` and the page catalogs. The automated - builder can provide the initial baseline. + builder can provide the initial baseline when explicitly dispatched. It + does not run on merge: changing English and its translations together must + not trigger machine retranslation of the reviewed text. Offline validation + still runs automatically for website changes. 2. Review the shared navigation and a representative set of documentation pages with a native speaker. 3. Add the locale code to `routing.ts` only when the locale is ready to be diff --git a/web/app/[locale]/docs/about/code-of-conduct/page.tsx b/web/app/[locale]/docs/about/code-of-conduct/page.tsx index de18716b..53365824 100644 --- a/web/app/[locale]/docs/about/code-of-conduct/page.tsx +++ b/web/app/[locale]/docs/about/code-of-conduct/page.tsx @@ -24,23 +24,27 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s } } -async function getCodeOfConductContent(notFoundMsg: string, loadFailedMsg: string) { +async function getCodeOfConductContent(notFoundMsg: string, loadFailedMsg: string, translatedMarkdown?: string) { try { const codeOfConductPath = path.join(process.cwd(), "..", "CODE_OF_CONDUCT.md") - if (!fs.existsSync(codeOfConductPath)) { + if (!translatedMarkdown && !fs.existsSync(codeOfConductPath)) { console.error("CODE_OF_CONDUCT.md file not found.") return `

${notFoundMsg}

` } - const fileContents = fs.readFileSync(codeOfConductPath, "utf8") + const fileContents = translatedMarkdown || fs.readFileSync(codeOfConductPath, "utf8") const result = await remark() .use(gfm.default || gfm) .use(html) .process(fileContents) - return result.toString() + // Repository-relative policy links otherwise resolve under the docs URL. + return result.toString().replaceAll( + 'href="./SECURITY.md"', + 'href="https://github.com/MacRimi/ProxMenux/blob/main/SECURITY.md"', + ) } catch (error) { console.error("Error reading the CODE_OF_CONDUCT.md file", error) return `

${loadFailedMsg}

` @@ -71,7 +75,11 @@ export default async function CodeOfConductPage({ params }: { params: Promise<{ const { locale } = await params setRequestLocale(locale) const t = await getTranslations({ locale, namespace: "docs.about.codeOfConduct" }) - const codeOfConductContent = await getCodeOfConductContent(t("errors.notFound"), t("errors.loadFailed")) + const translatedMarkdown = t.has("contentMarkdown") ? t.raw("contentMarkdown") : undefined + const codeOfConductContent = await getCodeOfConductContent( + t("errors.notFound"), t("errors.loadFailed"), + typeof translatedMarkdown === "string" ? translatedMarkdown : undefined, + ) const cleanedInlineCode = cleanInlineCode(codeOfConductContent) const parsedContent = wrapCodeBlocksWithCopyable(cleanedInlineCode) diff --git a/web/app/[locale]/docs/post-install/page.tsx b/web/app/[locale]/docs/post-install/page.tsx index e4bc8304..89332b87 100644 --- a/web/app/[locale]/docs/post-install/page.tsx +++ b/web/app/[locale]/docs/post-install/page.tsx @@ -89,10 +89,12 @@ export default async function PostInstallPage({ docs: { postInstall: { routes: Route[] related: { items: RelatedItem[] } + pbs?: { items: string[] } } } } const routes = messages.docs.postInstall.routes const relatedItems = messages.docs.postInstall.related.items + const pbsItems = messages.docs.postInstall.pbs?.items ?? [] const code = (chunks: React.ReactNode) => {chunks} const strong = (chunks: React.ReactNode) => {chunks} @@ -210,6 +212,14 @@ export default async function PostInstallPage({ {t.rich("community.body", { em, code, a: communityAnchor, link: externalRepoLink })}

+

{t("pbs.heading")}

+

{t.rich("pbs.body", { strong })}

+
    + {pbsItems.map((_, i) => ( +
  • {t.rich(`pbs.items.${i}`, { strong, em })}
  • + ))} +
+

{t("related.heading")}

    {relatedItems.map((item) => ( diff --git a/web/app/[locale]/docs/post-install/storage/page.tsx b/web/app/[locale]/docs/post-install/storage/page.tsx index 1e763441..61d629d1 100644 --- a/web/app/[locale]/docs/post-install/storage/page.tsx +++ b/web/app/[locale]/docs/post-install/storage/page.tsx @@ -282,8 +282,8 @@ done < /usr/local/share/proxmenux/zfs_autotrim_pools`}

    {t("vzdump.changedTitle")}

    diff --git a/web/app/[locale]/docs/settings/show-version-information/page.tsx b/web/app/[locale]/docs/settings/show-version-information/page.tsx index dde89e34..8eb72e43 100644 --- a/web/app/[locale]/docs/settings/show-version-information/page.tsx +++ b/web/app/[locale]/docs/settings/show-version-information/page.tsx @@ -85,10 +85,8 @@ export default async function ShowVersionInformationPage({

{t("sampleHeading")}

-
{`Current ProxMenux version: 1.2.4
-
-Installation type:
-✓ Translation Version (Multi-language support)
+      
{`Current ProxMenux version: VERSION
+Release channel: Stable (main branch)
 
 Installed components:
 ✓ post_install_settings: installed
@@ -103,28 +101,24 @@ ProxMenux files:
 ✓ utils.sh → /usr/local/share/proxmenux/utils.sh
 ✓ config.json → /usr/local/share/proxmenux/config.json
 ✓ version.txt → /usr/local/share/proxmenux/version.txt
-✓ cache.json → /usr/local/share/proxmenux/cache.json
-
-Virtual Environment:
-✓ Installed → /opt/googletrans-env
-✓ pip: Installed → /opt/googletrans-env/bin/pip
 
 Current language:
-es`}
+en +✓ Translation files: /usr/local/share/proxmenux/lang/`}

{t("manualHeading")}

{`# Version
 cat /usr/local/share/proxmenux/version.txt
 
-# Install type detection (replicates the script's logic)
-[[ -d /opt/googletrans-env ]] && echo "venv: yes" || echo "venv: no"
-jq -r '.language // "missing"' /usr/local/share/proxmenux/config.json
+# Release channel metadata and installed translation catalogs
+jq '{beta_program, install_branch}' /usr/local/share/proxmenux/config.json
+ls /usr/local/share/proxmenux/lang/*.json
 
 # All component statuses
 jq 'to_entries[] | "\\(.key): \\(.value)"' /usr/local/share/proxmenux/config.json
 
 # Current language
-jq -r '.language' /usr/local/share/proxmenux/config.json`}
+jq -r '.language // "en"' /usr/local/share/proxmenux/config.json`}

{t("troubleshoot.heading")}

diff --git a/web/app/[locale]/docs/utils/upgrade-pve8-pve9/page.tsx b/web/app/[locale]/docs/utils/upgrade-pve8-pve9/page.tsx index c61e3837..d272b910 100644 --- a/web/app/[locale]/docs/utils/upgrade-pve8-pve9/page.tsx +++ b/web/app/[locale]/docs/utils/upgrade-pve8-pve9/page.tsx @@ -110,7 +110,7 @@ export default async function UpgradePve8Pve9Page({ ) const cephlink = (chunks: React.ReactNode) => ( / tags * because Next.js requires those at the root level. Lang is set to the - * default locale here and refined client-side from inside the locale - * layout so the page still renders correctly with JS disabled (defaults - * to English) and switches to the active locale once hydrated. + * default locale here; scripts/localize-export.mjs sets the exported + * document's language before search indexing, and LocaleHtmlSync keeps + * it in sync during client navigation. The root-only hydration suppression + * allows that intentional lang attribute change in the static export. */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} diff --git a/web/components/language-switcher.tsx b/web/components/language-switcher.tsx index aa084783..cede72da 100644 --- a/web/components/language-switcher.tsx +++ b/web/components/language-switcher.tsx @@ -29,7 +29,12 @@ export function LanguageSwitcher() { const labels: Record = { en: t("en"), es: t("es"), + de: t("de"), + fr: t("fr"), + it: t("it"), + pt: t("pt"), sk: t("sk"), + sv: t("sv"), } return ( diff --git a/web/i18n/routing.ts b/web/i18n/routing.ts index 41c0cfdc..ffc11c7f 100644 --- a/web/i18n/routing.ts +++ b/web/i18n/routing.ts @@ -19,7 +19,7 @@ import { defineRouting } from "next-intl/routing" * `CONTRIBUTING-TRANSLATIONS.md`. */ export const routing = defineRouting({ - locales: ["en", "es", "sk"], + locales: ["en", "es", "de", "fr", "it", "pt", "sk", "sv"], defaultLocale: "en", localePrefix: "always", }) diff --git a/web/messages/de/common.json b/web/messages/de/common.json new file mode 100644 index 00000000..c3aefb73 --- /dev/null +++ b/web/messages/de/common.json @@ -0,0 +1,254 @@ +{ + "site": { + "name": "ProxMenux", + "tagline": "Interaktives Menü und Web-Dashboard für Proxmox VE" + }, + "nav": { + "home": "Startseite", + "docs": "Dokumentation", + "documentation": "Dokumentation", + "guides": "Anleitungen", + "changelog": "Änderungsprotokoll", + "github": "GitHub", + "rss": "RSS", + "rssTitle": "RSS-Feed", + "menuOpen": "Menü öffnen" + }, + "footer": { + "sponsorHeading": "Projekt unterstützen", + "sponsorBody": "Wenn Sie das Projekt unterstützen möchten.", + "sponsorAlt": "Auf Ko-fi unterstützen", + "connectHeading": "Community", + "connectBody": "Beteiligen Sie sich an den Community-Diskussionen auf GitHub, um Hilfe zu erhalten, Ideen auszutauschen und zum Projekt beizutragen. Jede Idee ist willkommen!", + "joinDiscussion": "An der Diskussion teilnehmen", + "copyrightPrefix": "ProxMenux, ein gemeinschaftliches Open-Source-Projekt von", + "copyrightSuffix": "." + }, + "language": { + "switcher": "Sprache", + "en": "English", + "es": "Español", + "sk": "Slovenčina", + "de": "Deutsch", + "fr": "Français", + "it": "Italiano", + "pt": "Português", + "sv": "Svenska" + }, + "resources": { + "documentation": { + "title": "Dokumentation", + "description": "Systembeschreibung und Benutzeranleitungen" + }, + "changelog": { + "title": "Änderungsprotokoll", + "description": "Informationen zu den neuesten Aktualisierungen" + }, + "guides": { + "title": "Anleitungen", + "description": "Schrittweise Anleitungen für häufige Aufgaben" + }, + "github": { + "title": "GitHub-Repository", + "description": "Sehen Sie sich den Quellcode an." + } + }, + "docSidebar": { + "documentation": "Dokumentation", + "items": { + "introduction": "Einführung", + "installation": "Installation", + "proxmenuxMonitor": "ProxMenux Monitor", + "monitorOverview": "Übersicht", + "architecture": "Architektur", + "accessAuth": "Zugriff und Authentifizierung", + "dashboard": "Dashboard", + "dashboardSystemOverview": "Registerkarte Systemübersicht", + "dashboardApps": "Registerkarte Apps", + "dashboardStorage": "Registerkarte Speicher", + "dashboardNetwork": "Registerkarte Netzwerk", + "dashboardVmsLxcs": "Registerkarte VMs und LXCs", + "dashboardVmsLxcsApp": "App", + "dashboardVmsLxcsUpdates": "Aktualisierungen", + "dashboardHardware": "Registerkarte Hardware", + "dashboardSystemLogs": "Registerkarte Systemprotokolle", + "dashboardTerminal": "Registerkarte Terminal", + "dashboardSecurity": "Registerkarte Sicherheit", + "dashboardSettings": "Registerkarte Einstellungen", + "healthMonitor": "Zustandsüberwachung", + "auditReport": "Prüfung und Berichte", + "auditReportOverview": "Übersicht", + "auditReportAssessment": "Bewertung und Inventar", + "auditReportPolicy": "Richtlinie", + "auditReportChanges": "Änderungen", + "auditReportReports": "Berichte und Vergleiche", + "auditReportScope": "Umfang und Garantien", + "notifications": "Benachrichtigungen", + "aiAssistant": "KI-Assistent", + "apiReference": "API-Referenz", + "integrations": "Integrationen", + "ociManager": "OCI manager Apps", + "ociOverview": "Übersicht", + "ociArchitecture": "So wird ein OCI-Image umgesetzt", + "ociCustomImage": "Image außerhalb des Katalogs", + "ociStorageNetwork": "Daten, Pfade und Netzwerk", + "ociHardware": "Geräte und Beschleunigung", + "ociStacks": "Anwendungen mit mehreren Containern", + "ociLifecycle": "Installieren, aktualisieren und ändern", + "ociMonitor": "In ProxMenux Monitor", + "proxmenuxScripts": "ProxMenux Scripts", + "postInstallScript": "Skript zur Nachinstallation", + "postInstallOverview": "Übersicht", + "postInstallAutomated": "Automatisch", + "postInstallCustomizable": "Anpassbar", + "postInstallBasicSettings": "Grundeinstellungen", + "postInstallSystem": "System", + "postInstallVirtualization": "Virtualisierung", + "postInstallNetwork": "Netzwerk", + "postInstallStorage": "Speicher", + "postInstallSecurity": "Sicherheit", + "postInstallCustomization": "Anpassung", + "postInstallMonitoring": "Überwachung", + "postInstallPerformance": "Leistung", + "postInstallOptional": "Optional", + "postInstallUpdates": "Verfügbare Aktualisierungen installieren", + "postInstallUninstall": "Optimierungen entfernen", + "gpusCoralTpu": "GPUs und Coral TPU", + "nvidiaHost": "NVIDIA-Treiber auf dem Host installieren", + "coralHost": "Coral TPU auf dem Host installieren", + "addGpuLxc": "GPU für einen LXC freigeben", + "addCoralLxc": "Coral TPU für einen LXC freigeben", + "addGpuVm": "GPU einer VM zuweisen (Passthrough)", + "switchGpuMode": "GPU-Modus wechseln (VM ↔ LXC)", + "createVm": "VM erstellen", + "createVmOverview": "Übersicht", + "createVmSystemNas": "NAS-Systeme", + "createVmSynology": "Synology-VM", + "createVmNasOthers": "Weitere NAS-Systeme", + "createVmSystemWindows": "Windows-Systeme", + "createVmSystemLinux": "Linux-Systeme", + "diskManager": "Datenträgerverwaltung", + "diskManagerOverview": "Übersicht", + "diskImportVm": "Datenträger in eine VM importieren", + "diskImportImageVm": "Datenträgerabbild in eine VM importieren", + "diskAddController": "Controller oder NVMe-Gerät zu einer VM hinzufügen", + "diskImportLxc": "Datenträger in einen LXC importieren", + "diskFormat": "Physischen Datenträger formatieren oder löschen", + "diskSmart": "SMART-Zustand und Datenträgertests", + "storageShareManager": "Speicher und Freigaben verwalten", + "storageShareOverview": "Übersicht", + "hostStorage": "Speicher in den Host einbinden", + "hostNfs": "NFS-Freigabe als Proxmox-Speicher hinzufügen", + "hostSamba": "Samba-Freigabe als Proxmox-Speicher hinzufügen", + "hostIscsi": "iSCSI-Ziel als Proxmox-Speicher hinzufügen", + "hostLocalDisk": "Lokalen Datenträger als Proxmox-Speicher hinzufügen", + "hostLocalShared": "Freigegebenes Verzeichnis auf dem Host anlegen", + "lxcMountPoints": "LXC-Mountpoints (Host ↔ CT)", + "lxcNetworkSharing": "Netzwerkfreigaben in LXC", + "lxcNfsClient": "NFS-Client in LXC", + "lxcSambaClient": "Samba-Client in LXC", + "lxcNfsServer": "NFS-Server in LXC", + "lxcSambaServer": "Samba-Server in LXC", + "network": "Netzwerk", + "networkOverview": "Übersicht", + "networkDiagnostics": "Diagnose", + "networkMonitoring": "Werkzeuge zur Echtzeitüberwachung", + "networkBridge": "Netzwerk-Bridges prüfen und reparieren", + "networkConfig": "Konfiguration prüfen und bereinigen", + "networkPersistent": "Dauerhafte Schnittstellennamen", + "networkBackup": "Schnittstellen sichern und neu starten", + "security": "Sicherheit", + "securityOverview": "Übersicht", + "securityFail2ban": "Fail2Ban", + "securityLynis": "Lynis", + "utilities": "Dienstprogramme", + "utilsOverview": "Übersicht", + "utilsUupDump": "ISO-Erstellung mit UUP Dump", + "utilsSystemUtils": "Systemwerkzeuge installieren", + "utilsSystemUpdate": "Proxmox-System aktualisieren", + "utilsUpgradePve": "Von PVE 8 auf PVE 9 aktualisieren", + "utilsExportVm": "VM als OVA/OVF exportieren", + "utilsImportVm": "VM aus OVA/OVF importieren", + "settingsProxmenux": "ProxMenux-Einstellungen", + "settingsOverview": "Übersicht", + "settingsMonitor": "ProxMenux Monitor", + "settingsBeta": "Release-Kanal wechseln", + "settingsVersion": "Versionsinformationen anzeigen", + "settingsUninstall": "ProxMenux deinstallieren", + "commandsReference": "Befehlsreferenz", + "commandsOverview": "Übersicht", + "commandsSystem": "Nützliche Systembefehle", + "commandsVmCt": "VMs und CTs verwalten", + "commandsStorage": "Speicher und Datenträger", + "commandsNetwork": "Netzwerkbefehle", + "commandsUpdates": "Aktualisierungen und Pakete", + "commandsGpu": "GPU-Passthrough", + "commandsZfs": "ZFS verwalten", + "commandsBackup": "Sichern und Wiederherstellen", + "commandsTools": "Systemwerkzeuge für die Befehlszeile", + "glossary": "Glossar", + "about": "Über das Projekt", + "aboutOverview": "Übersicht", + "aboutFaq": "FAQ", + "aboutContributors": "Mitwirkende", + "aboutContributing": "Mitwirken", + "aboutCodeOfConduct": "Verhaltenskodex", + "externalRepositories": "Externe Repositorys", + "backupRestore": "Sichern und Wiederherstellen", + "backupRestoreOverview": "Übersicht", + "backupRestoreHowItWorks": "Funktionsweise", + "backupRestoreDestinations": "Ziele", + "backupRestoreDestOverview": "Übersicht", + "backupRestoreDestLocal": "Lokales Archiv", + "backupRestoreDestPbs": "Proxmox Backup Server", + "backupRestoreDestBorg": "Borg", + "backupRestoreCreating": "Sicherungen erstellen", + "backupRestoreJobs": "Geplante Aufträge", + "backupRestoreRestoring": "Wiederherstellen", + "backupRestoreCrossKernel": "Wiederherstellung mit anderer Kernel-Version", + "backupRestoreGlossary": "Glossar" + } + }, + "hero": { + "title": "ProxMenux", + "tagline": "Ein interaktives Menü und Web-Dashboard für Proxmox VE", + "description": "ProxMenux verbindet ein interaktives Menü mit einem Web-Dashboard für Proxmox VE. Führen Sie Befehle, Skripte und geführte Assistenten über das Terminalmenü aus oder verfolgen Sie Host-Zustand, Messwerte und Benachrichtigungen im Browser.", + "installButton": "Jetzt installieren", + "whatIsButton": "Was ist ProxMenux?", + "logoAlt": "ProxMenux-Logo" + }, + "docNav": { + "previous": "Zurück", + "next": "Weiter" + }, + "tocPanel": { + "onThisPage": "Auf dieser Seite" + }, + "supportProject": { + "heading": "Unterstützen Sie das Projekt!", + "body": "Wenn Sie ProxMenux hilfreich finden, geben Sie dem Projekt eine ⭐ auf GitHub, damit auch andere darauf aufmerksam werden.", + "button": "Stern auf GitHub vergeben" + }, + "rssLink": { + "heading": "Bleiben Sie auf dem Laufenden!", + "body": "Abonnieren Sie unseren RSS-Feed, um über neue Änderungen informiert zu werden.", + "copy": "Kopieren", + "copied": "Kopiert!", + "copyTitle": "RSS-URL kopieren", + "openFeed": "RSS-Feed öffnen", + "openTitle": "RSS-Feed öffnen" + }, + "changelog": { + "pageTitle": "Änderungsprotokoll", + "meta": { + "title": "ProxMenux-Änderungsprotokoll", + "titleSuffix": "Versionshinweise", + "latest": "Aktuell", + "mostRecent": "Neueste Version", + "description": "Versionshinweise und Aktualisierungsverlauf für ProxMenux, das menügesteuerte Skript zur Verwaltung von Proxmox VE.", + "descriptionTail": "Neue Funktionen, Fehlerbehebungen, Sicherheitsupdates, Verbesserungen an ProxMenux Monitor, Kompatibilität mit KI-Anbietern, zusätzliche Absicherung beim GPU-Passthrough und mehr.", + "ogDescription": "Versionshinweise für ProxMenux.", + "ogTail": "Abonnieren Sie den RSS-Feed oder besuchen Sie diese Seite erneut, um neue Versionen zu finden." + } + } +} diff --git a/web/messages/de/docs/about/code-of-conduct.json b/web/messages/de/docs/about/code-of-conduct.json new file mode 100644 index 00000000..4e79d0fe --- /dev/null +++ b/web/messages/de/docs/about/code-of-conduct.json @@ -0,0 +1,13 @@ +{ + "meta": { + "title": "Verhaltenskodex | ProxMenux-Dokumentation", + "description": "ProxMenux Verhaltenskodex und Best Practices für Mitwirkende, Tester und Nutzer.", + "ogTitle": "Verhaltenskodex | ProxMenux-Dokumentation", + "ogDescription": "Erwartungen an die Verantwortung für eingereichten Code, die Meldung von Sicherheitslücken und den Umfang von Pull Requests im ProxMenux-Projekt." + }, + "errors": { + "notFound": "Fehler: Die Datei CODE_OF_CONDUCT.md wurde nicht gefunden.", + "loadFailed": "Fehler: Der Inhalt des Verhaltenskodex kann nicht geladen werden." + }, + "contentMarkdown": "# 📝 Verhaltenskodex und bewährte Vorgehensweisen\n\nDieses Dokument legt die **Grundsätze und Richtlinien** für die Nutzung von **ProxMenux** fest. Es soll **Respekt, Zusammenarbeit und Sicherheit** bei der Entwicklung fördern.\n\n## 📌 1. Urheberangaben und Anerkennung\n\n- Werden **Skripte, Codeausschnitte oder Ideen** anderer Repositorys oder Entwickler verwendet, wird der ursprüngliche Urheber immer genannt.\n- Ein Kommentar im Code oder in der Dokumentation nennt die Quelle und verlinkt möglichst auf das ursprüngliche Repository.\n- Bei Code mit einer bestimmten **Lizenz** müssen deren Bedingungen eingehalten werden.\n\n### ✅ Beispiel für eine korrekte Quellenangabe im Code:\n\n```bash\n# Skript auf Grundlage der Arbeit von [Autor] unter [Repository-Link]\n```\n\n## 🔒 2. Sicherheit und Verantwortung für den Code\n\n- Der gesamte Code in diesem Repository folgt bewährten Vorgehensweisen für sichere Entwicklung.\n- Beiträge mit absichtlich eingebauten Schwachstellen oder unsicheren Vorgehensweisen sind nicht erlaubt.\n- Skripte dürfen niemals die Systemsicherheit oder Benutzerdaten gefährden.\n- Beiträge sollen vor der Implementierung auf Sicherheit und Stabilität geprüft werden.\n\n### ⚠️ Verantwortungsvolle Meldung von Sicherheitslücken\n\nEntdecken Sie eine Sicherheitslücke, melden Sie diese NICHT in öffentlichen Issues oder Foren.\n\nMelden Sie sie stattdessen vertraulich per E-Mail:\n\n📧 proxmenux@macrimi.pro\n\nWeitere Informationen zu Sicherheitslücken und deren Meldung finden Sie in unserer [Sicherheitsrichtlinie](./SECURITY.md).\n\n## 🤝 3. Regeln für die Community\n\nFür ein respektvolles und professionelles Umfeld müssen alle Benutzer diese Regeln beachten:\n\n- ✅ Bleiben Sie in Diskussionen respektvoll und konstruktiv.\n- ❌ Belästigung, Hassrede und beleidigendes Verhalten werden nicht toleriert.\n- ✅ Geben Sie Rückmeldungen sachlich und ohne persönliche Angriffe.\n- ❌ Verstöße können je nach Schwere zu Verwarnungen oder Ausschlüssen führen.\n\n##\n\nDieses Dokument fördert den ethischen Umgang mit Code, Sicherheitsbewusstsein und eine respektvolle Community.\n\n📢 Lassen Sie uns gemeinsam etwas Großartiges schaffen!\n" +} diff --git a/web/messages/de/docs/about/contributing.json b/web/messages/de/docs/about/contributing.json new file mode 100644 index 00000000..7827e51f --- /dev/null +++ b/web/messages/de/docs/about/contributing.json @@ -0,0 +1,293 @@ +{ + "meta": { + "title": "Zu ProxMenux beitragen | ProxMenux-Dokumentation", + "description": "Zu ProxMenux beitragen: Branch-Modell (main / develop / feature/*), Pull-Request-Ablauf, Skript-Header mit Autorenangabe und optionalem Sponsoring-Link, zweiphasige UI-Richtlinie, Nachrichtenfunktionen und der Weg vom Feature-Branch zur stabilen Version.", + "ogTitle": "Zu ProxMenux beitragen", + "ogDescription": "Branch-Modell, Arbeitsablauf, Autorenangabe im Skript-Header und zweiphasige UI-Richtlinie für Mitwirkende." + }, + "header": { + "title": "Zu ProxMenux beitragen", + "description": "ProxMenux ist offen für Community-Beiträge - neue Skripte, Fixes für bestehende, Dialogverbesserungen, Übersetzungen, Integrationen. Diese Seite ist die formale Tür: wie das Repository strukturiert ist, das Verzweigungsmodell und der Pull-Request-Workflow, der Skript-Header mit Autorzuweisung und optionalem Sponsor-Link und die Design-Konventionen, denen jeder Beitrag folgen muss.", + "section": "Über" + }, + "twoPagesCallout": { + "title": "Zwei verwandte Seiten, ein Projekt", + "body": "Verwechseln Sie diese Seite nicht mit Mitwirkende, die bisherige Beiträge von Testern, Reviewern und Entwicklern würdigt. Diese Seite richtet sich an Sie als künftigen Mitwirkenden und erklärt, wie Sie einen Pull Request zur Aufnahme vorbereiten." + }, + "branching": { + "heading": "Verzweigungsmodell", + "intro": "ProxMenux verwendet ein dreistufiges Verzweigungsmodell:", + "headerBranch": "Branch", + "headerPurpose": "Zweck", + "rows": [ + { + "branch": "main", + "purposeRich": "Stabiler, produktionsreifer Code. Hier werden nur veröffentlichungsreife Änderungen aufgenommen. Diese Version erhalten Endnutzer bei einer normalen ProxMenux-Installation." + }, + { + "branch": "develop", + "purposeRich": "Aktiver Entwicklungsbranch. Neue Arbeiten werden zuerst hier aufgenommen; dies ist der Beta-Kanal. Ist eine stabile Version bereit, wird sie von hier nach main übernommen." + }, + { + "branch": "feature/*", + "purposeRich": "Kurzlebige Branchs pro Feature, Fix oder Verbesserung. Erstellt aus develop, nach Überprüfung über einen Pull Request wieder in develop zusammengeführt." + } + ], + "calloutTitle": "Was das für die Nutzer bedeutet", + "calloutBody": "Endbenutzer, die main verfolgen, erhalten getestete Releases. Benutzer, die die neuesten Funktionen frühzeitig wünschen - und gelegentliche raue Kanten nicht stören - können develop folgen. Neue Funktionen passieren immer zuerst develop; nichts erreicht main, ohne diesen Zyklus zu durchlaufen." + }, + "workflow": { + "heading": "Pull Request Workflow", + "intro": "Von der Idee zum fusionierten Release in fünf Schritten:", + "step1Lead": "Erstellen Sie einen Feature Branch aus develop:", + "step1Code": "git clone https://github.com/MacRimi/ProxMenux.git\ncd ProxMenux\ngit checkout develop\ngit pull origin develop\ngit checkout -b feature/your-feature-name", + "step1Trail": "Verwenden Sie einen beschreibenden Zweignamen: feature/zfs-arc-tuning, feature/fix-iommu-detection, feature/add-tailscale-script.", + "step2Lead": "Arbeiten Sie an Ihren Änderungen und schieben Sie den Branch: ", + "step2Code": "# Make your changes, then:\ngit add scripts/...\ngit commit -m \"Add ZFS ARC tuning script\"\ngit push -u origin feature/your-feature-name", + "step3": "Öffnen Sie einen Pull Request für develop, nicht für main. Prüfen Sie auf GitHub, dass der Basisbranch auf develop steht. Beschreiben Sie, was das Skript tut und ändert und auf welchen Proxmox-VE-Versionen es getestet wurde.", + "step4": "Nach der Prüfung werden Änderungen in develop zusammengeführt. Ein Maintainer prüft Codequalität, Header-Konventionen und die zweiphasige UI-Richtlinie. Falls Änderungen nötig sind, pushen Sie weitere Commits in denselben Branch; der PR aktualisiert sich automatisch.", + "step5": "Stabile Versionen werden von develop nach main übernommen. Bei der Veröffentlichung wird main per Fast-Forward auf develop gebracht und mit einem Tag versehen. Ihr Beitrag wird Teil der nächsten stabilen Version." + }, + "scriptHeader": { + "heading": "Skript-Header — Metadaten und Beschreibung", + "intro": "Jedes Skript in ProxMenux wird mit zwei benachbarten Kommentarblöcken geöffnet, die zusammen den Header bilden. Sie sind beide erforderlich - zusammen lassen sie jeden Leser wissen, wer das Skript geschrieben hat und was es tut, alles ohne den Code selbst zu öffnen.", + "bullets": [ + "Oberer Block: Metadaten. Autor, optionale GitHub-/Sponsoring-Links, Maintainer, Copyright, Lizenz, Version und Aktualisierungsdatum. Hier erfolgt auch die Würdigung der Mitwirkenden: Bei einem neuen Skript tragen Sie Ihren Namen und optional Links zur persönlichen GitHub-Seite und einem Unterstützungsprofil ein, etwa Ko-fi, GitHub Sponsors oder Buy Me a Coffee.", + "Unterer Block: Beschreibung. Ein kurzer Absatz in einfachem Englisch erklärt die Funktion. Nutzer lesen ihn vor dem Code. Er muss für sich allein Zweck, Hauptaktionen, betroffene Ressourcen und Voraussetzungen verständlich machen." + ], + "licenseCalloutTitle": "Die Lizenzlinie ist festgelegt — GPL-3.0", + "licenseCalloutBody": "ProxMenux wird unter der GNU General Public License v3.0 veröffentlicht. Jedes Skript im Projekt wird unter derselben Lizenz ausgeliefert; die License-Zeile im Header ist immer die GPL-3.0-Referenz, die im folgenden Beispiel gezeigt wird - es ist keine Per-Script-Wahl. Indem Sie ein Skript beisteuern, erklären Sie sich damit einverstanden, es unter GPL-3.0 zu veröffentlichen, was bedeutet, dass jeder es lesen, ändern und weiterverteilen kann (einschließlich Änderungen), solange er es unter der gleichen Lizenz behält. Der vollständige Text befindet sich unter MacRimi/ProxMenux/LICENSE.", + "templateCode": "#!/bin/bash\n\n# ==========================================================\n# ProxMenux - A menu-driven script for Proxmox VE management\n# ==========================================================\n# Author : Your Name\n# GitHub : github.com/yourhandle\n# Sponsor : ko-fi.com/yourhandle\n# Maintainer : MacRimi\n# Copyright : (c) 2026 MacRimi & contributors\n# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)\n# Version : 1.0\n# Last Updated: DD/MM/YYYY\n# ==========================================================\n# Description:\n# Short paragraph explaining what the script does.\n# Mention the main actions (e.g. \"creates a ZFS pool\",\n# \"configures IOMMU and reboots\", \"imports an ISO into a VM\"),\n# the resources it touches, and any prerequisites the user\n# should be aware of before running it.\n# ==========================================================", + "optionalNote": "Die Linien GitHub und Sponsor sind optional - lassen Sie sie aus, wenn Sie sie nicht veröffentlichen möchten. Alles andere ist erforderlich.", + "whyCalloutTitle": "Warum das wichtig ist", + "whyCalloutBody": "Open-Source-Beiträge sind freiwillige Arbeit. Name und optionaler Sponsoring-Link im Quellcode machen die Urheberschaft bei jedem Lesen sichtbar, statt sie hinter dem Projektnamen zu verbergen. Der Beschreibungsblock ist aus einem anderen Grund wichtig: Er ist der erste Anlaufpunkt künftiger Maintainer und neugieriger Nutzer. Eine klare Erklärung entscheidet oft darüber, ob sie einem Skript vertrauen oder es meiden." + }, + "structure": { + "heading": "Projektstruktur", + "intro": "Wo jede Art von Skript im Repository lebt:", + "treeCode": "scripts/\n├── menus/ # Top-level menu scripts (entry points)\n├── storage/ # Disk, storage and passthrough scripts\n├── share/ # NFS, Samba, local share scripts\n├── vm/ # VM creation and configuration scripts\n├── gpu_tpu/ # GPU / TPU passthrough scripts\n├── post_install/ # Post-install automation scripts\n├── backup_restore/ # Backup and restore scripts\n├── utilities/ # System utility scripts\n├── global/ # Shared helper libraries (sourced by other scripts)\n├── utils.sh # Shared utility functions and message helpers\n└── help_info_menu.sh # Interactive help and command reference", + "outro": "Jedes Skript lädt utils.sh beim Start per source, um Nachrichtenfunktionen, Spinner, Farbvariablen und Übersetzungssystem einzubinden. Gemeinsame Hilfsbibliotheken in scripts/global/ werden ausdrücklich von den Skripten geladen, die sie benötigen." + }, + "twoPhase": { + "heading": "Die zweiphasige UI Design Policy", + "intro": "Dies ist die wichtigste Konvention in ProxMenux und die PRs werden am häufigsten gebeten, sie zu beheben. Jedes Skript ist in genau zwei Phasen unterteilt:", + "headerPhase": "Phase", + "headerPurpose": "Zweck", + "headerScreen": "Bildschirmzustand", + "rows": [ + { + "phaseRich": "Phase 1 — Auswahl", + "purposeRich": "Sammeln Sie jede Benutzerentscheidung. Führen Sie alle erforderlichen Vorbereitungsarbeiten durch (Sonden, Scans, Überprüfungen).", + "screenRich": "dialog-Overlays. Wenn eine Suche zwischen zwei Menüs länger als ein oder zwei Sekunden dauert, zeigen Sie einen msg_info-Spinner an, damit der Benutzer weiß, dass das Skript nicht eingefroren ist, und rufen Sie dann stop_spinner direkt vor dem nächsten Dialog auf." + }, + { + "phaseRich": "Phase 2 — Ausführung", + "purposeRich": "Führen Sie jede Operation aus. Zeigen Sie den gesamten Fortschrittsverlauf an, der sich auf dem Bildschirm ansammelt.", + "screenRich": "Sichtbare msg_info / msg_ok Nachrichten; niemals dialog." + } + ], + "principle": "Das Prinzip: Sammeln Sie zuerst alles und führen Sie dann alles aus. Der Benutzer sieht ein sauberes dialoggesteuertes Menü (Phase 1), dann eine saubere Ausführungsansicht im Protokollstil (Phase 2). Kein Mischen — kein Dialog erscheint mitten in der Ausführung, kein Fortschrittsrauschen während der Auswahl.", + "phase1Heading": "Phase 1 — typisches Muster", + "phase1Code": "# Silent preparatory work between dialogs\nmsg_info \"$(translate \"Checking disk assignments...\")\"\nASSIGNED_TO=$(check_assignments \"$DISK\")\nstop_spinner # ← clears line silently, result saved in variable\n\n# Next dialog can now use ASSIGNED_TO\nif [ -n \"$ASSIGNED_TO\" ]; then\n dialog --yesno \"$(translate \"Disk already assigned. Continue?\")\" $UI_YESNO_H $UI_YESNO_W\nfi\n\n# Collect multiple decisions per item with parallel arrays\ndeclare -a DISK_LIST=()\ndeclare -a DISK_FORMAT_TYPES=()\ndeclare -a DISK_MOUNT_POINTS=()\nfor DISK in $SELECTED; do\n msg_info \"$(translate \"Analyzing disk...\")\"\n CURRENT_FS=$(lsblk -no FSTYPE \"$DISK\" | xargs)\n stop_spinner\n\n FORMAT=$(dialog --backtitle \"$BACKTITLE\" \\\n --title \"$(translate \"Select Filesystem\")\" \\\n --menu \"...\" $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \\\n \"ext4\" \"...\" \"xfs\" \"...\" \"btrfs\" \"...\" \\\n 2>&1 >/dev/tty)\n [ -z \"$FORMAT\" ] && continue\n\n DISK_LIST+=(\"$DISK\")\n DISK_FORMAT_TYPES+=(\"$FORMAT\")\ndone", + "phase1RulesIntro": "Regeln für Phase 1:", + "phase1Rules": [ + "Läuft gerade ein msg_info-Spinner und soll ein Menü mit dialog oder whiptail geöffnet werden, rufen Sie zuerst stop_spinner auf. Der Spinner verträgt sich nicht mit deren Bildschirmüberlagerung. Ohne aktiven Spinner ist der Aufruf nicht nötig.", + "Verwenden Sie show_proxmenux_logo + msg_title + msg_info, um in Phase 1 einen längeren Vorgang sichtbar einzuordnen, etwa eine Prüfung ab fünf Sekunden. Die Funktion leert bereits den Bildschirm; rufen Sie davor nicht clear auf.", + "Rufen Sie show_proxmenux_logo nicht zwischen Dialogmenüs auf, in denen nichts angezeigt werden kann - das Löschen des Bildschirms für ein leeres Terminal ist nur visuelles Rauschen.", + "Speichern Sie alle Entscheidungen und Sondenergebnisse in Variablen oder parallelen Arrays. Die sichtbare Zusammenfassung erfolgt zu Beginn der Phase 2, nicht in Phase 1." + ], + "phase2Heading": "Phase 2 — typisches Muster", + "phase2Code": "# ── PHASE 2 — EXECUTION ─────────────────────────────\nshow_proxmenux_logo\nmsg_title \"$(translate \"My Script Title\")\"\n\n# Recap Phase 1 preparatory results — show what was already done\nmsg_ok \"$(translate \"CT $CTID selected.\")\"\nmsg_ok \"$(translate \"Repositories verified.\")\"\nmsg_ok \"$(translate \"Disks to process: ${#DISK_LIST[@]}\")\"\n\n# Now execute operations\nfor i in \"${!DISK_LIST[@]}\"; do\n DISK=\"${DISK_LIST[$i]}\"\n FORMAT=\"${DISK_FORMAT_TYPES[$i]}\"\n\n msg_info \"$(translate \"Formatting\") $DISK $(translate \"as\") $FORMAT...\"\n mkfs.\"$FORMAT\" \"$DISK\" >/dev/null 2>&1\n msg_ok \"$(translate \"Formatted.\")\"\ndone\n\nmsg_ok \"$(translate \"Completed. ${#DISK_LIST[@]} disk(s) processed.\")\"\nmsg_success \"$(translate \"Press Enter to return to menu...\")\"\nread -r", + "phase2Rules": "Regeln für Phase 2: Beginnen Sie mit show_proxmenux_logo + msg_title und fassen Sie Phase 1 sofort in msg_ok-Zeilen zusammen. Rufen Sie show_proxmenux_logo nicht erneut auf, da dies den bisherigen Verlauf löscht. Verwenden Sie kein dialog; ist eine Entscheidung zur Laufzeit unvermeidbar, nutzen Sie whiptail wie im nächsten Abschnitt." + }, + "dialogVsWhiptail": { + "headingRich": "Wann dialog vs whiptail zu verwenden", + "intro": "Beide Tools zeichnen Benutzeroberflächen im Textmodus, aber sie verhalten sich auf dem Bildschirm sehr unterschiedlich - und ProxMenux verwendet sie aus einem bestimmten Grund in zwei verschiedenen Phasen.", + "headerTool": "Werkzeug", + "headerWhen": "Wann man es benutzt", + "headerEffect": "Wirkung auf den Bildschirm", + "rows": [ + { + "toolRich": "dialog", + "whenRich": "Immer in Phase 1. Jede interaktive Auswahl – Auswahl einer VM, einer Festplatte, eines Dateisystems, das eine Aktion bestätigt – verwendet dialog. Dies ist das Standard-UI-Tool von ProxMenux.", + "effectRich": "Übernimmt das Terminal: löscht den Bildschirm, zieht sein Overlay und bringt das Terminal beim Schließen in seinen vorherigen Zustand zurück. Fein in Phase 1, weil sich noch nichts Nützliches zeigt." + }, + { + "toolRich": "whiptail", + "whenRich": "Nur in Phase 2, nur wenn unvermeidlich. Der typische Fall ist eine Neustartaufforderung am Ende der Ausführung. Greifen Sie nicht zu whiptail, um \"Weiter?\" mitten in der Ausführung zu fragen - diese Entscheidung hätte in Phase 1 getroffen werden sollen.", + "effectRich": "Leichteres Overlay, das den Terminalkontext nicht löscht. Der akkumulierte msg_ok / msg_info-Verlauf der Phase 2 bleibt hinter dem Dialogfeld sichtbar. Deshalb ist es die richtige Wahl, wenn der Fortschritt bereits auf dem Bildschirm zu sehen ist." + } + ], + "calloutTitle": "Warum die Trennung wichtig ist", + "calloutBody": "Wenn Sie dialog in der Mitte der Phase 2 anrufen, verschwindet jede msg_ok-Zeile, die der Benutzer beobachtet hat. Sie wischen den Audit-Trail. whiptail vermeidet das. Die Regel ist also nicht willkürlich - es geht darum, die Sicht des Benutzers auf das, was das Skript bisher getan hat, zu bewahren.", + "rebootHeading": "Neustartabfrage: das typische whiptail in Phase 2", + "rebootIntro": "Wenn ein Skript endet und ein Neustart erforderlich ist (z.B.) IOMMU aktiviert, Kernelparameter geändert), die Aufforderung am Ende der Phase 2 verwendet whiptail. Fügen Sie immer einen \"Nein\"-Zweig hinzu, der den Benutzer warnt, die betroffene Ressource nicht zu verwenden, bis er neu startet:", + "rebootCode": "if [[ \"$HOST_REBOOT_REQUIRED\" == \"yes\" ]]; then\n echo \"\"\n if whiptail --title \"$(translate \"Reboot Required\")\" --yesno \\\n \"\\n$(translate \"A host reboot is required before starting the VM. Reboot now?\")\" \\\n 13 78; then\n msg_warn \"$(translate \"Rebooting the system...\")\"\n reboot\n else\n echo \"\"\n msg_info2 \"$(translate \"Do not start the VM until the system has been rebooted.\")\"\n fi\nfi\n\nmsg_success \"$(translate \"Press Enter to return to menu...\")\"\nread -r" + }, + "messageFunctions": { + "heading": "Referenz der Nachrichtenfunktionen", + "intro": "Alle definiert in utils.sh. Verwenden Sie sie als Standard für jede benutzersichtliche Ausgabe - konsistente Visuals über Skripte hinweg ist der springende Punkt. Wenn Ihr Skript eine neue Funktion benötigt, die nicht zum vorhandenen Set passt (ein neuer Schweregrad, ein neuer Layout-Helfer usw.), schlagen Sie es in Ihrem Pull Request vor - es wird überprüft und zu utils.sh hinzugefügt, wenn es allgemein nützlich ist.", + "headerFunction": "Funktion", + "headerWhen": "Wann zu verwenden", + "headerSpinner": "Fortschrittsanzeige", + "rows": [ + { + "function": "msg_info \"text\"", + "whenRich": "Operation in Arbeit.", + "spinner": "Startet" + }, + { + "function": "stop_spinner", + "whenRich": "Ende der stillen Vorbereitungsarbeiten in Phase 1 — tötet Spinner, löscht die Linie.", + "spinner": "Stoppt" + }, + { + "function": "msg_ok \"text\"", + "whenRich": "Vorgang erfolgreich. Auch für „Funktion aktiviert“ verwenden, wenn noch ein Neustart erforderlich ist.", + "spinner": "Stoppt" + }, + { + "function": "msg_warn \"text\"", + "whenRich": "Tatsächliche Warnung oder gestörter Zustand.", + "spinner": "Stoppt" + }, + { + "function": "msg_error \"text\"", + "whenRich": "Schwerwiegender Fehler.", + "spinner": "Stoppt" + }, + { + "function": "msg_info2 \"text\"", + "whenRich": "Nicht blockierender Hinweis (türkisfarbene Infozeile).", + "spinner": "Stoppt" + }, + { + "function": "msg_success \"text\"", + "whenRich": "Abschließende Aufforderung „Enter zum Zurückkehren“ am Ende von Phase 2.", + "spinner": "Stoppt" + }, + { + "function": "msg_title \"text\"", + "whenRich": "Fetter Titel mit eingebautem Abstand. Wird zu Beginn der Phase 2 verwendet.", + "spinner": "—" + }, + { + "function": "show_proxmenux_logo", + "whenRich": "Leert den Bildschirm und zeigt das Logo. Zu Beginn von Phase 2 einmal aufrufen.", + "spinner": "—" + } + ] + }, + "dialogConventions": { + "heading": "Dialogkonventionen", + "bullets": [ + "Geben Sie --backtitle \"$BACKTITLE\" immer an jeden dialog- und whiptail-Aufruf weiter. $BACKTITLE ist immer \"ProxMenux\" - einmal im Skript-Header gesetzt und nie außer Kraft gesetzt. Der Benutzer muss den Projektnamen immer als Framing-Kontext sehen, niemals den eigenen Titel des Skripts.", + "Verpacken Sie Titel und Nachrichten immer mit $(translate \"...\").", + "Immer dialog Ausgabe mit 2>&1 >/dev/tty umleiten — die erfasste stdout wird die Auswahl des Benutzers, während der Dialog selbst auf das Terminal zieht.", + "Verwenden Sie die Standard-UI-Dimensionsvariablen ($UI_MENU_H, $UI_MSG_W, usw.) für eine konsistente Dimensionierung über Skripte hinweg.", + "Prüfen Sie immer auf leere oder abgebrochene Auswahlen und behandeln Sie diese sauber." + ], + "exampleIntro": "Vollständiges Beispiel — Erstellung eines VM-Auswahlmenüs und Bearbeitung der Stornierung:", + "exampleCode": "# 1) Build the list of VMs as alternating \"ID NAME\" pairs.\n# dialog --menu expects this exact shape: tag1 description1 tag2 description2 ...\nVM_LIST=\"\"\nfor vmid in $(qm list | awk 'NR>1 {print $1}'); do\n vm_name=$(qm config \"$vmid\" | awk -F': ' '/^name:/ {print $2}')\n VM_LIST=\"$VM_LIST $vmid \\\"$vm_name\\\"\"\ndone\n\n# 2) Show the dialog. Captured stdout is the user's selection (the VMID).\nVMID=$(eval \"dialog --backtitle \\\"\\$BACKTITLE\\\" \\\n --title \\\"\\$(translate \\\"Select VM\\\")\\\" \\\n --menu \\\"\\$(translate \\\"Choose a VM from the list:\\\")\\\" \\\n \\$UI_MENU_H \\$UI_MENU_W \\$UI_MENU_LIST_H \\\n $VM_LIST \\\n 2>&1 >/dev/tty\")\n\n# 3) Empty result means the user pressed Cancel or Esc — exit silently.\nif [ -z \"$VMID\" ]; then\n exit 0\nfi\n\n# 4) Continue with the rest of Phase 1, knowing VMID is now set.\necho \"User selected VMID=$VMID\"" + }, + "translation": { + "heading": "Übersetzungspolitik", + "intro": "Alle vom Benutzer sichtbaren Zeichenfolgen müssen mit der translate-Funktion gewickelt sein. ProxMenux übersetzt sie automatisch in alle unterstützten Sprachen - Sie schreiben Englisch, der Benutzer liest seine Muttersprache.", + "code": "msg_ok \"$(translate \"Operation completed successfully.\")\"\nmsg_error \"$(translate \"Failed to start container\") $CTID.\"\ndialog --title \"$(translate \"Select Storage\")\" ...", + "bullets": [ + "Schreibe Strings in Englisch – die Übersetzung wird automatisch vom Build bearbeitet.", + "Halten Sie Strings kurz. Vermeiden Sie das Einbetten von Variablen in lange Sätze, wenn möglich.", + "Variablennamen, Pfade oder technische Bezeichner nichtübersetzen." + ] + }, + "variableStyle": { + "heading": "Variable und Stilkonventionen", + "bullets": [ + "Verwenden Sie UPPER_CASE für Variablen auf Skriptebene.", + "Verwenden Sie lower_case für lokale Funktionsvariablen (mit local deklarieren).", + "Setzen Sie alle Variablenexpansionen in Anführungszeichen: \"$VAR\" statt $VAR.", + "Verwenden Sie [[ ]] für Konditionale, nicht [ ] (außer wenn POSIX erforderlich ist).", + "show_proxmenux_logo ist der geeignete Weg, um den Bildschirm zu löschen - er enthält das Clear und zeigt das Projektlogo, so dass der Benutzer immer einen visuellen Kontext hat. Rufen Sie es einmal zu Beginn der Phase 2 (und optional vor einem langen Phase 1-Spinnerblock) auf." + ], + "standardNamesIntro": "Standardvariablennamen im gesamten Projekt:", + "standardNamesCode": "CTID # container ID\nVMID # virtual machine ID\nDISK # device path (e.g. /dev/sdb)\nPARTITION # partition path (e.g. /dev/sdb1)\nSTORAGE # Proxmox storage name\nMOUNT_POINT # filesystem mount path", + "redirectHeading": "Umleitung der Werkzeugausgabe während der Phase 2", + "redirectIntro": "Phase 2 zeigt ein sauberes Protokoll der msg_info → msg_ok-Zeilen an, die sich auf dem Bildschirm ansammeln. Wenn ein Tool, das Sie aufrufen (apt, mkfs, qm, pct, dd, etc.), seine eigene Ausgabe in stdout/stderr schreibt, scrollt es an Ihren Nachrichten vorbei und unterbricht den visuellen Fluss - Sie haben ein chaotisches Terminal, in dem der Benutzer die eigenen Fortschrittslinien des Skripts nicht vom Rauschen des zugrunde liegenden Tools unterscheiden kann.", + "withoutRedirectIntro": "Ohne Umleitung: die Ausgabe für Nutzer, wenn zusätzliche Werkzeugausgaben nicht abgefangen werden.", + "withoutRedirectCode": "ℹ Installing kernel package...\nReading package lists... Done\nBuilding dependency tree... Done\nReading state information... Done\nThe following NEW packages will be installed:\n proxmox-kernel-6.5\n0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.\nNeed to get 12.5 MB of archives.\nAfter this operation, 47.2 MB of additional disk space will be used.\nGet:1 http://download.proxmox.com/debian/pve trixie/pve-no-subscription amd64 ...\n... [200+ more lines of dpkg output] ...\n✓ Kernel installed.", + "withRedirectIntro": "Mit Umleitung: dieselbe Aktion, wenn zusätzliche Ausgaben an anderer Stelle landen.", + "withRedirectCode": "ℹ Installing kernel package...\n✓ Kernel installed.\nℹ Configuring kernel command line...\n✓ Configured.\nℹ Refreshing boot loader...\n✓ Boot loader updated.", + "twoPatternsIntro": "Zwei Muster zur Auswahl:", + "discardLead": "Ausgabe verwerfen, wenn sie nicht benötigt wird: die einfachste und schnellste Variante.", + "discardCode": "DEBIAN_FRONTEND=noninteractive apt-get install -y \"$package\" >/dev/null 2>&1", + "logLead": "Ausgabe in eine Protokolldatei schreiben, wenn Sie sie später prüfen möchten, etwa zur Fehlersuche bei einer Installation oder um dpkg-Aktionen nachzuvollziehen. Dies ist das bevorzugte Muster für apt-Aufrufe:", + "logCode": "apt-get install -y \"$package\" >> \"$log_file\" 2>&1", + "referenceOutro": "scripts/global/update-pve9_2.sh dient als Referenz: Jeder apt-get-Aufruf schreibt in eine Protokolldatei. Nutzer sehen nur den Ablauf msg_info → msg_ok, während die Datei bei Fehlern genau zeigt, was apt getan hat." + }, + "dosAndDonts": { + "heading": "Do's und Don'ts", + "doHeading": "✅ Empfohlen", + "doBullets": [ + "stop_spinner vor einem dialog in Phase 1 nur dann, wenn ein msg_info Spinner gerade läuft.", + "Phase 2 beginnt mit show_proxmenux_logo + msg_title + msg_ok als Zusammenfassung der Ergebnisse von Phase 1.", + "Verwenden Sie msg_ok für erfolgreich aktivierte Funktionen – auch wenn ein Neustart erforderlich ist.", + "Verwenden Sie whiptail (nicht dialog) für jede Aufforderung nach der Ausführung, die in Phase 2 erscheinen muss.", + "Fügen Sie immer einen \"Nein\" -Zweig in Reboot-Dialoge ein, der den Benutzer warnt, die betroffene Ressource bis zum Neustart nicht zu starten.", + "Schützen Sie die reine VM-Logik, indem Sie [[ -f \"/etc/pve/qemu-server/$'{'vmid'}'.conf\" ]] überprüfen - Controller und NVMe PCIe können nicht zu LXC-Containern hinzugefügt werden.", + "Verwenden Sie ensure_repositories von utils-install-functions.sh anstelle von bedingungslosem apt-get update.", + "Verwenden Sie parallele Arrays in Phase 1, wenn jedes Element mehrere Dialoge benötigt." + ], + "dontHeading": "❌ Vermeiden", + "dontBullets": [ + "Rufen Sie dialog an, während ein Spinner aktiv ist.", + "Überspringen Sie die Phase 1 Zusammenfassung zu Beginn der Phase 2.", + "Rufen Sie show_proxmenux_logo ein zweites Mal an - es löscht alles, was Phase 2 gedruckt hat.", + "Verwenden Sie dialog in Phase 2 (verwenden Sie whiptail für den seltenen unvermeidlichen Fall).", + "Verwenden Sie reine clear.", + "msg_title mit Leerzeilen aus echo \"\" umgeben; die Abstände sind bereits enthalten.", + "Verwenden Sie msg_warn, um eine erfolgreich aktivierte Funktion zu melden - das ist ein msg_ok.", + "Führen Sie bedingungslos apt-get update aus - verwenden Sie ensure_repositories." + ] + }, + "submitting": { + "heading": "Einreichen Ihres Beitrags", + "steps": [ + "Fork MacRimi/ProxMenux auf GitHub und klonen Sie Ihre Fork.", + "Erstellen Sie einen feature/*-Zweig aus develop (siehe Branching-Modell oben).", + "Befolgen Sie diese Anleitung für neue oder geänderte Skripte. Schreiben Sie den Header mit Ihrem Namen; fügen Sie die optionale Sponsorenzeile hinzu, wenn Sie möchten.", + "Testen Sie Ihr Skript auf einer echten Proxmox VE-Instanz - sowohl Phase 1 (jeder Dialogzweig) als auch Phase 2 (jede Operation ist erfolgreich und wird bei Fehlern sauber zurückgesetzt).", + "Öffnen Sie eine Pull Request gegen develop mit einer klaren Beschreibung: was das Skript macht, was sich geändert hat, auf welcher Proxmox VE-Version es getestet wurde.", + "Stellen Sie sicher, dass Ihr Beitrag den Code of Conduct respektiert." + ], + "securityOutro": "Befolgen Sie bei sicherheitsrelevanten Problemen den Offenlegungsfluss in SECURITY.md, anstatt ein öffentliches Problem zu öffnen." + }, + "whereNext": { + "heading": "Wohin als nächstes", + "items": [ + { + "kind": "external", + "url": "https://github.com/MacRimi/ProxMenux/blob/main/CONTRIBUTING.md", + "label": "CONTRIBUTING.md (vollständige Anleitung)", + "tail": "— die Quelle der Wahrheit für jede Konvention, einschließlich der fortgeschrittenen Hardware-Muster." + }, + { + "kind": "internal", + "href": "/docs/about/contributors", + "label": "Beitragende", + "tail": " — die Menschen, deren Arbeit ProxMenux-Versionen geprägt hat. Eine andere Seite als diese hier." + }, + { + "kind": "internal", + "href": "/docs/about/code-of-conduct", + "label": "Verhaltenskodex", + "tail": "- die Standards, die jeder Mitwirkende zu befolgen bereit ist." + }, + { + "kind": "external", + "url": "https://github.com/MacRimi/ProxMenux/discussions", + "label": "GitHub Discussions", + "tail": "Fragen Sie, bevor Sie bauen, ob Sie nicht sicher sind, ob eine Idee passt, oder um Mitarbeiter für ein größeres Feature zu finden." + } + ] + } +} diff --git a/web/messages/de/docs/about/contributors.json b/web/messages/de/docs/about/contributors.json new file mode 100644 index 00000000..40718e21 --- /dev/null +++ b/web/messages/de/docs/about/contributors.json @@ -0,0 +1,38 @@ +{ + "meta": { + "title": "Mitwirkende | ProxMenux-Dokumentation", + "description": "Anerkennung für die Menschen, die ProxMenux möglich machen: Tester, Reviewer und Entwickler, die jede Version mitgestalten.", + "ogTitle": "Mitwirkende | ProxMenux-Dokumentation", + "ogDescription": "Treffen Sie die Mitwirkenden hinter ProxMenux." + }, + "header": { + "title": "Mitwirkende", + "description": "Das ProxMenux-Projekt wächst dank seiner Mitwirkenden. Diese Seite würdigt ihre Arbeit: Versionen vor der Veröffentlichung testen, Regressionen erkennen, PRs prüfen und die Entwicklung neuer Funktionen mitgestalten.", + "section": "Über" + }, + "beyond": { + "title": "Weitere Mitwirkende", + "body": "Viele weitere Menschen helfen mit Fehlermeldungen, Funktionsideen in Discussions, Beta-Tests und Übersetzungen. Die vollständige Liste der Code-Mitwirkenden steht im GitHub-Mitwirkendendiagramm. Diese Seite hebt diejenigen hervor, die besonders aktiv ProxMenux-Versionen testen und prüfen." + }, + "testers": { + "heading": "Tester & Reviewer", + "intro": "Diese Personen gehörten zu den ersten Mitwirkenden. Mit Tests, Fehlermeldungen und Verbesserungsvorschlägen halfen sie ProxMenux dabei, zu dem zu werden, was es heute ist.", + "roles": { + "testing": "Testen", + "testingReviewer": "Tests und Review" + }, + "youtube": "YouTube" + }, + "contribute": { + "heading": "Möchten Sie dazu beitragen?", + "intro": "Jeder Beitrag ist willkommen - Code, Testen, Design, Dokumentation oder einfach nur das Teilen Ihrer Ideen:", + "tester": "Tester — probieren Sie den Beta-Kanal aus und melden Sie vor der stabilen Veröffentlichung, was funktioniert und was nicht.", + "developer": "Entwickler — Bash, Next.js (TypeScript) und etwas Python für die virtuelle Übersetzungsumgebung (venv). PRs auf GitHub sind willkommen.", + "designer": "Designer — Verbesserungen der Benutzeroberfläche, Icons, Screenshots zur Dokumentation.", + "ideas": "Ideen — eröffnen Sie zuerst ein Thema in GitHub Discussions, bevor Sie einen Funktionswunsch als Issue einreichen." + }, + "coc": { + "title": "Lesen Sie zuerst den Verhaltenskodex", + "body": "Lesen Sie vor dem Einreichen von Code oder Fehlermeldungen kurz den Verhaltenskodex und die bewährten Vorgehensweisen. Darin werden die Verantwortung für Code, die Meldung von Sicherheitslücken und der Umfang von PRs erläutert." + } +} diff --git a/web/messages/de/docs/about/faq.json b/web/messages/de/docs/about/faq.json new file mode 100644 index 00000000..737e140c --- /dev/null +++ b/web/messages/de/docs/about/faq.json @@ -0,0 +1,94 @@ +{ + "meta": { + "title": "Häufige Fragen | ProxMenux-Dokumentation", + "description": "Häufige Fragen zu ProxMenux: Funktionsumfang, Installation, Kompatibilität ab PVE 8, Anpassung, Updates, Sicherheit, Produktionseinsatz, Systemdateien, Beiträge und Deinstallation.", + "ogTitle": "Häufige Fragen | ProxMenux-Dokumentation", + "ogDescription": "Häufige Fragen zu Installation, Kompatibilität, Sicherheit und Beiträgen zu ProxMenux." + }, + "header": { + "title": "Häufig gestellte Fragen", + "description": "Häufige Fragen zu ProxMenux: Funktionsumfang, Zielgruppe, Installation, Updates, Deinstallation, Kompatibilität, Anpassung, Sicherheit und Beiträge. Fehlt Ihre Frage, sehen Sie in GitHub Discussions nach oder erstellen Sie ein Issue.", + "section": "Über" + }, + "quickLinks": { + "title": "Schnelle Links", + "installationLabel": "Installation", + "installationSuffix": "— der One-Line-Installationsbefehl + Abhängigkeiten.", + "introductionLabel": "Einleitung", + "introductionSuffix": "— was ProxMenux tut, in 5 Minuten.", + "uninstallLabel": "Deinstallieren", + "uninstallSuffix": "— sauberes Entfernen.", + "issuesLabel": "GitHub Issues", + "discussionsLabel": "Diskussionen" + }, + "q1": { + "question": "Was ist ProxMenux und wofür wird es verwendet?", + "p1Rich": "ProxMenux ist ein interaktives menügesteuertes Tool, das Proxmox VE für alle Benutzer unabhängig von ihrer technischen Erfahrung zugänglich macht. Es vereinfacht die Befehlsausführung und ermöglicht es Benutzern, Aktionen auf ihrem System durchzuführen, ohne dass fortgeschrittene Linux-Kenntnisse erforderlich sind.", + "p2": "Weniger erfahrene Benutzer können Befehle über übersichtliche Menüs ausführen und müssen seltener selbst im Terminal tippen. Erfahrenen Administratoren erspart ProxMenux Eingaben bei wiederkehrenden Aufgaben wie der Absicherung nach der Installation, GPU-Passthrough, OVF-VM-Importen und der Rotation von ZFS-Snapshots.", + "p3": "Proxmox VE ist weit verbreitet für:", + "items": [ + "Virtualisierung auf Unternehmensebene", + "HomeLab und persönliche Cloud-Lösungen", + "Multimedia-Server, Automatisierung und mehr" + ] + }, + "q2": { + "question": "Wie installiere ich ProxMenux?", + "p1Rich": "Folgen Sie der Installationsanleitung. Kurz zusammengefasst:", + "stableInstall": "bash -c \"$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)\"", + "p2": "Einmal installiert, starten Sie es einfach mit:", + "menuCmd": "menu" + }, + "q3": { + "question": "Ist ProxMenux mit allen Proxmox-Versionen kompatibel?", + "bodyRich": "Nein — ProxMenux benötigt Proxmox VE 8 oder neuer. PVE 7 und ältere Versionen werden nicht unterstützt. Aktualisieren Sie PVE 7 zunächst nach der offiziellen Proxmox-Anleitung auf 8.x. Den anschließenden Wechsel von 8 auf 9 können Sie über Werkzeuge → PVE 8 auf PVE 9 aktualisieren in ProxMenux durchführen." + }, + "q4": { + "question": "Kann ich ProxMenux anpassen?", + "p1Rich": "Lokale Änderungen an den Hauptskripten können beim Aktualisieren überschrieben werden. Bearbeiten Sie diese Dateien daher möglichst nicht direkt. Folgendes können Sie anpassen:", + "items": [ + "Das Konsolenbanner über das FastFetch Tool in den Post-Install-Optionen.", + "Welche optionalen Komponenten installiert werden (Monitor, Fail2Ban, Lynis...) – wählen Sie während der Installation oder später über das entsprechende Menü aus." + ] + }, + "q5": { + "question": "Wie aktualisiere ich ProxMenux?", + "p1Rich": "Wenn eine neue Version verfügbar ist, erkennt ProxMenux sie beim nächsten Aufruf von menu und bietet ein Update an. Stimmen Sie zu, werden die Dateien und die Konfiguration des Werkzeugs in der bestehenden Installation ersetzt. Ein manueller Download ist nicht nötig.", + "p2Rich": "Benutzer der stabilen Version erhalten stabile Updates, Beta-Benutzer die Beta-Updates. Sobald eine stabile Version erscheint, informiert die Beta beim nächsten Menüstart darüber und bietet den automatischen Wechsel an. Weitere Informationen finden Sie im Beta-Programm." + }, + "q6": { + "question": "Wo kann ich Probleme melden?", + "p1Rich": "Melden Sie Fehler über GitHub Issues. Beschreiben Sie die Schritte zum Reproduzieren, den Fehlertext und gegebenenfalls die Monitor-Ausgabe von journalctl -u proxmenux-monitor -n 50.", + "p2Rich": "Bei Sicherheitsproblemen: Veröffentlichen Sie keine Details. Folgen Sie dem Verfahren zur verantwortungsvollen Meldung im Verhaltenskodex und den bewährten Vorgehensweisen." + }, + "q7": { + "question": "Kann ich zu ProxMenux beitragen?", + "p1Rich": "Unbedingt. ProxMenux ist ein gemeinschaftliches Open-Source-Projekt. Beiträge sind als Code (Bash, Next.js/TypeScript und Python-Übersetzungswerkzeuge), Tests, Dokumentation, Design oder Ideen willkommen.", + "item1Rich": "Besprechen Sie Ideen in GitHub Discussions, bevor Sie eine Feature Request öffnen.", + "item2Rich": "Lesen Sie vor einem Pull Request den Verhaltenskodex und die bewährten Vorgehensweisen.", + "item3Rich": "Auf der Mitwirkendenseite finden Sie die aktuellen Tester und Reviewer." + }, + "q8": { + "question": "Ändert ProxMenux kritische Systemdateien?", + "p1Rich": "Bei der Installation von ProxMenux werden folgende Komponenten hinzugefügt.", + "item1Rich": "Abhängigkeiten über apt: dialog, curl, jq und git. Die Übersetzungen verwenden vorgefertigte JSON-Kataloge. Der Installer legt auf dem Host keine virtuelle Python-Umgebung für Übersetzungen an.", + "item2Rich": "Der Skriptbaum bei /usr/local/share/proxmenux/.", + "item3Rich": "Das Startskript unter /usr/local/bin/menu.", + "item4Rich": "Optional: der systemd-Dienst von Monitor unter proxmenux-monitor.service.", + "p2Rich": "ProxMenux verändert die zentrale Proxmox-Konfiguration nicht, es sei denn, Sie fordern dies ausdrücklich über ein entsprechendes Menü an. Die Netzwerkreparatur bearbeitet beispielsweise /etc/network/interfaces, jedoch erst nach Ihrer Bestätigung und dem Anlegen einer Sicherung." + }, + "q9": { + "question": "Ist es sicher, ProxMenux in der Produktion zu verwenden?", + "bodyRich": "Ja. Da ProxMenux die zentralen Proxmox-Dateien nur auf ausdrücklichen Wunsch verändert, kann es in Produktionsumgebungen eingesetzt werden. Testen Sie automatisierte Abläufe zunächst in einer kontrollierten Umgebung und erstellen Sie Sicherungen, bevor Sie destruktive Vorgänge ausführen, etwa das Upgrade von PVE 8 auf 9, das Formatieren eines Datenträgers oder das Löschen einer VM." + }, + "q10": { + "question": "Wie deinstalliere ich ProxMenux?", + "p1Rich": "In ProxMenux: Einstellungen → ProxMenux deinstallieren. Die Deinstallation entfernt den Skriptbaum, das Startskript und den Monitor-Dienst sowie auf Wunsch ausgewählte Abhängigkeiten. /root/.bashrc und /etc/motd werden aus den von Post-Install angelegten Sicherungen wiederhergestellt, sofern diese vorhanden sind (andernfalls wird die ProxMenux-Zeile aus der MOTD entfernt).", + "p2Rich": "Die vollständige Anleitung finden Sie unter ProxMenux deinstallieren." + }, + "q11": { + "question": "VirusTotal markiert die Installations-URL - ist es sicher?", + "p1Rich": "Der in Issue #162 gemeldete Treffer von Chong Lua Dao bei 1 von 95 Prüfungen wurde vom Maintainer als Fehlalarm eingestuft. Der Installer wird mit einem Befehl gestartet, der das Skript herunterlädt und ausführt (bash -c \"$(wget -qLO - …)\") und kann jq aus dessen offizieller GitHub-Veröffentlichung herunterladen. Dieses Verhalten kann heuristische Scanner auslösen. Der damalige Bericht ist keine Bewertung aller künftigen Downloads.", + "p2Rich": "ProxMenux ist vollständig quelloffen. Sie können das Installationsskript Zeile für Zeile prüfen: install_proxmenux.sh. Weitere Hintergründe finden Sie in Issue #162." + } +} diff --git a/web/messages/de/docs/about/index.json b/web/messages/de/docs/about/index.json new file mode 100644 index 00000000..335ab2d4 --- /dev/null +++ b/web/messages/de/docs/about/index.json @@ -0,0 +1,67 @@ +{ + "meta": { + "title": "Über das Projekt | ProxMenux-Dokumentation", + "description": "Über ProxMenux: Projektziele, Mitwirkende, FAQ und Verhaltenskodex. Open-Source-Menümanagement-Tool für Proxmox VE 8+, erstellt und gepflegt von MacRimi mit einem kleinen Team von Testern und Mitwirkenden.", + "ogTitle": "Über das Projekt | ProxMenux-Dokumentation", + "ogDescription": "Über ProxMenux — ein menügesteuertes Open-Source-Werkzeug zur Verwaltung von Proxmox VE 8+." + }, + "header": { + "title": "Über", + "description": "Projektinformationen, FAQ, Mitwirkende und Verhaltenskodex. ProxMenux ist ein Open-Source-Menümanagement-Tool für Proxmox VE 8+, das von MacRimi mit Hilfe eines kleinen Teams von Testern und Mitwirkenden entwickelt und gewartet wird.", + "section": "Über" + }, + "callout": { + "title": "Open Source, von der Community getragen", + "body": "ProxMenux steht unter der GPL-3.0-Lizenz und wird öffentlich auf GitHub entwickelt. Jedes Skript lässt sich prüfen, jede Änderung nachvollziehen. Beiträge in Form von Fehlermeldungen, Ideen, Code und Tests sind willkommen." + }, + "section": { + "heading": "In diesem Abschnitt", + "options": [ + { + "icon": "HelpCircle", + "href": "/docs/about/faq", + "title": "FAQ", + "description": "Häufig gestellte Fragen: Was ist ProxMenux, Installation, Kompatibilität, Anpassung, Updates, Sicherheit, Produktionsnutzung, Deinstallation." + }, + { + "icon": "Users", + "href": "/docs/about/contributors", + "title": "Mitwirkende", + "description": "Anerkennung für die Menschen, die ProxMenux möglich machen: Tester, Reviewer und Entwickler, die jede Version mitgestalten." + }, + { + "icon": "ScrollText", + "href": "/docs/about/code-of-conduct", + "title": "Verhaltenskodex", + "description": "Projektwerte, Verantwortung für eingereichten Code und Beitragsrichtlinien. Pflichtlektüre vor dem Einreichen eines PR oder einer Fehlermeldung." + } + ] + }, + "involved": { + "heading": "Beteiligen Sie sich", + "intro": "Drei Orte, um sich mit dem Projekt zu beschäftigen:", + "cards": [ + { + "href": "https://github.com/MacRimi/ProxMenux/issues/new", + "title": "🐛 Einen Bug melden", + "description": "GitHub Issues — fügen Sie Protokolle und Schritte zum Nachstellen bei." + }, + { + "href": "https://github.com/MacRimi/ProxMenux/discussions", + "title": "💡 Schlagen Sie ein Feature vor", + "description": "GitHub Discussions — teilen Sie Ideen, bevor Sie eine PR öffnen." + }, + { + "href": "https://github.com/MacRimi/ProxMenux/pulls", + "title": "🔀 Einen PR einreichen", + "description": "GitHub Pull Requests – lesen Sie zuerst den Verhaltenskodex." + } + ] + }, + "support": { + "heading": "Unterstützung des Projekts", + "introRich": "Wenn ProxMenux Ihnen Zeit spart, ist der einfachste Weg, anderen zu helfen, es zu entdecken, dem Repo ein ⭐ auf GitHub zu geben. Wenn Sie weiter gehen wollen:", + "kofiLabel": "Auf Ko-fi unterstützen", + "kofiOutro": "- kauft dem Betreuer einen Kaffee und hält die Entwicklung am Laufen." + } +} diff --git a/web/messages/de/docs/backup-restore/creating-backups.json b/web/messages/de/docs/backup-restore/creating-backups.json new file mode 100644 index 00000000..b33b1a24 --- /dev/null +++ b/web/messages/de/docs/backup-restore/creating-backups.json @@ -0,0 +1,220 @@ +{ + "meta": { + "title": "Backups erstellen – interaktiver Backup-Flow | ProxMenux", + "description": "Der interaktive Backup-Ablauf in ProxMenux: zwei Einstiegspunkte (das TUI-Menü von Scripts und die Weboberfläche von Monitor), drei Ziele, zwei Profilmodi, ein gemeinsamer Vorbereitungsschritt und ein Bestätigungsdialog. Beschrieben werden die Matrix mit sechs Optionen, das Standardprofil und das benutzerdefinierte Profil sowie die Schritte von der Auswahl eines Backups bis zum Speichern des Archivs am Ziel.", + "ogTitle": "ProxMenux Backup — Erstellen von Backups", + "ogDescription": "Der interaktive Backup-Flow mit drei Destinationen, zwei Profilen und einem gemeinsamen Staging-Schritt.", + "twitterTitle": "Backups erstellen | ProxMenux", + "twitterDescription": "Interaktiver Backup-Flow mit drei Zielen und zwei Profilmodi." + }, + "header": { + "title": "Backups erstellen", + "description": "Der interaktive Backup-Ablauf: Wählen Sie ein Ziel und ein Profil, prüfen Sie die Zusammenfassung und verfolgen Sie, wie das Archiv gespeichert wird. Beide Einstiegspunkte verwenden dasselbe Backend und erzeugen identische Archive.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Zwei Einstiegspunkte, identische Funktionalität", + "body": "Die TUI von Scripts und die Weboberfläche von Monitor bieten genau dieselben Funktionen. Jedes Backup — manuell oder geplant — durchläuft dieselbe Auswahlmatrix (drei Ziele × zwei Profile) und ruft pro Zelle dieselbe Backend-Funktion auf (_bk_pbs, _bk_borg oder _bk_local). Die über beide Einstiegspunkte erzeugten Archive sind nicht zu unterscheiden. Die Wahl ist Geschmackssache: Die TUI eignet sich für SSH und Skripte; Monitor bietet Mausbedienung sowie Ansichten für Benachrichtigungen und laufende Protokollausgaben." + }, + "entryPoints": { + "heading": "Die beiden Eingangspunkte", + "rows": [ + { + "entry": "ProxMenux Scripts (TUI)", + "path": "menu → Utilities → Host Config Backup", + "detail": "Dialogbasierter Flow, SSH-freundlich. Das Hauptmenü präsentiert die sechs Optionen direkt. Verwendet backup_menu in backup_host.sh." + }, + { + "entry": "ProxMenux Monitor (Weboberfläche)", + "path": "Backups tab → Create backup", + "detail": "Assistentengestützter Ablauf. Dieselben sechs Optionen erscheinen als zweistufiges Formular (Ziel → Profil). Über die Flask-API werden dieselben Backend-Funktionen aufgerufen." + } + ] + }, + "modes": { + "heading": "Manuelle vs. geplante Backups", + "body": "Backups können in zwei Modi erstellt werden: manuell (der interaktive Fluss, den diese Seite dokumentiert - der Benutzer wählt ein Ziel und ein Profil aus einem Menü aus und verfolgt das Speichern des Archivs) oder geplant (ein unbeaufsichtigter Job, der auf einem Cron-Timer läuft und die für den Job konfigurierte Aufbewahrung anwendet). Beide Modi unterstützen die gleichen drei Ziele und die gleichen zwei Profile, und beide sind von beiden Einstiegspunkten verfügbar - dem Scripts TUI-Menü und der Registerkarte Monitor-Backups. Geplante Jobs verwenden die gleichen Backend-Funktionen wie der manuelle Ablauf durch run_scheduled_backup.sh; die produzierten Archive sind nicht zu unterscheiden.", + "seeAlso": "Die Seite mit geplanten Aufträgen deckt den gesamten Ablauf ab, einschließlich der Art und Weise, wie Sie einen Auftrag erstellen, ihn an einen vorhandenen PVE vzdump-Timer anhängen und die Retentionswerte konfigurieren.", + "monitorAlt": "ProxMenux Monitor Backups Registerkarte zeigt den Neuen geplanten Backup-Dialog mit Ziel, Profil, Zeitplan und Aufbewahrungsfelder.", + "monitorCaption": "Geplantes Backup — ProxMenux Monitor. Derselbe Wizard-Dialog, der ein manuelles Backup erstellt, trägt die Zeitplan- und Aufbewahrungsfelder unten für den unbeaufsichtigten Pfad." + }, + "matrix": { + "heading": "Die Matrix mit sechs Optionen", + "intro": "Die Auswahl bestimmt sowohl das ausgeführte Backend als auch die Strategie zur Pfadauswahl. Die zielspezifischen Seiten erläutern die Konfiguration jeder Zelle.", + "rows": [ + { + "combo": "1", + "destination": "PBS", + "profile": "Standard", + "action": "Lädt das Standardprofil plus persistente Extras in ein konfiguriertes PBS-Repository hoch." + }, + { + "combo": "2", + "destination": "Borg", + "profile": "Standard", + "action": "Erstellt ein Archiv mit dem Standardprofil plus persistenten Extras im ausgewählten Borg-Repository." + }, + { + "combo": "3", + "destination": "Lokal", + "profile": "Standard", + "action": "Schreibt ein .tar.zst-Archiv mit dem Standardprofil plus persistenten Extras an das konfigurierte lokale Ziel." + }, + { + "combo": "4", + "destination": "PBS", + "profile": "Benutzerdefiniert", + "action": "Öffnet vor dem PBS-Upload die Pfadauswahl; der Benutzer markiert die gewünschten Pfade und kann neue hinzufügen." + }, + { + "combo": "5", + "destination": "Borg", + "profile": "Benutzerdefiniert", + "action": "Öffnet den Pfadwähler, bevor das Borg-Archiv erstellt wird." + }, + { + "combo": "6", + "destination": "Lokal", + "profile": "Benutzerdefiniert", + "action": "Öffnet den Pfadwähler vor dem Schreiben des lokalen .tar.zst." + } + ] + }, + "profiles": { + "heading": "Standardprofil und benutzerdefiniertes Profil", + "defaultTitle": "Standardprofil", + "defaultBody": "Das Standardprofil ist die kuratierte Liste aus hb_default_profile_paths (dokumentiert in Funktionsweise unter Pfadkategorien) sowie jeder Eintrag in der persistenten Extras-Datei /usr/local/share/proxmenux/backup-extra-paths.txt. Der Benutzer bestätigt die Ziel- und Verschlüsselungsoptionen und das Backup verläuft ohne weitere Pfadauswahl.", + "customTitle": "Benutzerdefiniertes Profil", + "customBody": "Das benutzerdefinierte Profil öffnet eine Checkliste mit allen Pfaden des Standardprofils (nicht markiert) und allen persistenten Extras (vormarkiert, mit dem Präfix [+]). Der Benutzer markiert die Pfade für diesen Durchlauf und kann über Benutzerdefinierten Pfad hinzufügen einen neuen absoluten Pfad ergänzen. Jeder dabei hinzugefügte Pfad wird in backup-extra-paths.txt gespeichert, damit künftige Backups ihn automatisch berücksichtigen. Das Abwählen eines persistenten Extras gilt nur für diesen Durchlauf und löscht es nicht aus der Datei — dafür gibt es außerhalb des Backup-Ablaufs die separate Aktion Benutzerdefinierte Pfade verwalten.", + "customPickerAlt": "Benutzerdefinierte Profil-Checkliste mit den Standard-Profilpfaden (nicht markiert) und persistenten Extras (vormarkiert mit einem [+]-Präfix) sowie Schaltflächen, um einen neuen Pfad hinzuzufügen oder die Auswahl zu bestätigen.", + "customPickerCaption": "Benutzerdefiniertes Profil — die Pfadauswahl. Die Pfade des Standardprofils sind nicht markiert; persistente Extras sind mit dem Präfix [+] vormarkiert. Der Benutzer markiert die Pfade für diesen Durchlauf.", + "manageCustomAlt": "Menü zur Verwaltung benutzerdefinierter Pfade mit der Liste persistenter Extras und Optionen zum Hinzufügen, Entfernen und Bearbeiten.", + "manageCustomCaption": "Benutzerdefinierte Pfade verwalten - der Einstiegspunkt, an dem persistente Extras hinzugefügt oder entfernt werden. Jeder hier aufgeführte Pfad wird automatisch in Backups im Standardmodus aufgenommen, ohne dass die benutzerdefinierte Pfadauswahl geöffnet werden muss." + }, + "commonPipeline": { + "heading": "Was läuft unabhängig vom Ziel", + "intro": "Nachdem das Profil aufgelöst wurde, führt jedes Backend die gleiche Staging-Pipeline aus, bevor es in seinen eigenen Upload-Pfad divergiert. hb_prepare_staging stellt den Archivbaum in /tmp/proxmenux-DESTINATION-stage.XXXXXX zusammen und füllt jede der drei Nutzlasten aus.", + "steps": [ + { + "step": "1", + "name": "rootfs zusammenstellen", + "detail": "Führt rsync -a für jeden ausgewählten Pfad in staging_root/rootfs/ aus. Die pmxcfs-Datenbank bei /var/lib/pve-cluster/config.db wird separat über sqlite3 .backup erfasst, um einen konsistenten Snapshot zu erstellen, ohne den Cluster zu stoppen, und die .db-wal / .db-shm Sidecars sind vom rsync ausgeschlossen; Wenn sqlite3 nicht verfügbar ist, wird stattdessen eine Datei config.db.raw-fallback abgelegt, die bei der Wiederherstellung während des Imports als Datenbank übernommen wird. Flüchtige Subpfade (Bash-Historie, Caches, Trash) sind von /root/ ausgeschlossen. Pfade, die in der Quelle fehlen, werden in metadata/missing_paths.txt aufgezeichnet, ohne das Backup zu stoppen." + }, + { + "step": "2", + "name": "Manifest erstellen", + "detail": "build_manifest.sh steuert die sechs Datensammler und schreibt manifest.json in das Hauptverzeichnis des Staging-Bereichs. Fällt ein Datensammler aus, erhält der betroffene Abschnitt einen dokumentierten leeren Standardwert; das Manifest bleibt gültig." + }, + { + "step": "3", + "name": "Paketbestand", + "detail": "apt-mark showmanual wird wortwörtlich in metadata/packages.manual.list erfasst. Der Komponentenzustand befindet sich bereits innerhalb der wiederhergestellten Rootfs (components_status.json), da /usr/local/share/proxmenux/ Teil des Standardprofils ist." + }, + { + "step": "4", + "name": "Informationen zum Durchlauf", + "detail": "metadata/run_info.env zeichnet die Backup-Laufidentität (Hostname, Zeitstempel, Kernelversion) auf, die von der Kompatibilitätsprüfung der Wiederherstellung zur Bestimmung der kernübergreifenden Richtung verwendet wird." + }, + { + "step": "5", + "name": "Mitteilung (Beginn)", + "detail": "hb_notify_lifecycle \"start\" wird ausgelöst. Sind im Monitor Benachrichtigungen konfiguriert, wird das Ereignis Host-Backup gestartet für den Benutzer ausgegeben. Ohne konfigurierte Kanäle erfolgt keine Benachrichtigung." + } + ] + }, + "included": { + "heading": "Was reingeht und was draußen bleibt", + "intro": "Jeder Pfad im aufgelösten Profil (Standard + persistente Extras + Custom-Mode-Auswahl) wird mit rsync -aAXH --numeric-ids kopiert. Eine gemeinsame Ausschlussliste gilt für jeden Pfad, und zwei Verzeichnisse tragen zusätzliche pfadspezifische Ausschlüsse.", + "globalTitle": "Globale Ausschlüsse (auf jeden Pfad angewendet)", + "globalItems": [ + "images/ — Image Dumps.", + "dump/ — Ausgaben von vzdump.", + "tmp/ — temporäre Dateien.", + "*.log — Protokolldateien." + ], + "rootTitle": "/root/ Ausschlüsse", + "rootBody": "/root/ ist Teil des Standardprofils, sodass Benutzerskripte und Konfigurationen im Archiv landen. Flüchtige Subpfade werden fallen gelassen:", + "rootItems": [ + ".bash_history", + ".cache/", + "tmp/", + ".local/share/Trash/" + ], + "proxmenuxTitle": "/usr/local/share/proxmenux/ Ausschlüsse", + "proxmenuxBody": "Dieses Verzeichnis liefert nur Benutzerstatus - components_status.json, Einstellungen, Cache nach der Installation. Code, den das Ziel bereits von seiner eigenen ProxMenux-Installation haben wird, ist ausgeschlossen, so dass eine Wiederherstellung die aktuellen Binärdateien des Ziels nicht mit älteren überschreibt:", + "proxmenuxItems": [ + "restore-pending/, scripts/, web/", + "monitor-app/, monitor-app.*/, AppImage/", + "images/, json/", + "utils.sh, helpers_cache.json", + "ProxMenux-Monitor.AppImage*, install_proxmenux*.sh" + ], + "notInProfileTitle": "Pfade außerhalb des Profils", + "notInProfileBody": "Alles, was nicht in hb_default_profile_paths aufgeführt ist und nicht als benutzerdefiniertes oder persistentes Extra hinzugefügt wurde, ist nicht Teil des Backups. Bemerkenswerte Beispiele:", + "notInProfileItems": [ + "VM und LXC Festplatten — werden von vzdump behandelt, nicht von dieser Funktion. Gastkonfigurationsdateien unter /etc/pve/nodes/*/qemu-server/*.conf und lxc/*.conf werden erfasst (sie leben unter /etc/pve), so dass die Wiederherstellung das Inventar reproduziert; die Festplatten selbst werden von einem vorhandenen vzdump / PBS-Backup neu angehängt.", + "/boot und /boot/efi — Kernel-Binärdateien, Initramfs und die UEFI-ESP-Partition werden nach der Wiederherstellung durch die eigenen update-initramfs, update-grub oder proxmox-boot-tool refresh des Ziels regeneriert. Der Bootloader wird niemals wörtlich kopiert.", + "Kernel und Systemlaufzeit-Dateisysteme — /proc, /sys, /dev und /run sind Pseudo-Dateisysteme, die vom Kernel und von udev erzeugt werden; sie werden nirgendwo persistent.", + "Paket-Binärdateien unter /usr/bin, /usr/lib, /lib, /sbin — vom APT des Ziels aus packages.manual.list neu installiert.", + "/var/log/, /var/tmp/, /var/cache/ — Laufzeitzustand pro Host, nicht wiederhergestellt.", + "/home/USER — nicht im Standardprofil. Fügen Sie es als benutzerdefinierten Pfad hinzu, wenn ein System Heimverzeichnisse des Benutzers trägt, die eine Wiederherstellung überleben müssen." + ], + "customPathsTitle": "Wie benutzerdefinierte Pfade gehandhabt werden", + "customPathsBody": "Ein benutzerdefinierter Pfad, der im benutzerdefinierten Modus hinzugefügt wurde oder in backup-extra-paths.txt persistiert wurde, durchläuft dieselbe rsync-Pipeline wie Standardprofilpfade. Globale Ausschlüsse gelten. Wenn der benutzerdefinierte Pfad unter /root/ oder /usr/local/share/proxmenux/ liegt, gelten die oben genannten pfadspezifischen Ausschlüsse weiterhin. Jeder archivierte Pfad – standardmäßig oder benutzerdefiniert – wird in metadata/paths_archived.txt aufgezeichnet. Pfade, die in der Quelle nicht vorhanden sind, werden in metadata/missing_paths.txt aufgezeichnet, ohne das Backup zu stoppen." + }, + "archiveStructure": { + "heading": "Archivstruktur", + "intro": "Das von jedem Backend erstellte Staging-Verzeichnis folgt unabhängig vom Ziel dem gleichen Layout. Das Tarball-, PBS .pxar oder Borg-Archiv speichert diesen Baum wortwörtlich.", + "tree": "backup-[timestamp]/\n├── manifest.json # structured host state (kernel_params, hardware, storage, guests, components, source_host)\n├── metadata/\n│ ├── packages.manual.list # output of apt-mark showmanual\n│ ├── run_info.env # hostname, timestamp, kernel version, pmxcfs method\n│ ├── paths_archived.txt # exact list of paths that reached rootfs/\n│ └── missing_paths.txt # paths from the profile absent on source\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/systemd/network, /etc/ssh, /etc/apt, ...\n ├── root/ # /root without volatile subpaths\n ├── usr/local/ # /usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux (state only)\n └── var/ # /var/lib/pve-cluster (config.db via sqlite3 .backup), /var/spool/cron/crontabs" + }, + "confirmation": { + "heading": "Bestätigungszusammenfassung", + "body": "Bevor das Backend etwas an das Ziel schreibt, zeigt ProxMenux einen zusammenfassenden Dialog mit dem Ziel, der Backup-ID oder dem Archivnamen, dem Verschlüsselungsstatus und der Liste der zu kopierenden Pfade an. Das Abbrechen hier bricht das Backup sauber ab - das Staging-Verzeichnis wird durch den trap-Hook entfernt, der auf der Backend-Funktion eingestellt ist, und keine Teildaten erreichen das Ziel." + }, + "writing": { + "heading": "Schreiben an den Bestimmungsort", + "intro": "Sobald der Benutzer bestätigt, führt jedes Backend seinen eigenen Schreibschritt aus. Die Mechanik wird auf den Zielseiten behandelt; die gemeinsame Oberfläche ist das Protokoll, das Sidecar und die Abschlussbenachrichtigung.", + "rows": [ + { + "topic": "Protokolldatei", + "detail": "Jedes Backend schreibt seine vollständige Ausgabe in /tmp/proxmenux-DESTINATION-backup-YYYYMMDD_HHMMSS.log und bietet bei Ausfall an, sie in einem scrollbaren Dialog zu öffnen. Der Protokollpfad wird in der Abschlusszusammenfassung nur dann gedruckt, wenn die Datei Inhalt hat." + }, + { + "topic": "Begleitdatei (nur lokal)", + "detail": "hb_write_archive_sidecar legt einen *.proxmenux.json neben das lokale Archiv, so dass der Monitor ihn als ProxMenux-Host-Backup auch nach Umzügen oder Umbenennungen identifiziert." + }, + { + "topic": "Benachrichtigung (Abschluss/Fehler)", + "detail": "hb_notify_lifecycle \"complete\" oder \"fail\" wird mit Dauer, Archivgröße und — bei Fehlern — der letzten nach einem Fehler aussehenden Protokollzeile ausgelöst." + } + ] + }, + "finishedScreens": { + "heading": "Wie ein fertiges Backup aussieht", + "intro": "Das gleiche Abschlussereignis wird an beiden Einstiegspunkten angezeigt. Die TUI schreibt einen Zusammenfassungsblock in das Terminal; die Registerkarte Backups des Monitors zeigt den Ablauf in der Archivliste mit Größe, Dauer und Statusabzeichen an.", + "scriptsAlt": "ProxMenux Scripts TUI zeigt ein fertiges Host-Backup an - Ziel, Backup-ID, Snapshot-Pfad, Datengröße, Dauer und Verschlüsselungszustand.", + "scriptsCaption": "Abgeschlossenes Backup — ProxMenux Scripts (TUI). Der Vervollständigungsblock druckt das Ziel, die Backup-ID, den resultierenden Snapshot- oder Archivnamen, die Datengröße, die Dauer und den Verschlüsselungszustand.", + "monitorAlt": "Die Registerkarte ProxMenux Monitor Backups zeigt einen fertigen Host-Backup-Eintrag mit Größe, Dauer, Methodenabzeichen und Verschlüsselungsindikator.", + "monitorCaption": "Abgeschlossenes Backup — ProxMenux Monitor Backups Registerkarte. Das neue Backup erscheint in der Archivliste mit dem Zielmethode-Badge, der Größe, der Dauer und gegebenenfalls dem Verschlüsselungssperrindikator." + }, + "whereNext": { + "heading": "Wohin Sie als nächstes gehen", + "items": [ + { + "label": "Ziele", + "href": "/docs/backup-restore/destinations", + "tail": "— Konfigurationsdetails für Local, PBS und Borg." + }, + { + "label": "Geplante Aufträge", + "href": "/docs/backup-restore/scheduled-jobs", + "tail": "- das gleiche Backup unbeaufsichtigt nach einem Zeitplan anstatt interaktiv auszuführen." + }, + { + "label": "Wiederherstellung", + "href": "/docs/backup-restore/restoring", + "tail": "— der Fluss, der verbraucht, was diese Seite produziert." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/cross-kernel.json b/web/messages/de/docs/backup-restore/cross-kernel.json new file mode 100644 index 00000000..fb78e27e --- /dev/null +++ b/web/messages/de/docs/backup-restore/cross-kernel.json @@ -0,0 +1,170 @@ +{ + "meta": { + "title": "Wiederherstellung zwischen Kernel-Versionen — Richtungserkennung, Sicherheitsfilter und Ergänzung | ProxMenux", + "description": "Wie ProxMenux ein Host-Backup auf einem Ziel mit einer anderen Hauptkernelversion wiederherstellt. Dokumentiert, wie die Richtung der Fehlanpassung erkannt wird, den Safe-Subset-Filter, der bootkritische Pfade überspringt, wenn das Ziel einen neueren Kernel als das Backup ausführt, und die vierphasige Kernel-agnostische Hydratation, die Benutzertuning wie IOMMU, VFIO IDs und benutzerdefinierte Cmdline-Token erneut anwendet, ohne Kernel-gebundene Dateien wörtlich zu kopieren.", + "ogTitle": "ProxMenux Backup — kernübergreifende Wiederherstellung und Hydratation", + "ogDescription": "Wiederherstellung zwischen Kernel-Versionen mit Richtungserkennung, sicherer Pfadauswahl und kernelunabhängiger Ergänzung.", + "twitterTitle": "Cross-Kernel-Wiederherstellung | ProxMenux", + "twitterDescription": "Wie ProxMenux eine Wiederherstellung durchführt, wenn sich der Zielkernel vom Backup unterscheidet." + }, + "header": { + "title": "Kernübergreifende Wiederherstellung", + "description": "Wie ProxMenux eine Wiederherstellung durchführt, wenn der Zielhost einen anderen Kernel als den im Backup aufgezeichneten ausführt - insbesondere wenn das Ziel einen neueren Kernel ausführt. Dokumentiert, wie der Unterschied erkannt wird, wie bootkritische Pfade, die das Ziel unterbrechen könnten, gefiltert werden und wie die eigene Konfiguration des Benutzers erneut angewendet wird, ohne Kernel-gebundene Dateien wörtlich zu kopieren.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Jeder Kernelsprung wird anders gehandhabt", + "body": "Der Kernel eines Proxmox-Hosts wird durch eine Versionszeichenfolge wie 6.17.13-2-pve identifiziert. Die ersten beiden Zahlen (6.17 in diesem Beispiel) definieren die Hauptversion: Wenn sie sich ändern, sind einige Konfigurationsdateien, die für den vorherigen Kernel geschrieben wurden, möglicherweise nicht für den neuen gültig. Wenn die Wiederherstellung erkennt, dass das Backup und das Ziel unterschiedliche Hauptkernelversionen haben, verzweigt sich der Fluss basierend auf der Richtung des Sprungs - ob das Backup älter oder neuer als das Ziel ist - weil die beiden Fälle entgegengesetzte Fehlermodi haben. Backups, die neuer sind als das Ziel, stellen sauber wieder her, wie sie sind (bench-verifiziert über mehrere Kernelsprünge). Backups, die älter als das Ziel sind, benötigen einen Filter, um zu vermeiden, dass der Boot des Ziels mit einer Konfiguration unterbrochen wird, die für einen Kernel geschrieben wurde, der sich seitdem geändert hat." + }, + "directionCheck": { + "heading": "Die Richtungskontrolle", + "intro": "Während der Kompatibilitätsprüfung vergleicht hb_compat_check den im Backup-Manifest aufgezeichneten Kernel mit dem aktuellen Kernel des Ziels (uname -r) und klassifiziert die Wiederherstellung in einen von drei Fällen, die in der internen Variable HB_COMPAT_KERNEL_DIRECTION gespeichert sind:", + "rows": [ + { + "direction": "same", + "condition": "Die Hauptkernelversion stimmt zwischen Backup und Target überein.", + "behavior": "Der vollständige Restaurationsfluss läuft unverändert. Kein extra Filter, keine Hydratation, kein besonderer Hinweis in der UI." + }, + { + "direction": "bk_newer", + "condition": "Der Backup-Kernel ist NEUER als der des Ziels (zum Beispiel: Das Backup wurde auf dem Kernel 7.0 erstellt und das Ziel führt den Kernel 6.17 aus).", + "behavior": "Der vollständige Wiederherstellungsfluss läuft unverändert, genau wie same. Es wird kein Filter angewendet. Empirisch verifiziert: Treiber werden automatisch gegen den Zielkernel neu installiert, IOMMU config wird sauber angewendet, VMs mit GPU-Durchgang kommen ohne Probleme auf." + }, + { + "direction": "bk_older", + "condition": "Der Backup-Kernel ist älter als der des Ziels (zum Beispiel: Das Backup wurde auf dem Kernel 6.17 erstellt und das Ziel führt den Kernel 7.0 aus).", + "behavior": "Der Safe-Subset-Filter und die folgende vierphasige Hydratation werden aktiviert. Die Wiederherstellung geht weiter - das Ziel reproduziert die Quelle, aber bootkritische Dateien werden nicht wörtlich kopiert." + } + ] + }, + "whyBkNewerIsSafe": { + "heading": "Warum ein Backup mit einem neueren Kernel als das Ziel unverändert wiederherstellt", + "body": "Wenn die Wiederherstellung gegen ein Ziel mit einem älteren-Kernel als dem im Backup aufgezeichneten läuft, ist jeder Mechanismus, von dem die Wiederherstellung abhängt, unabhängig von der Kernelversion. GPU-Treiberinstallateure und andere PCI-Geräteinstallateure erkennen den laufenden Kernel mit uname -r und kompilieren DKMS mit dem, was das Ziel hat. Die APT-Paketinstallation zieht Binärdateien, die für die Verteilung des Ziels erstellt wurden. IOMMU-Cmdline-Token wie intel_iommu=on sind in den wichtigsten Kernelversionen stabil. Das Backup trägt Pfade, die unter einem neueren Kernel geschrieben wurden, aber diese Pfade (Module Blacklists, GRUB Defaults, Initramfs Config) sind immer noch gültige Syntax auf dem älteren - Kernel ignorieren Token, die sie nicht erkennen, anstatt zu scheitern. Dieser Fall entspricht in der Praxis einer Wiederherstellung des gleichen Kerns, und ProxMenux behandelt ihn als solchen." + }, + "safeSubsetFilter": { + "heading": "Der Safe-Subset-Filter (nur wenn der Zielkernel neuer ist)", + "intro": "Wenn der Zielkernel neuer ist als das Backup, fügt die Kompatibilitätsprüfung 16 bootkritische Pfade von hb_unsafe_paths_cross_version zu RS_SKIP_PATHS hinzu. Diese Pfade sind von der Wiederherstellung ausgeschlossen, da das Schreiben einer Version von ihnen aus einem älteren Kernel über ein Ziel mit einem neueren Kernelpanik in Bench-Tests verursacht hat. Die Pfade umfassen fünf Kategorien:", + "categoryRows": [ + { + "category": "Bootloader", + "paths": "/etc/default/grub, /etc/kernel", + "reason": "GRUB-Standards, die an die vorherige Kernel-Order gebunden sind, und proxmox-boot-tool-Status (cmdline, ESP-UUIDs, Hooks), der auf Pfade und Identifikatoren aus der älteren Installation verweist." + }, + { + "category": "Kernelmodule und Bootartefakte", + "paths": "/etc/modules-load.d, /etc/modprobe.d, /etc/initramfs-tools", + "reason": "Autoload-Listen, die möglicherweise auf Module verweisen, die zwischen Kernel-Majors umbenannt wurden, Moduloptionen, die möglicherweise nicht gelten, Initramfs-Hooks, die für den älteren Kernel geschrieben wurden." + }, + { + "category": "Storage Stack und Filesystem Identität", + "paths": "/etc/fstab, /etc/multipath, /etc/iscsi, /etc/udev/rules.d, /etc/zfs", + "reason": "UUIDs, die in dieser Installation möglicherweise nicht vorhanden sind, Multipath-Treiber, die zwischen Kerneln wechseln, iSCSI-Parameter, die sich entwickeln, udev-Regeln, die an nicht vorhandene Subsysteme binden können, ZFS-Zustand (zpool.cache + hostid), der den Pool als fremd sperren kann." + }, + { + "category": "APT-Quellen", + "paths": "/etc/apt", + "reason": "APT-Source-Suiten können beim nächsten Upgrade ein Downgrade kritischer Pakete auslösen." + }, + { + "category": "systemd", + "paths": "/etc/systemd/system, /etc/systemd/journald.conf, /etc/systemd/logind.conf, /etc/systemd/system.conf, /etc/systemd/user.conf", + "reason": "Unit overrides und .wants, die an den älteren systemd Major gebunden sind; Konfigurationsschlüssel, die möglicherweise nicht auf einem neueren systemd analysiert werden." + } + ], + "outroBody": "Der Filter läuft VOR dem Bestätigungsdialog, so dass der Benutzer die genaue Liste der Pfade sieht, die nach dem Grund kategorisiert übersprungen werden. Die Wiederherstellung geht weiter - alles andere (VM-/LXC-Konfiguration, Netzwerk, /etc/pve, Benutzer, cron, ProxMenux-Zustand, Pakete, Treiber, /root) wird normal wiederhergestellt." + }, + "hydration": { + "heading": "Kern-agnostische Hydratation", + "intro": "Der Filter überspringt bootkritische Dateien; die Ergänzung übernimmt ausgewählte Benutzereinstellungen wie IOMMU-Parameter, VFIO-IDs, eigenes GRUB_TIMEOUT und nvidia-Sperrlisten. Sie läuft bei neuerem Zielkernel. Cmdline-Schlüssel des Ziels bleiben erhalten und fehlende Module werden ergänzt. Freigegebene GRUB-Werte und Benutzerdateien können abweichende Zielinhalte ersetzen. Gleiche Eingaben sind bei Wiederholung idempotent.", + "phaseRows": [ + { + "phase": "1a — GRUB-Trasse", + "detail": "Für Hosts mit GRUB (ext4/lvm-Installationen). _rs_hyd_grub führt jedes Token aus dem manifest.kernel_params.cmdline_extra des Backups in das GRUB_CMDLINE_LINUX_DEFAULT des Ziels ein und überspringt Token, deren Schlüssel das Ziel bereits trägt. Dann werden die GRUB_*-Schlüssel auf der weißen Liste (GRUB_TIMEOUT, GRUB_TIMEOUT_STYLE, GRUB_DEFAULT, GRUB_TERMINAL, GRUB_DISABLE_OS_PROBER, GRUB_SERIAL_COMMAND, GRUB_GFXMODE, GRUB_GFXPAYLOAD_LINUX) aus dem /etc/default/grub des Backups zusammengeführt, wenn sie sich von denen des Ziels unterscheiden." + }, + { + "phase": "1b — systemd-boot / ZFS", + "detail": "Für Hosts mit systemd-Boot (normalerweise ZFS-on-root). _rs_hyd_kernel_cmdline verschmilzt Benutzer-Token von cmdline_extra in /etc/kernel/cmdline des Ziels, wobei die eigene root=, boot= und rootflags= Boilerplate des Ziels intakt bleibt." + }, + { + "phase": "2 — /etc/modules-Merge", + "detail": "_rs_hyd_modules fügt Module aus manifest.kernel_params.modules_loaded_at_boot hinzu, die sich in der Whitelist befinden (vfio, vfio_pci, vfio_iommu_type1, vfio_virqfd, kvm, kvm_intel, kvm_amd, nvidia, nvidia_drm, nvidia_modeset, nvidia_uvm, i915, xe) UND nicht bereits im /etc/modules des Ziels vorhanden sind." + }, + { + "phase": "3 — Kopie von Dateien auf der weißen Liste", + "detail": "_rs_hyd_files kopiert vom Benutzer verfasste Dateien von den Staging-Rootfs zum Live-Ziel, wenn sich der Inhalt unterscheidet. Whitelist umfasst VFIO/nvidia / Blacklist-Dateien unter /etc/modprobe.d, /etc/modules-load.d und die ProxMenux VFIO Binderegel + nvidia udev Regeln unter /etc/udev/rules.d. Distro-eigene Dateien (pve-blacklist.conf, mdadm.conf, nvme.conf) werden absichtlich ausgeschlossen - ihre Inhalte entwickeln sich zwischen den Releases." + }, + { + "phase": "4 — Neubildung nach dem Booten erzwingen", + "detail": "Die vier Phasen schreiben direkt an das Live-Ziel außerhalb der normalen Wiederherstellungspipeline. Damit die zusammengeführten Token/Module/Dateien beim nächsten Booten wirksam werden, breitet sich HB_HYDRATION_APPLIED=1 durch plan.env bis apply_pending_restore.sh aus, was NEEDS_INITRAMFS=1 und NEEDS_GRUB=1 unabhängig davon erzwingt, was in der Anwendungsliste war. Der Post-Boot-Dispatcher regeneriert dann Initramfs und aktualisiert den Bootloader." + } + ] + }, + "planCommit": { + "heading": "Plan vs Commit - der Benutzer sieht zuerst eine Vorschau", + "body": "Die Hydratation läuft in zwei Modi. Vor dem Bestätigungsdialog führt ProxMenux _rs_apply_bk_older_hydration im plan-Modus aus: Es berechnet genau, was zusammengeführt werden würde, füllt RS_HYDRATION_SUMMARY mit einem grünen Block, der jede Aktion auflistet, und gibt ohne zu schreiben zurück. Der Bestätigungsdialog zeigt den grünen Block neben der gelben Liste der durch den Sicherheitsfilter übersprungenen Pfade an, sodass der Benutzer VORAB sieht, was automatisch erneut angewendet wird. Nachdem der Benutzer bestätigt hat, führt ProxMenux den gleichen Helfer im commit-Modus erneut aus - gleiche Phasen, gleiche Logik, aber dieses Mal schreibt jede Phase an das Live-Ziel. Das Abbrechen des Bestätigungsdialogs lässt das Ziel unberührt." + }, + "flowDiagram": { + "heading": "Der Flow, wenn der Zielkernel neuer ist als das Backup", + "intro": "Die Schritte, die hinzugefügt werden, wenn der Zielkernel neuer ist, befinden sich innerhalb des normalen Wiederherstellungsflusses. Alles andere - heißes Aufbringen, ausstehende Vorbereitung, Paketinstallation, Post-Boot-Dispatcher - läuft identisch mit einer Wiederherstellung mit dem gleichen Kern.", + "diagram": " ┌────────────────────────────────────────────────────────────┐\n │ hb_compat_check │\n │ HB_COMPAT_KERNEL_DIRECTION = bk_older │\n └───────────────────────────┬────────────────────────────────┘\n │\n ▼\n ┌────────────────────────────────────────────────────────────┐\n │ Add 16 boot-critical paths to RS_SKIP_PATHS │\n │ /etc/default/grub, /etc/kernel, /etc/modules-load.d, ... │\n └───────────────────────────┬────────────────────────────────┘\n │\n ▼\n ┌────────────────────────────────────────────────────────────┐\n │ _rs_apply_bk_older_hydration \"plan\" │\n │ Compute what would be merged │\n │ Populate RS_HYDRATION_SUMMARY (green block) │\n │ No writes │\n └───────────────────────────┬────────────────────────────────┘\n │\n ▼\n ┌────────────────────────────────────────────────────────────┐\n │ Confirmation dialog │\n │ Amber: paths skipped by safe-subset filter │\n │ Green: tokens/files reapplied by hydration │\n │ User accepts or cancels │\n └───────────────────────────┬────────────────────────────────┘\n │ (accepted)\n ▼\n ┌────────────────────────────────────────────────────────────┐\n │ _rs_apply_bk_older_hydration \"commit\" │\n │ Phase 1a/1b: merge cmdline tokens + GRUB keys │\n │ Phase 2: append modules to /etc/modules │\n │ Phase 3: copy whitelisted vfio/nvidia files │\n │ Set HB_HYDRATION_APPLIED=1 │\n └───────────────────────────┬────────────────────────────────┘\n │\n ▼\n ┌────────────────────────────────────────────────────────────┐\n │ Rest of the restore runs normally │\n │ _rs_apply hot (skips RS_SKIP_PATHS) │\n │ _rs_prepare_pending_restore (writes plan.env with │\n │ HB_HYDRATION_APPLIED=1) │\n │ packages.manual.list install │\n │ Reboot │\n │ apply_pending_restore.sh (forces NEEDS_INITRAMFS=1, │\n │ NEEDS_GRUB=1 because of the hydration flag) │\n │ apply_cluster_postboot.sh │\n │ update-initramfs -u -k all │\n │ update-grub / proxmox-boot-tool refresh │\n │ component --auto-reinstall │\n └────────────────────────────────────────────────────────────┘" + }, + "concreteExamples": { + "heading": "Konkrete Beispiele", + "intro": "Der Hydratationspass ist nicht abstrakt - er erzeugt beobachtbare, korrekte Ergebnisse für gängige Szenarien. Zwei Beispiele, die Hydratation automatisch handhaben:", + "rows": [ + { + "scenario": "GPU-Durchgang (VFIO)", + "detail": "Quelle hatte intel_iommu=on iommu=pt in der cmdline, vfio/vfio_pci/vfio_iommu_type1 in /etc/modules, ein /etc/modprobe.d/vfio.conf mit options vfio-pci ids=10de:2216 und ein /etc/modprobe.d/blacklist-nvidia.conf. Hydration führt die Cmdline-Token in die GRUB- oder Kernel-Cmdline des Ziels ein, fügt die vfio-Module an /etc/modules an und kopiert die beiden vom Benutzer verfassten Modprobe-Dateien. Beim nächsten Booten ist IOMMU aktiv, VFIO-Module laden, der GPU ist an vfio-pci gebunden und der VM hat Passthrough-Funktionen." + }, + { + "scenario": "Custom GRUB-Standards", + "detail": "Quelle hatte GRUB_TIMEOUT=1 und GRUB_DISABLE_OS_PROBER=true. Hydration liest beide Schlüssel aus dem /etc/default/grub des Backups und sieht, dass sie sich von den Standardeinstellungen der Neuinstallation unterscheiden, und schreibt diese beiden Zeilen in der Zieldatei um (alles andere, einschließlich GRUB_DISTRIBUTOR, bleibt unberührt)." + } + ] + }, + "callout": { + "warningTitle": "Was bei neuerem Zielkernel nicht erneut angewendet wird", + "warningBody": "Hydration wendet nur die Konfiguration ProxMenux an, von der bekannt ist, dass sie in Kernelversionen sicher ist: IOMMU-Token, VFIO-IDs, GRUB-Schlüssel auf der weißen Liste und Module aus einer festen Liste (vfio*, nvidia*, i915, xe, kvm*). Alles außerhalb dieses Sets - benutzerdefinierte Initramfs-Hooks unter /etc/initramfs-tools/hooks/, nicht auf der weißen Liste stehende Dateien unter /etc/modprobe.d/, vom Benutzer verfasste systemd-Unit-Overrides - bleibt ausgeschlossen. Der Grund dafür ist konkret: Diese Dateien können Kernel-interne Schnittstellen (Modul-APIs, /sys-Layout, udev-Hooks) aufrufen, die zwischen den Hauptversionen wechseln, und ihre wörtliche Anwendung auf den neueren Kernel kann das Booten des Ziels verhindern. Wenn eine exakte Reproduktion der Bootkette erforderlich ist, muss die Wiederherstellung auf einem Host mit der gleichen Hauptkernelversion wie das Backup ausgeführt werden." + }, + "codeReference": { + "heading": "Wo die Mechanismen leben", + "intro": "Für Entwickler, die das Cross-Kernel-Verhalten verfolgen oder erweitern möchten:", + "rows": [ + { + "component": "Richtungserkennung", + "location": "hb_compat_check in lib_host_backup_common.sh. Setzt HB_COMPAT_KERNEL_DIRECTION." + }, + { + "component": "Liste sicherer Teilstrecken", + "location": "hb_unsafe_paths_cross_version in lib_host_backup_common.sh. Sendet path\\treason-Linien aus, die sowohl vom CLI-Filter als auch vom Web-Endpunkt /api/host-backups/restore/prepare verwendet werden." + }, + { + "component": "Hydratationsphasen", + "location": "_rs_hyd_grub, _rs_hyd_kernel_cmdline, _rs_hyd_modules, _rs_hyd_files in backup_host.sh. Orchestriert von _rs_apply_bk_older_hydration." + }, + { + "component": "Neubildung nach dem Booten erzwingen", + "location": "apply_pending_restore.sh liest HB_HYDRATION_APPLIED von plan.env und zwingt NEEDS_INITRAMFS=1/NEEDS_GRUB=1." + }, + { + "component": "Web Preview", + "location": "/api/host-backups/restore/prepare in flask_server.py. Lädt die Hilfsbibliothek, führt _rs_apply_bk_older_hydration im Planmodus aus und gibt die Aktionen an den Webmodal zurück." + } + ] + }, + "whereNext": { + "heading": "Wohin Sie als nächstes gehen", + "items": [ + { + "label": "Wiederherstellung", + "href": "/docs/backup-restore/restoring", + "tail": "— die vollständige Wiederherstellungspipeline der Mechanismen dieser Seite." + }, + { + "label": "Wie es funktioniert", + "href": "/docs/backup-restore/how-it-works", + "tail": " — Manifest und Erfassungsroutinen, die den von der Ergänzung gelesenen Block kernel_params erzeugen." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/destinations/borg.json b/web/messages/de/docs/backup-restore/destinations/borg.json new file mode 100644 index 00000000..1f4fea6f --- /dev/null +++ b/web/messages/de/docs/backup-restore/destinations/borg.json @@ -0,0 +1,234 @@ +{ + "meta": { + "title": "Borg-Ziel — Repository-Typen, SSH-Authentifizierung, Verschlüsselung | ProxMenux", + "description": "Das Borg-Ziel speichert ProxMenux-Host-Backups in einem Borg-Repository: lokal, auf einem eingehängten externen Datenträger oder auf einem per SSH erreichbaren Server. Beschrieben werden die Suche nach der ausführbaren borg-Datei, die vier anfänglichen SSH-Schlüsseloptionen, repokey-Verschlüsselung, gespeicherte Ziele und Aufbewahrungsregeln je geplantem Auftrag.", + "ogTitle": "ProxMenux Backup — Zielort Borg", + "ogDescription": "So speichert ProxMenux Host-Backups in Borg-Repositories mit SSH-Authentifizierung und repokey-Verschlüsselung.", + "twitterTitle": "Borg Backup Ziel | ProxMenux", + "twitterDescription": "Lokale, USB- und SSH-gestützte Borg-Repositories für Host-Backups." + }, + "header": { + "title": "Borg", + "description": "Das Borg-Ziel schreibt Host-Backups in ein Borg-Repository. Es werden drei Repository-Typen unterstützt: lokaler Dateisystempfad, eingehängte externe Festplatte oder Remote-Server über SSH. Chunk-Level-Deduplizierung über alle Archive im Repository und optionale Repokey-Verschlüsselung.", + "section": "Sicherung und Wiederherstellung" + }, + "aboutBorg": { + "heading": "Was Borg ist", + "body": "Borg (auch BorgBackup geschrieben) ist ein Open-Source-Deduplizierungs-Backup-Tool, das von der Borg Backup-Community verwaltet wird. Es speichert jedes Archiv als eine Reihe von Blöcken variabler Länge in einem Repository; Blöcke werden über Archive hinweg geteilt, so dass das erneute Sichern unveränderter Daten nahezu Null Speicherkosten verursacht. Borg ist nicht an Proxmox gebunden - es ist ein universelles Backup-Tool, das in vielen Umgebungen verwendet wird. ProxMenux verwendet es als eines der drei Ziele für Host-Backups; jeder auf dieser Seite beschriebene Borg-spezifische Mechanismus (Repository-Typen, borg-serve über SSH, Repokey-Verschlüsselung, prune) ist Standard-Borg-Verhalten." + }, + "intro": { + "title": "Chunk-basierte Deduplizierung mit lokalen oder SSH-servierten Repositorys", + "body": "Ein Borg-Repository speichert Brocken, die in jedem enthaltenen Archiv geteilt werden, so dass das erneute Sichern einer unveränderten Datei nichts Neues überträgt und speichert. ProxMenux ruft borg create zum Backup-Zeitpunkt und borg extract zum Wiederherstellungszeitpunkt gegen ein Repository auf. Das Repository kann auf demselben Dateisystem wie die Quelle, auf einer montierten externen Festplatte oder auf einem Remote-Host, auf den über SSH zugegriffen wird, leben." + }, + "binarySourcing": { + "heading": "Die ausführbare borg-Datei", + "intro": "Borg ist keine Basisabhängigkeit des ProxMenux-Installationsprogramms. Wenn ein Backup ausgeführt wird, löst hb_ensure_borg die Binärdatei in der Reihenfolge aus vier Quellen auf und gibt die erste zurück, die funktioniert:", + "rows": [ + { + "priority": "1", + "source": "Systemweites borg", + "detail": "Wenn der Host bereits borg über APT installiert hat (which borg löst auf), wird diese Binärdatei verwendet." + }, + { + "priority": "2", + "source": "State-Dir-Cache", + "detail": "/usr/local/share/proxmenux/borg — aus einem früheren GitHub-Download auf diesem Host aufbewahrt." + }, + { + "priority": "3", + "source": "AppImage-Paket des Monitors", + "detail": "/usr/local/share/proxmenux/monitor-app/usr/bin/borg — der AppImage bündelt einen signierten borg-linux64. Dies ist der offline-sichere Pfad: Ein Host ohne Internet hat immer noch einen funktionierenden borg." + }, + { + "priority": "4", + "source": "GitHub Download", + "detail": "wget gegen die angepinnte borg-linux64-URL unter github.com/borgbackup/borg/releases/, verifiziert gegen eine Konstante SHA-256 in lib_host_backup_common.sh (HB_BORG_LINUX64_SHA256). Bei Checksummenfehlanpassung wird die Binärdatei verworfen und das Backup abgebrochen. Die heruntergeladene Datei wird im State Dir zwischengespeichert, so dass nachfolgende Backups den Pfad 2 verwenden." + } + ] + }, + "repoTypes": { + "heading": "Repository-Typen", + "intro": "Der Repository-Typ wird ausgewählt, wenn ein Ziel hinzugefügt wird. Jeder Typ löst sich in einer anderen Repository-URL auf, die Borg versteht.", + "rows": [ + { + "type": "remote", + "url": "ssh://USER@HOST/RPATH", + "detail": "Repository lebt auf einem Remote-Server, auf dem borg serve ausgeführt wird. Erfordert eine SSH-Verbindung zu diesem Server." + }, + { + "type": "usb", + "url": "/mnt/MOUNTPOINT/borgbackup", + "detail": "Repository lebt auf einer montierten externen Festplatte (normalerweise USB). Die Mount wird über hb_prompt_mounted_path aufgelöst, das USB-Partitionen nach Bedarf erkennt, einhängt oder formatiert." + }, + { + "type": "local", + "url": "/backup/borgbackup (any absolute path)", + "detail": "Repository lebt in einem lokalen Verzeichnis. Bedeutsam nur, wenn sich das Zielverzeichnis auf einer separaten physischen Festplatte befindet – ein Repository auf derselben Festplatte wie die Quelle schützt vor Operatorfehlern, aber nicht vor Festplattenausfällen." + } + ] + }, + "serverSetup": { + "heading": "Vorbereitung des Borg-Servers (Serverseite)", + "intro": "Der Repository-Typ remote setzt einen funktionierenden, per SSH erreichbaren Borg-Server voraus. ProxMenux richtet den Server nicht selbst ein; es autorisiert nur einen Schlüssel für ein vorhandenes Konto. Dieser Abschnitt beschreibt, was der Server vor der Verbindung benötigt.", + "hostChoicesTitle": "Wo der Server leben kann", + "hostChoicesBody": "Jeder Linux-Host mit SSH-Zugang ist qualifiziert. Gemeinsame Setups:", + "hostChoicesItems": [ + "Eine dedizierte NAS- oder Backup-Box (Debian, Ubuntu, TrueNAS SCALE mit einer Shell).", + "Ein LXC Container in einem Proxmox Knoten. Kleiner Footprint, isoliert vom Host, der die Backups ausführt.", + "Ein anderer Proxmox-Host im selben LAN oder ein VM, der über SSH erreichbar ist." + ], + "lxcWarningTitle": "LXC als Borg-Server", + "lxcWarningBody": "Läuft der Borg-Server in einem LXC, benötigt generate-auto zur Schlüsselinstallation ein Konto, das SSH-Passwortanmeldung erlaubt. Ist diese nicht verfügbar, verwenden Sie die manuelle Installation oder generate-pct über den PVE-Host.", + "requirementsTitle": "Anforderungen an den Server", + "requirementsRows": [ + { + "requirement": "Ausführbare borg-Datei unter /usr/bin/borg", + "detail": "Die Zeile command=\"/usr/bin/borg serve ...\", die ProxMenux in authorized_keys einträgt, verwendet diesen festen Pfad. APT (apt install borgbackup) installiert das Programm dort. Für eine separat installierte Binärdatei ist ein symbolischer Link unter /usr/bin/borg nötig." + }, + { + "requirement": "Ein dediziertes Benutzerkonto (normalerweise borg)", + "detail": "Besitzt das Repository-Verzeichnis und empfängt eingehende SSH-Verbindungen. Benötigt keinen sudo- oder shell-zugriff - die authorized_keys-linie deaktiviert interaktive shells sowieso." + }, + { + "requirement": "Ein beschreibbares Repository Verzeichnis", + "detail": "Der vom Betreiber angegebene Pfad ProxMenux (z. B. /backup/borgbackup) muss auf dem Server vorhanden sein und im Besitz des borg-Benutzers sein." + }, + { + "requirement": "SSH Daemon akzeptiert den Borg-Benutzer", + "detail": "PubkeyAuthentication yes (Standard). Die Passwort-Authentifizierung wird nur für den One-Shot generate-auto-Flow benötigt – nachdem der Schlüssel installiert wurde, kann der Server das Passwort auth vollständig deaktivieren." + } + ], + "minimalSetupTitle": "Minimale Server-Einrichtung", + "minimalSetupBody": "Auf einem Debian- oder Ubuntu-Borg-Server besteht eine Arbeitsbasislinie aus vier Befehlen als root:", + "minimalSetupCmd": "apt install borgbackup\nuseradd -m -d /home/borg -s /bin/bash borg\nmkdir -p /backup/borgbackup\nchown borg:borg /backup/borgbackup", + "minimalSetupNote": "Sind Server und SSH-Passwortanmeldung vorbereitet, installiert generate-auto den Schlüssel über ein Konto mit Schreibzugriff auf die authorized_keys-Datei des borg-Benutzers. Spätere Backups verwenden diesen auf borg serve am konfigurierten Pfad beschränkten Schlüssel. Ohne Passwortanmeldung wird er manuell oder über den PVE-Host installiert." + }, + "sshAuth": { + "heading": "SSH-Authentifizierung (Clientseite)", + "intro": "Remote-Borg-Repositories sind über SSH zugänglich. Die Verbindung läuft vom ProxMenux-Host zu einem Benutzerkonto auf dem Borg-Server, auf dem borg serve ausgeführt wird. Dieser Benutzer heißt normalerweise borg, NICHT der Admin / Root-Benutzer des Servers - die Befehlszeile borg-serve sperrt die Verbindung zu diesem spezifischen Repository-Pfad (siehe die wichtigsten Strategien unten).", + "strategiesTitle": "SSH-Schlüsselstrategien", + "strategiesIntro": "Beim Hinzufügen eines entfernten Ziels fragt ProxMenux nach SSH-Benutzer, Host, Port und entferntem Pfad und bietet vier Anmeldeoptionen. Wird die Passwortanmeldung abgewiesen, steht zusätzlich generate-pct zur Wahl. Die Modi sind unten beschrieben:", + "strategyRows": [ + { + "mode": "generate-auto", + "label": "Empfohlen", + "detail": "ProxMenux erzeugt oder verwendet ein ed25519-Schlüsselpaar unter ~/.ssh/borg_proxmenux_HOST_ed25519. Mit sshpass prüft es die Anmeldung und installiert den öffentlichen Schlüssel in ~borg/.ssh/authorized_keys. Das Passwort gehört zu einem Konto mit Schreibzugriff auf diese Datei, meist root oder borg. Es wird nur bei der Einrichtung verwendet und nicht gespeichert." + }, + { + "mode": "generate-manual", + "label": "Kein Admin-Passwort auf diesem Host", + "detail": "ProxMenux generiert das Keypair wie oben, zeigt aber die vollständige authorized_keys-Zeile an, die der Betreiber manuell in den Server einfügen kann. Das Admin-Passwort verlässt niemals den ProxMenux-Host, weil es nie angefordert wird." + }, + { + "mode": "generate-pct", + "label": "Borg Server ist ein PVE LXC", + "detail": "Der Borg-Server läuft innerhalb eines LXC auf einem PVE-Knoten. ProxMenux autorisiert den Schlüssel über pct exec vom PVE-Host - root auf dem PVE-Host schreibt in den ~borg/.ssh/authorized_keys des LXC, ohne dass SSH selbst in den LXC benötigt wird." + }, + { + "mode": "existing", + "label": "Verwenden Sie einen vorhandenen Schlüssel", + "detail": "ProxMenux scannt /root/.ssh/ und $HOME/.ssh/ nach Parseable ed25519/RSA Private Keys und listet diese auf. Der Bediener wählt einen oder durchsucht manuell einen nicht standardmäßigen Pfad." + }, + { + "mode": "none", + "label": "Standard SSH-Konfiguration", + "detail": "Kein benutzerdefinierter Schlüssel - Borg stützt sich auf die standardmäßige SSH-Konfiguration des Hosts (normalerweise ~/.ssh/id_rsa oder ein SSH-Agent)." + } + ], + "restrictTitle": "Die authorized_keys Linie", + "restrictBody": "Für jeden erzeugten Schlüssel beschränkt die von ProxMenux auf dem Server eingetragene authorized_keys-Zeile den Zugriff auf den folgenden borg-serve-Aufruf für den konfigurierten Repository-Pfad:", + "restrictLine": "command=\"/usr/bin/borg serve --restrict-to-path RPATH\",restrict PUBKEY", + "restrictNote": "Der command= zwingt jede SSH-Sitzung mit diesem Schlüssel, nur den Befehl borg-serve auszuführen; restrict deaktiviert die Portweiterleitung, die Agentenweiterleitung, die X11-Weiterleitung und die PTY-Zuweisung. Der Schlüssel kann nicht verwendet werden, um eine interaktive Shell auf dem Server zu öffnen oder auf einen anderen Repository-Pfad zuzugreifen, selbst wenn das Konto über breitere Privilegien verfügt." + }, + "savedTargets": { + "heading": "Gespeicherte Ziele", + "intro": "Ein gespeichertes Ziel hält die Repository-Konfiguration unter einem verständlichen Namen fest, damit die Angaben nicht erneut eingegeben werden müssen. Dateien im ProxMenux-Zustandsverzeichnis:", + "rows": [ + { + "file": "borg-targets.txt", + "content": "Eine Zeile je Ziel: NAME|REPO|SSH_KEY_PATH|ENCRYPT_MODE. hb_collect_borg_configs liest sie ein, um das Zielauswahlmenü zu füllen." + }, + { + "file": "borg-pass-NAME.txt", + "content": "Passphrase des oben mit NAME benannten Ziels (chmod 600). Nur bei gewählter repokey-Verschlüsselung vorhanden." + } + ], + "outro": "Das Speichern des Ziels ist optional. Bei Ablehnung wird keine benannte Zielkonfiguration gespeichert; ein erzeugter SSH-Schlüssel wird jedoch nicht entfernt. Bei repokey-Verschlüsselung ohne gespeichertes Ziel legt ProxMenux die Passphrase weiterhin in der älteren Datei borg-pass.txt ab." + }, + "encryption": { + "heading": "Verschlüsselung", + "body": "Repositories werden mit einem Verschlüsselungsmodus über hb_borg_init_if_needed initialisiert: das moderne borg repo-create -e MODE, wenn verfügbar, oder das alte borg init --encryption=MODE. ProxMenux zeigt zwei Modi: repokey (Standard) und none. Im repokey-Modus speichert Borg den Verschlüsselungsschlüssel im Repository selbst; der Zugriff erfordert eine Passphrase, die ProxMenux zweimal mit Übereinstimmungsvalidierung anfordert und für gespeicherte Ziele unter borg-pass-NAME.txt speichert, sonst in der oben beschriebenen älteren Datei. Ein obligatorischer Bestätigungsdialog wird angezeigt, nachdem die Passphrase gespeichert wurde: Die Passphrase ist die einzige Möglichkeit, auf die verschlüsselten Archive zuzugreifen, und wenn sie verloren geht, ist jedes Archiv im Repository nicht wiederherstellbar." + }, + "runtimeEnv": { + "heading": "Laufzeitumgebung", + "intro": "ProxMenux exportiert die folgenden Umgebungsvariablen, bevor borg aufgerufen wird. Sie konfigurieren die Verbindung und entsperren das Repository, ohne Geheimnisse in die Kommandozeile einzubetten.", + "rows": [ + { + "var": "BORG_RSH", + "value": "ssh -i SSH_KEY_PATH -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new [-p PORT]", + "purpose": "Remote-Shell-Befehl für Borg-Repositories über SSH. Ein gewählter Schlüssel ergänzt die Identitätsoptionen. Wird ein Ziel erstmals mit einem abweichenden Port hinzugefügt, ergänzt ProxMenux zusätzlich -p PORT; gespeicherte Ziele, geplante Aufträge und der Monitor verwenden stattdessen den Port aus der ssh://-URL. Ohne eigenen Schlüssel und bei einem neuen Ziel mit Standardport bleibt die Variable ungesetzt; Borg nutzt dann die SSH-Vorgaben des Hosts." + }, + { + "var": "BORG_PASSPHRASE", + "value": "(Passphrase von borg-pass-NAME.txt)", + "purpose": "Entsperrt den Repokey. Setzen Sie nur, wenn das Ziel repokey-Verschlüsselung verwendet. Erscheint nie in der Prozessargumentenliste." + }, + { + "var": "BORG_ENCRYPT_MODE", + "value": "repokey | none", + "purpose": "Wird von hb_borg_init_if_needed bei der Initialisierung eines noch nicht vorhandenen Repositorys verwendet. Ignoriert von borg create auf bestehenden Repositories." + }, + { + "var": "BORG_RELOCATED_REPO_ACCESS_IS_OK", + "value": "yes", + "purpose": "Unterdrückt die interaktive Eingabeaufforderung, die Borg aufruft, wenn die Repository-URL von der URL abweicht, von der sie ursprünglich erreicht wurde (üblich nach einem Rename für einen Mount-Point oder SSH-Host, der über eine neue Adresse erreichbar ist)." + }, + { + "var": "BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK", + "value": "yes", + "purpose": "Unterdrückt die interaktive Eingabeaufforderung, die Borg beim ersten Zugriff auf ein unverschlüsseltes Repository von einem neuen Client aus erstellt." + } + ] + }, + "archiveFormat": { + "heading": "Bezeichnung und Aufbewahrung des Archivs", + "intro": "Jedes Backup erstellt ein neues Archiv im Repository.", + "namePattern": "hostcfg-HOSTNAME-YYYYMMDD_HHMMSS", + "retentionBody": "Für geplante Aufträge führt run_scheduled_backup.sh borg prune nach jedem erfolgreichen Backup mit den für den Auftrag konfigurierten Retentionswerten (--keep-last, --keep-daily, --keep-weekly) gegen das Repository aus. Interaktive Backups beschneiden nicht." + }, + "restoreAccess": { + "heading": "Abruf auf der Wiederherstellungsseite", + "body": "Die Wiederherstellung listet Archive mit borg list REPO auf und entpackt das gewählte Archiv mit borg extract REPO::ARCHIVE-NAME in ein Vorbereitungsverzeichnis. _rs_check_layout übergibt dieses an den normalen Wiederherstellungsablauf. Dieselben Befehle funktionieren auch manuell außerhalb von ProxMenux. Der entpackte Verzeichnisbaum entspricht der in So funktioniert es beschriebenen Struktur aus drei Datenblöcken." + }, + "references": { + "heading": "Referenzen", + "intro": "Offizielle Borg-Dokumentation zu den Komponenten, auf die ProxMenux aufbaut.", + "items": [ + { + "label": "Borg-Dokumentation", + "href": "https://borgbackup.readthedocs.io/", + "tail": "— Haupteinstiegspunkt für Konzepte, Bereitstellung, Schnellstart und jeden Befehl." + }, + { + "label": "borg create", + "href": "https://borgbackup.readthedocs.io/en/stable/usage/create.html", + "tail": " — der Befehl, den ProxMenux für jedes Backup aufruft, mit allen unterstützten Optionen." + }, + { + "label": "Datenspeicherverschlüsselung", + "href": "https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-mode", + "tail": "- die Verschlüsselungsmodi, die Borg unterstützt, einschließlich Repokey, den ProxMenux standardmäßig verwendet." + }, + { + "label": "borg serve und Bereitstellung über SSH", + "href": "https://borgbackup.readthedocs.io/en/stable/deployment/central-backup-server.html", + "tail": " - wie man einen zentralen Borg-Server einrichtet, auf den über SSH zugegriffen wird, einschließlich des borg-serve-Befehls und des restrict-to-path-Flags ProxMenux schreibt in authorized_keys." + }, + { + "label": "borg prune", + "href": "https://borgbackup.readthedocs.io/en/stable/usage/prune.html", + "tail": "— das Retentionsmodell hinter --keep-last / --keep-daily / --keep-weekly, das ProxMenux pro geplantem Auftrag anwendet." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/destinations/index.json b/web/messages/de/docs/backup-restore/destinations/index.json new file mode 100644 index 00000000..1654efab --- /dev/null +++ b/web/messages/de/docs/backup-restore/destinations/index.json @@ -0,0 +1,110 @@ +{ + "meta": { + "title": "Backup-Ziele — Lokal, Proxmox Backup Server, Borg | ProxMenux", + "description": "Drei Storage-Backends erhalten die gleiche ProxMenux Backup-Nutzlast: ein lokales .tar.zst-Archiv auf jedem beschreibbaren Dateisystem, ein PBS-Backup in einem Proxmox Backup Server-Datenspeicher oder ein Borg-Archiv in einem lokalen oder entfernten Borg-Repository. Jedes Backend hat seine eigenen Kompressions-, Deduplizierungs-, Verschlüsselungs- und Netzwerkeigenschaften.", + "ogTitle": "ProxMenux Backup-Ziele", + "ogDescription": "Local, Proxmox Backup Server und Borg – drei Storage-Backends für die gleiche Host-Backup-Nutzlast.", + "twitterTitle": "ProxMenux Backup Ziele | ProxMenux", + "twitterDescription": "Lokales Archiv, Proxmox Backup Server und Borg Backends für Host-Backups." + }, + "header": { + "title": "Ziele", + "description": "Drei Storage Backends, die von ProxMenux Backup unterstützt werden: lokales Archiv, Proxmox Backup Server (empfohlen) und Borg. Jeder erhält das gleiche Drei-Nutzlast-Archiv; sie unterscheiden sich in Speicherformat, Deduplizierung, Verschlüsselungsmodell und Netzwerkanforderungen.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Gleiche Nutzlast, drei Backends", + "body": "Jedes Backup erzeugt die gleichen drei Nutzlasten (Rootfs, Manifest, Anwendungen). Was sich zwischen Zielen ändert, ist, wie diese Nutzlasten gespeichert werden und wie der Betreiber sie für eine Wiederherstellung abruft. Jedes Backend wird als separate Funktion in backup_host.sh - _bk_local, _bk_pbs, _bk_borg - implementiert, aber alle drei haben den gleichen Staging-Schritt (hb_prepare_staging) und den gleichen Wiederherstellungscodepfad. Die Wahl des Ziels betrifft nur das Schreiben und den Abruf; es ändert nicht, was eine Wiederherstellung tut oder wie sie das Archiv verbraucht." + }, + "comparison": { + "heading": "Merkmalsvergleich", + "intro": "Die folgende Tabelle listet die konkreten Unterschiede zwischen den drei Backends auf, wie sie sich heute in ProxMenux verhalten. Alle Werte beschreiben das beobachtete Verhalten des Backends selbst, nicht redaktionelle Empfehlungen.", + "rows": [ + { + "feature": "Speicherformat", + "local": "Einzelne .tar.zst-Datei (oder .tar.gz, wenn zstd fehlt).", + "pbs": "PBS Backup (.pxar Chunks im Datenspeicher).", + "borg": "Borg-Archiv in einem Borg-Repository (Segmentdateien)." + }, + { + "feature": "Kompression", + "local": "zstd (Standardstufe) über tar --zstd. gzip Fallback.", + "pbs": "Von proxmox-backup-client verwaltet. ProxMenux überschreibt die Kompressionseinstellungen nicht.", + "borg": "In Borg integrierte Kompression. ProxMenux ruft borg create auf, ohne die Kompressionseinstellungen zu überschreiben." + }, + { + "feature": "Deduplizierung", + "local": "Keine. Jedes Backup ist ein vollständiges unabhängiges Archiv.", + "pbs": "Vollständige Chunk-Level-Deduplizierung über alle Backups im Datenspeicher.", + "borg": "Vollständige Chunk-Level-Deduplizierung über alle Archive im Repository." + }, + { + "feature": "Verschlüsselung im Ruhezustand", + "local": "Keine (Verlässt sich auf den Schutz auf Dateisystemebene).", + "pbs": "Optionale clientseitige Keyfile-Verschlüsselung. Recovery-Blob als separate Backup-Gruppe hochgeladen, wenn aktiviert.", + "borg": "Optionale Repokey-Verschlüsselung (Schlüssel im Repo gespeichert, durch eine Passphrase freigeschaltet)." + }, + { + "feature": "Aufbewahrung / Bereinigung", + "local": "Wird pro geplantem Auftrag über KEEP_LAST angewendet. Alte Archive werden zusammen mit ihren Begleitdateien und Ausführungsprotokollen gelöscht. Interaktive Backups führen keine Bereinigung aus.", + "pbs": "Angewendet pro geplanten Auftrag über proxmox-backup-client prune --keep-last / --keep-daily / --keep-weekly. Interaktive Backups führen keine Bereinigung aus.", + "borg": "Angewendet pro geplanten Auftrag über borg prune --keep-last / --keep-daily / --keep-weekly. Interaktive Backups führen keine Bereinigung aus." + }, + { + "feature": "Netzwerk", + "local": "Keine. Schreibt auf einen lokalen Einhängepunkt (normalerweise eine interne Festplatte oder ein USB-Laufwerk).", + "pbs": "TCP auf den PBS-Server (Standardport 8007). Erfordert PBS Anmeldeinformationen + Fingerabdruck.", + "borg": "Lokaler Dateisystempfad oder SSH Tunnel zu einem entfernten Borg-Host." + }, + { + "feature": "Abhängigkeiten", + "local": "tar, zstd (standardmäßig auf Proxmox vorhanden).", + "pbs": "proxmox-backup-client Paket (gebündelt mit Proxmox VE 8+).", + "borg": "borg binär. Automatische Bereitstellung durch ProxMenux aus dem Monitor AppImage-Bundle, wenn es fehlt." + }, + { + "feature": "Zugriff für die Wiederherstellung", + "local": "Jeder Host mit tar+zstd kann das Archiv extrahieren. Keine PBS/Borg erforderlich.", + "pbs": "Erfordert proxmox-backup-client + PBS Anmeldeinformationen + die Schlüsseldatei (falls verschlüsselt).", + "borg": "Erfordert borg + den Repository-Pfad + die Passphrase (falls verschlüsselt)." + } + ], + "captionCode": "feature", + "captionLocal": "Lokal", + "captionPbs": "Proxmox Backup Server (empfohlen)", + "captionBorg": "Borg" + }, + "sameArchive": { + "heading": "Das Archivlayout ändert sich nicht mit dem Ziel", + "body": "Innerhalb eines der drei Ziele ist das gleiche rootfs/ + metadata/ + manifest.json Layout vorhanden, das in Funktionsweise beschrieben wird. Ein aus einem lokalen Archiv extrahierter .tar.zst, ein aus PBS wiederhergestellter .pxar und ein mit borg extract extrahiertes Borg-Archiv ergeben alle einen identischen Verzeichnisbaum. Der Wiederherstellungscodepfad (_rs_check_layout, _rs_apply, _rs_prepare_pending_restore) liest die gleichen drei Nutzlasten, ohne zu wissen, von welchem Ziel sie kamen." + }, + "extractStandalone": { + "heading": "Extrahieren eines Backups außerhalb von ProxMenux", + "intro": "Jedes der drei Archivformate kann mit Standardtools gelesen werden, ohne dass ProxMenux auf dem Lesehost installiert ist. Die folgenden Befehle erzeugen den gleichen Verzeichnisbaum, den eine Wiederherstellung intern verbraucht.", + "localCmd": "# From a local .tar.zst archive:\ntar --zstd -xf hostcfg-HOST-TIMESTAMP.tar.zst\n\n# From a .tar.gz fallback:\ntar -xzf hostcfg-HOST-TIMESTAMP.tar.gz", + "pbsCmd": "# From a PBS backup (requires proxmox-backup-client):\nproxmox-backup-client restore \\\n --repository USER@REALM@HOST:DATASTORE \\\n host/hostcfg-HOST/BACKUP-TIME \\\n hostcfg.pxar /tmp/hostcfg\n\n# Add --keyfile KEY-PATH when the backup is encrypted.", + "borgCmd": "# Aus einem Borg-Archiv (benötigt borg und bei verschlüsselten Repositories die Passphrase):\nborg extract REPO-PATH::ARCHIVE-NAME\n\n# Bei einem über SSH erreichbaren entfernten Repository:\nborg extract ssh://USER@HOST:PORT/REPO-PATH::ARCHIVE-NAME", + "note": "Der extrahierte Baum kann dann manuell inspiziert oder einer manuellen Wiederherstellung zugeführt werden. Sehen Sie sich die einzelnen Zielseiten für den genauen Abruffluss an, den ProxMenux unter der Haube verwendet." + }, + "whereNext": { + "heading": "Details je Ziel", + "intro": "Jedes Ziel hat eine eigene Seite, die den Konfigurationsfluss, das On-Disk-Format und den von der Wiederherstellung verwendeten Abrufbefehl abdeckt.", + "items": [ + { + "label": "Lokales Archiv", + "href": "/docs/backup-restore/destinations/local", + "tail": " — Vorkonfiguration eines lokalen Ziels, Einhängen von USB-Laufwerken, Schutz vor dem Einschließen des Archivs in sich selbst und die JSON-Begleitdatei zur Erkennung im Monitor." + }, + { + "label": "Proxmox Backup Server (empfohlen)", + "href": "/docs/backup-restore/destinations/pbs", + "tail": "Datenspeicherauswahl, Anmeldeinformationen und Fingerabdruck, Verschlüsselungs-Keyfile-Lebenszyklus, Recovery-Passphrase, Hochladen des Recovery-Blobs zu PBS für die Wiederherstellung mit frischer Installation." + }, + { + "label": "Borg", + "href": "/docs/backup-restore/destinations/borg", + "tail": " — lokale und über SSH erreichbare Repositories, Bezugsquellen der Borg-Binärdatei (System / Cache / Monitor-AppImage-Bundle / GitHub-Download), Repository-Initialisierung, Umgang mit Passphrasen und SSH-Schlüssel für entfernte Repositories." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/destinations/local.json b/web/messages/de/docs/backup-restore/destinations/local.json new file mode 100644 index 00000000..eff0a73d --- /dev/null +++ b/web/messages/de/docs/backup-restore/destinations/local.json @@ -0,0 +1,75 @@ +{ + "meta": { + "title": "Lokales Archivziel — tar.zst auf einem Dateisystem oder USB-Laufwerk | ProxMenux", + "description": "Das lokale Sicherungsziel schreibt ein einzelnes .tar.zst-Archiv in ein beliebiges beschreibbares Verzeichnis: auf eine interne Festplatte, einen Proxmox-Mountpoint, eine NFS-Freigabe oder ein USB-Laufwerk. Diese Seite beschreibt die Einrichtung, das Erkennen und Einbinden von USB-Laufwerken, den Schutz vor dem Speichern des Archivs in einem selbst gesicherten Pfad und die zugehörige JSON-Datei zur Identifizierung der Sicherung.", + "ogTitle": "ProxMenux Backup — Ziel des lokalen Archivs", + "ogDescription": "Wie ProxMenux lokale Backups als .tar.zst-Archive schreibt und wie man das Zielverzeichnis oder das USB-Laufwerk konfiguriert.", + "twitterTitle": "Lokales Backup-Ziel | ProxMenux", + "twitterDescription": "Wie ProxMenux lokale Backups als .tar.zst-Archive auf ein Dateisystem oder USB-Laufwerk schreibt." + }, + "header": { + "title": "Lokales Archiv", + "description": "Das lokale Sicherungsziel schreibt ein einzelnes komprimiertes tar-Archiv in ein beliebiges beschreibbares Verzeichnis auf dem Host: auf eine interne Festplatte, einen NFS- oder SMB-Mount oder ein USB-Laufwerk.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Eine Datei, in sich geschlossen", + "body": "Ein lokales Backup erzeugt eine einzelne hostcfg-HOST-TIMESTAMP.tar.zst-Datei (oder .tar.gz, wenn zstd fehlt). Die Datei enthält den gesamten Archivbaum manifest.json, metadata/ und rootfs/ und kann auf jedem Proxmox-Host mit dem ProxMenux-Wiederherstellungsfluss wiederhergestellt oder manuell mit tar --zstd -xf auf jedem Linux-System extrahiert werden. Kein Server, kein Repository init, keine externe Abhängigkeit. Dies ist das Ziel mit dem kürzesten Wiederherstellungspfad, wenn weder PBS noch Borg verfügbar sind." + }, + "targetConfig": { + "heading": "Konfiguration des Zielverzeichnisses", + "intro": "Das lokale Ziel ist ein einziges dauerhaft gespeichertes Zielverzeichnis – keine Liste. ProxMenux speichert die Auswahl des Betreibers bei /usr/local/share/proxmenux/local-target.conf und liest sie bei jedem Backup. Wenn kein Ziel konfiguriert wurde, wird der Standard-HB_LOCAL_TARGET_DEFAULT = /var/lib/vz/dump verwendet (das gleiche Verzeichnis, das Proxmox für seine eigenen vzdump-Ausgänge verwendet). Das Ziel wird unter Backup-Ziele konfigurieren → Lokale Ziele konfiguriert:", + "options": [ + "Standard verwenden (/var/lib/vz/dump). Der lokale Proxmox-Speicher. Bei jeder Proxmox-Installation vorhanden; das Archiv befindet sich neben den vzdump-Ausgaben und wird automatisch von der Registerkarte Backups des Monitors aufgenommen.", + "Benutzerdefinierten Pfad eingeben. Jeder absolute Dateisystempfad kann verwendet werden: ein NFS-Mount, eine über fstab eingebundene SMB-Freigabe, ein eigenes ZFS-Dataset oder eine zweite interne Festplatte. ProxMenux prüft vor dem Speichern, ob der Pfad existiert und ein Verzeichnis ist.", + "Wählen Sie ein USB-Laufwerk. Öffnet das USB-Untermenü (unten), das Wechselgeräte erkennt und ein Gerät montieren oder formatieren kann." + ] + }, + "usbFlow": { + "heading": "USB-Laufwerkerkennung und -montage", + "intro": "Das USB-Untermenü listet Partitionen auf Wechselgeräten auf, die von lsblk gemeldet werden. Jede Partition wird mit ihrer Größe, Dateisystem-Beschriftung und aktuellem Zustand dargestellt. Der Zustand bestimmt die Aktion, die ProxMenux anbietet.", + "statesTitle": "Die drei Gerätezustände", + "stateRows": [ + { + "state": "eingehängt", + "shown": "Größe · Etikett · [fstype] · → /mount/point", + "action": "Die Partition ist bereits irgendwo montiert. Bei der Auswahl wird der aktuelle Einhängepunkt als lokales Ziel gespeichert. Es wird kein neuer Mount ausgeführt." + }, + { + "state": "nicht montiert", + "shown": "Größe · Etikett · [fstype] · (nicht montiert — wird montiert)", + "action": "Ein Dateisystem ist vorhanden, aber nicht montiert. Bei Bestätigung führt ProxMenux hb_mount_usb_partition aus: erstellt /mnt/backup-LABEL (oder einen UUID-basierten Pfad, wenn kein Label vorhanden ist), mountet die Partition und speichert den Einhängepunkt als lokales Ziel." + }, + { + "state": "leer", + "shown": "Größe · unformatierter USB-Datenträger — kein Dateisystem (wird FORMATIERT)", + "action": "Das Gerät hat kein Dateisystem. Dieser destruktive Vorgang ist durch zwei Bestätigungen geschützt. Zuerst erklärt ein Ja/Nein-Dialog, dass der Datenträger gelöscht wird. Anschließend muss der Benutzer in einem Eingabefeld den exakten Gerätepfad eingeben (z. B. /dev/sdb), bevor ProxMenux eine neue GPT mit ext4-Partition erstellt und diese einhängt." + } + ], + "notMountedFallback": "Wenn kein USB-Gerät erkannt wird, fällt das Untermenü auf eine einfache Eingabebox zurück. Der Bediener kann einen beliebigen Einhängepunktpfad eingeben; wenn der Pfad kein registrierter Einhängepunkt ist, warnt ein Bestätigungsdialog, bevor er fortfährt." + }, + "safetyCheck": { + "heading": "Sicherheitsprüfung — Ziel innerhalb eines gesicherten Pfads", + "body": "Vor dem Schreiben des Archivs überprüft _bk_local, dass das Zielverzeichnis nicht ein Unterpfad eines zu sichernden Verzeichnisses ist. Ein häufiger Fehler wäre das Hinzufügen von /root zum Profil und die Auswahl von /root/backups als Ziel – das Archiv würde sich dann selbst einschließen und entweder ein beschädigtes Archiv erzeugen oder unbegrenzt wachsen, bis die Festplatte voll ist. Die Prüfung löst beide Pfade mit readlink -m auf, vergleicht sie und bricht bei einem Konflikt die Sicherung mit einem Dialogfeld ab, das den in Konflikt stehenden Pfad benennt und drei Möglichkeiten zur Lösung auflistet: Wählen Sie ein Ziel außerhalb des in Konflikt stehenden Pfads, entfernen Sie den benutzerdefinierten Eintrag, der das Ziel enthält, oder deaktivieren Sie den in Konflikt stehenden Pfad für diesen Lauf im benutzerdefinierten Modus." + }, + "archiveFormat": { + "heading": "Archivformat und Komprimierung", + "intro": "Der Ausgabedateiname bettet den Quellhostnamen und den Backup-Zeitstempel ein, so dass ein Verzeichnis mit mehreren Archiven chronologisch sortiert und jede Datei sich selbst identifiziert.", + "namePattern": "hostcfg-HOSTNAME-YYYYMMDD_HHMMSS.tar.zst", + "compressionTitle": "Kompression", + "compressionBody": "Der primäre Pfad verwendet tar --zstd -cf - eine Single-Command-Pipeline, die auf dem Standardniveau von zstd komprimiert wird. Wenn zstd nicht in der Quelle vorhanden ist (selten bei Proxmox, aber bei minimalen Installationen möglich), fällt ProxMenux auf gzip zurück. Im Fallback-Pfad wird, wenn pv verfügbar ist, ein Fortschrittsbalken zur Pipeline hinzugefügt, so dass der Betreiber die Archivgröße in Echtzeit wachsen sieht; ohne pv wird tar -czf still verwendet.", + "sourceTitle": "Was geht ins Archiv", + "sourceBody": "Der tar-Befehl wird mit -C \"$staging_root\" . aufgerufen und archiviert dadurch das gesamte Staging-Hauptverzeichnis: rootfs/, metadata/ und manifest.json. Alle drei Datenblöcke liegen nebeneinander auf der obersten Ebene des tar-Archivs. Beim Entpacken entsteht genau der Baum, den der Wiederherstellungscode verwendet." + }, + "sidecar": { + "heading": "Die JSON-Begleitdatei", + "intro": "Jedes erfolgreiche lokale Backup erzeugt eine Begleitdatei: HOSTNAME-TIMESTAMP.tar.zst.proxmenux.json, geschrieben neben dem Archiv von hb_write_archive_sidecar. Diese kleine JSON-Datei ermöglicht es dem ProxMenux Monitor, das Archiv als ProxMenux-Host-Backup zu identifizieren, auch wenn es später an anderer Stelle verschoben, umbenannt oder archiviert wird.", + "contentTitle": "Inhalt der Begleitdatei", + "contentBody": "Das Sidecar speichert die Schemaversion, ob das Backup von einem interaktiven Lauf oder einem geplanten Job (kind) stammt, die Job-ID für geplante Läufe, den verwendeten Profilmodus (default oder custom), den Quellhostnamen, den ursprünglichen Archivbasisnamen, einen ISO-8601-Erstellungszeitstempel und die Archivgröße in Bytes.", + "whyBody": "Die Registerkarte Backups des Monitors scannt konfigurierte lokale Verzeichnisse für *.proxmenux.json-Begleitdateien - nicht für *.tar.zst-Dateien -, da ein .tar.zst ohne Sidecar möglicherweise überhaupt kein ProxMenux-Backup ist. Der Scan ist schnell (JSON-Dateien sind winzig) und die Paarung ist stabil bei Umbenennungen des Archivs, solange das Sidecar entsprechend umbenannt wird." + }, + "restoreAccess": { + "heading": "Wiederherstellen aus einem lokalen Archiv", + "body": "Der ProxMenux-Wiederherstellungsfluss entdeckt lokale Archive, indem er das konfigurierte lokale Ziel nach Begleitdateien durchsucht und in der Archivliste anzeigt. Die Auswahl eines Archivs löst _rs_check_layout aus, der den Tarball in ein Staging-Verzeichnis extrahiert und das Layout mit drei Nutzlasten bestätigt, bevor er fortfährt. Für die manuelle Extraktion außerhalb von ProxMenux liefert tar --zstd -xf hostcfg-HOSTNAME-TIMESTAMP.tar.zst -C /tmp/hostcfg den gleichen Baum, den der Wiederherstellungscode verbraucht." + } +} diff --git a/web/messages/de/docs/backup-restore/destinations/pbs.json b/web/messages/de/docs/backup-restore/destinations/pbs.json new file mode 100644 index 00000000..2beffaef --- /dev/null +++ b/web/messages/de/docs/backup-restore/destinations/pbs.json @@ -0,0 +1,113 @@ +{ + "meta": { + "title": "Proxmox Backup Server Zielort — Repository, Verschlüsselung, Wiederherstellung | ProxMenux", + "description": "Das PBS-Ziel speichert ProxMenux-Host-Backups als PBS-Backups. Beschrieben werden die Repository-Erkennung aus /etc/pve/storage.cfg, die manuelle PBS-Konfiguration, der .pxar-Upload, die clientseitige Verschlüsselung mit einer Schlüsseldatei samt optionaler passphrasengeschützter Sicherungskopie sowie die Schlüsselwiederherstellung aus mehreren Quellen auf einem neu installierten Host.", + "ogTitle": "ProxMenux Backup — Bestimmungsort Proxmox Backup Server", + "ogDescription": "So speichert ProxMenux Host-Backups auf Proxmox Backup Server mit clientseitiger Schlüsseldateiverschlüsselung und einer hinterlegten Wiederherstellungskopie.", + "twitterTitle": "PBS Backup Ziel | ProxMenux", + "twitterDescription": "Proxmox Backup Server als Ziel mit Schlüsseldateiverschlüsselung und passphrasengeschützter Wiederherstellungskopie." + }, + "header": { + "title": "Proxmox Backup Server", + "description": "Das PBS-Ziel lädt das gesamte Vorbereitungsverzeichnis als ein einzelnes .pxar-Backup in einen Proxmox-Backup-Server-Datenspeicher hoch. Optional wird es clientseitig mit einer Schlüsseldatei verschlüsselt; zusätzlich kann neben jedem Backup eine passphrasengeschützte Wiederherstellungskopie der Schlüsseldatei hinterlegt werden.", + "section": "Sicherung und Wiederherstellung" + }, + "recommendedBadge": "Empfohlenes Ziel", + "aboutPbs": { + "heading": "Was Proxmox Backup Server ist", + "body": "Proxmox Backup Server (PBS) ist das eigene Backup-Server-Produkt von Proxmox, das von dem gleichen Team entwickelt und gewartet wird, das auch Proxmox VE entwickelt hat. Es ist ein dedizierter Backup-Server, der entwickelt wurde, um Backups von Proxmox VE-Hosts (VMs, LXCs und - über proxmox-backup-client - beliebige Host-Verzeichnisse) mit chunkbasierter Deduplizierung, clientseitiger Verschlüsselung und serverseitig angewendeten Aufbewahrungsrichtlinien zu erhalten. ProxMenux verwendet PBS als eines der drei Ziele für Host-Backups; jeder auf dieser Seite beschriebene PBS-spezifische Mechanismus (Backup-Gruppen, .pxar-Archive, --backup-id, Keyfile-Verschlüsselung) ist Standard-PBS-Verhalten." + }, + "intro": { + "title": "Ein Backup pro Lauf, Chunk-Level-Dedup", + "body": "Ein PBS-Backup erzeugt einen einzelnen Backup-Eintrag im Datenspeicher, der unter der Backup-ID host/hostcfg-HOSTNAME/BACKUP-TIME gruppiert ist. Die Nutzlast ist ein .pxar-Archiv, das das gleiche dreiblockige Layout enthält, das in How it works beschrieben ist. PBS dedupliziert auf der Chunk-Ebene über alle Backups im Datenspeicher, so dass nachfolgende Backups desselben Hosts nur die geänderten Chunks übertragen und speichern. Die Retention wird von ProxMenux selbst für geplante Aufträge angewendet — run_scheduled_backup.sh führt proxmox-backup-client prune mit --keep-last / --keep-daily / --keep-weekly nach jedem erfolgreichen Lauf mit den für den Auftrag konfigurierten Werten aus." + }, + "repoSelection": { + "heading": "Repository-Auswahl", + "intro": "ProxMenux entdeckt PBS-Repositories aus zwei Quellen in jedem Backup. Der Bediener wählt einen aus einem einheitlichen Menü aus; die Auswahl bestimmt HB_PBS_REPOSITORY, HB_PBS_SECRET und HB_PBS_FINGERPRINT für den Lauf.", + "sourceRows": [ + { + "source": "Proxmox storage.cfg (automatisch erkannt)", + "path": "/etc/pve/storage.cfg + /etc/pve/priv/storage/NAME.pw", + "content": "Jede pbs: Strophe in der eigenen Speicherkonfiguration von Proxmox wird automatisch abgeholt. Server, Datenspeicher, Benutzername und Fingerabdruck stammen aus der Strophe. Das Passwort wird aus dem eigenen Anmeldeverzeichnis von Proxmox gelesen. Auf der ProxMenux-Seite ist kein erneuter Eintrag erforderlich – das Repository ist verfügbar, sobald es in Proxmox konfiguriert ist." + }, + { + "source": "ProxMenux manuelle Konfiguration", + "path": "/usr/local/share/proxmenux/pbs-manual-configs.txt + pbs-pass-NAME.txt + pbs-fingerprint-NAME.txt", + "content": "Zusätzlich von Backup-Ziele konfigurieren → PBS Ziele → PBS hinzufügen. Prompts für einen Namen, Benutzernamen (root@pam oder user@pbs!token), Host oder IP, Datenspeicher und Passwort. Das Passwort wird bei leeren Eingaben erneut angezeigt - ein leeres Speichern würde ansonsten still bleiben und jedes nachfolgende Backup würde mit einem undurchsichtigen Authentifizierungsfehler fehlschlagen. Dieser Pfad wird verwendet, wenn das Ziel PBS nicht als Proxmox-Speicher registriert ist." + } + ], + "menuTitle": "Auswahlmenü", + "menuBody": "Beide Quellen werden in einem einzigen Menü angezeigt, wobei jede Zeile mit ihrem Ursprung markiert ist ([proxmox] oder [manual]). Einträge, deren Passwort nicht aufgelöst werden konnte, werden mit einer ⚠ no password-Warnung versehen – die Auswahl einer löst einen erneuten Passworteintrag aus, bevor das Backup startet. Der Fingerabdruck wird über die PBS_FINGERPRINT-Umgebungsvariable an proxmox-backup-client übergeben; bei Abwesenheit fordert der Client den Betreiber auf, das Serverzertifikat beim ersten Backup interaktiv zu akzeptieren." + }, + "backupCommand": { + "heading": "Der Backup-Befehl", + "intro": "Der Upload erfolgt mit einem einzigen Aufruf von proxmox-backup-client backup. ProxMenux übergibt Passwort, Verschlüsselungspasswort und Fingerabdruck mit env als Umgebungsvariablen an den Client.", + "cmd": "env \\\n PBS_PASSWORD=\"$HB_PBS_SECRET\" \\\n PBS_ENCRYPTION_PASSWORD=\"$HB_PBS_ENC_PASS\" \\\n PBS_FINGERPRINT=\"$HB_PBS_FINGERPRINT\" \\\n proxmox-backup-client backup \\\n hostcfg.pxar:$staging_root \\\n --repository USER@REALM@HOST:DATASTORE \\\n --backup-type host \\\n --backup-id hostcfg-HOSTNAME \\\n --backup-time BACKUP-EPOCH \\\n [--keyfile /usr/local/share/proxmenux/pbs-key.conf]", + "backupIdTitle": "Backup-ID-Bezeichnung", + "backupIdBody": "Die Standard-Backup-ID ist hostcfg-HOSTNAME. Vor dem Upload wird sie bestätigt oder bearbeitet. Zeichen außerhalb von [A-Za-z0-9_-] sowie abschließende Bindestriche werden entfernt. Die Wiederverwendung derselben ID bei weiteren Läufen ist beabsichtigt: PBS behandelt die ID als Gruppe. Jedes weitere Backup erscheint als neuer Eintrag in dieser Gruppe und nutzt die Deduplizierung mit früheren Backups.", + "pxarTitle": "Warum die Quelle die vollständige Staging-Wurzel ist", + "pxarBody": "Die .pxar-Quelle ist die gesamte staging_root - rootfs/, metadata/ und manifest.json zusammen. Frühere Versionen bestanden $staging_root/rootfs als Quelle; Das ließ metadata/ aus dem Archiv und die Kompatibilitätsprüfung der Wiederherstellung hatte nichts zu lesen, was selbst bei Wiederherstellungen des gleichen Hosts zu Cross-Host-Warnungen führte. Alte Backups, die mit der Rootfs-only-Quelle erstellt wurden, werden immer noch korrekt über den _rs_check_layout-Fall-3-Zweig wiederhergestellt, der einen flachen etc/var/root/usr-Baum zurück in eine rootfs/-Hierarchie wickelt." + }, + "encryption": { + "heading": "Clientseitige Verschlüsselung", + "glossaryHint": "Dieser Abschnitt verwendet die Begriffe Schlüsseldatei, Passphrase und Wiederherstellungshülle. Das Glossar erklärt die Unterschiede mit einem Satz pro Begriff.", + "intro": "Die clientseitige PBS-Verschlüsselung mit einer Schlüsseldatei verschlüsselt die Blöcke vor dem Upload auf dem Quellhost. In ProxMenux ist die Verschlüsselung optional. Wird sie aktiviert, entscheidet der Betreiber bei der Einrichtung ausdrücklich mit Ja oder Nein, ob zusätzlich eine passphrasengeschützte Wiederherstellungskopie der Schlüsseldatei auf PBS hinterlegt wird; diese Wahl lässt sich später im Monitor ändern. Die Schlüsseldatei selbst liegt immer unter dem kanonischen Pfad /usr/local/share/proxmenux/pbs-key.conf und wird für alle weiteren verschlüsselten Backups ohne erneute Nachfrage verwendet.", + "keyfileTitle": "Einrichten der Keyfile", + "keyfileBody": "Der Verschlüsselungsdialog erscheint beim ersten verschlüsselten Backup des Hosts. Zunächst wird gefragt, ob das Backup verschlüsselt werden soll: Nein fährt ohne Verschlüsselung fort, Ja führt zum zweiten Schritt. Dieser hängt davon ab, ob bereits eine Schlüsseldatei unter /usr/local/share/proxmenux/pbs-key.conf installiert ist. Falls ja, wird sie ohne Nachfrage wiederverwendet und das Backup fortgesetzt; auch spätere Backups fragen nicht erneut. Andernfalls bietet ein Menü zwei Möglichkeiten. Neue Schlüsseldatei erzeugen führt proxmox-backup-client key create --kdf none aus. Vorhandene Schlüsseldatei verwenden erkennt eine von PVE verwaltete Datei automatisch: Ist das gewählte PBS-Repository in Proxmox mit einem Verschlüsselungsschlüssel unter /etc/pve/priv/storage/<NAME>.enc registriert, wird diese Datei ohne Nachfrage an den kanonischen ProxMenux-Pfad kopiert. Gibt es keine passende PVE-Datei, wird nach dem absoluten Pfad gefragt, unter dem der Betreiber die Schlüsseldatei bereits auf diesem Host abgelegt hat. In beiden Fällen liegt sie danach am kanonischen Pfad mit chmod 600.", + "modesTitle": "Schlüsseldatei je Host oder gemeinsam nutzen", + "modesIntro": "Beide Betriebsmodelle werden unterstützt und keines wird durchgesetzt - die Wahl liegt beim Betreiber, je nachdem, wie seine Flotte organisiert ist.", + "modesPerHostTitle": "Eigene Schlüsseldatei je Host (Standard)", + "modesPerHostBody": "Jeder Host generiert seine eigene Schlüsseldatei, wenn er zum ersten Mal die PBS-Verschlüsselung aktiviert. Die Isolation ist maximal: Durch die Kompromittierung der Schlüsseldatei eines Hosts werden die Backups anderer Hosts nicht offengelegt. Empfohlen für Produktionsflotten und für Umgebungen, in denen Hosts unterschiedliche Eigentümer oder Compliance-Grenzen haben.", + "modesSharedTitle": "Gemeinsame Keyfile (Import auf jedem Host)", + "modesSharedBody": "Eine Hauptschlüsseldatei wird einmal generiert und auf jedem Host über die Option Vorhandene Schlüsseldatei verwenden installiert. Die Verwaltung ist einfacher: Ein einziges Geheimnis muss geschützt werden, und jeder Host kann die Archive jedes anderen entschlüsseln (nützlich für Konsolidierung, hostübergreifende Wiederherstellungsübungen oder zentralisierte Backup-Verifizierung). Der Nachteil besteht darin, dass ein Leck der gemeinsam genutzten Schlüsseldatei jeden Host auf einmal offenlegt. Empfohlen für Homelabs und Flotten, bei denen alle Hosts denselben Eigentümer und dieselbe Vertrauensgrenze haben.", + "recoveryTitle": "Schlüssel auf PBS hinterlegen — der Betreiber entscheidet", + "recoveryBody": "Direkt nach Auswahl der Schlüsseldatei fragt ProxMenux ausdrücklich Schlüssel auf PBS hochladen? und bietet zwei Antworten. Erst wenn der Betreiber Ja wählt, wird ein Wiederherstellungsartefakt auf PBS hochgeladen. Vorgabe ist Nein, nur lokal behalten: Die Schlüsseldatei bleibt am kanonischen lokalen Pfad, ProxMenux lädt kein Wiederherstellungsartefakt auf PBS hoch und der Betreiber sorgt selbst für eine externe Kopie. Die Schaltfläche Schlüsseldatei herunterladen in der PBS-Zielzeile des Monitors stellt sie bei Bedarf bereit. Bei Ja, hochladen wird zweimal eine Wiederherstellungspassphrase abgefragt und auf Übereinstimmung geprüft. ProxMenux schützt die Schlüsseldatei damit mittels AES-256-CBC und PBKDF2 (600.000 Iterationen, zufälliges Salt) und erzeugt pbs-key.recovery.enc. Jedes weitere verschlüsselte Backup lädt diese Hülle als zugehörige Backup-Gruppe auf PBS hoch. Dabei werden weder die Passphrase noch die Schlüsseldatei im Klartext an PBS gesendet. Die Entscheidung lässt sich jederzeit im Monitor ändern: Der Schalter in der PBS-Zielzeile wählt Ja oder Nein und übernimmt je nach Zustand sofort Upload starten, Upload stoppen oder Passphrase aktualisieren.", + "blobUploadTitle": "Gepaarte Backup-Gruppe auf PBS", + "blobUploadBody1": "Ist die Hinterlegung auf Ja, hochladen gesetzt, lädt ProxMenux nach jedem verschlüsselten Backup die Wiederherstellungshülle als zweite, zugehörige Backup-Gruppe auf PBS hoch. Sie heißt wie die Host-Gruppe, ergänzt um -keyrecovery. Beide Gruppen stehen in der Datenspeicherliste nebeneinander: eine für Host-Backups, eine für Wiederherstellungshüllen. Die Hülle verlässt den Host niemals im Klartext: Noch vor der Übertragung verschlüsselt der Host sie mit AES-256-CBC und PBKDF2 (openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt). PBS erhält ausschließlich die bereits verschlüsselte Hülle.", + "envelopeSecurityTitle": "Ist das Hochladen der keyrecovery-Datei in PBS ein Sicherheitsrisiko?", + "envelopeSecurityBody": "Die keyrecovery-datei ist verschlüsselt in transit und in ruhe. Die Wiederherstellungspassphrase verlässt den Host nie: Verschlüsselung geschieht lokal vor dem Upload, und PBS sieht immer nur das bereits verschlüsselte Ergebnis. Ein PBS-Administrator - oder jeder, der Zugriff auf den Datenspeicher hat - kann die KeyRecovery-Datei herunterladen, aber ohne die Wiederherstellungspassphrase können sie die Keyfile nicht lesen: Es ist undurchsichtiger Geheimtext. Das Rekonstruieren der Keyfile erfordert beide Teile gleichzeitig, die KeyRecovery-Datei und die Passphrase, und nur der Operator hält beide.", + "blobUploadConstraintTitle": "Warum zwei Gruppen statt einer einzigen verschlüsselten", + "blobUploadConstraintBody": "Ein einzelner Aufruf von proxmox-backup-client backup verschlüsselt entweder alle enthaltenen Archive mit derselben Schlüsseldatei oder keines davon. Das Host-Backup muss mit der Host-Schlüsseldatei verschlüsselt werden. Die Wiederherstellungshülle darf aber nicht im selben Upload liegen, sonst würde sie mit genau der Schlüsseldatei verschlüsselt, die sie enthält. Auf einem neu installierten Host ließe sie sich nicht öffnen: Zum Entschlüsseln der Schlüsseldatei wäre diese selbst nötig. Deshalb werden die beiden Artefakte unabhängig hochgeladen und erscheinen als zwei Gruppen. Der PBS-Client verschlüsselt das Host-Backup mit der Schlüsseldatei; ProxMenux verschlüsselt die Wiederherstellungshülle vor dem Upload mit der Passphrase. Zwei verschiedene Verschlüsselungsschichten schützen zwei unterschiedliche Inhalte.", + "blobUploadImageAlt": "PBS Benutzeroberfläche zeigt die Backup-Gruppen hostcfg-HOSTNAME und hostcfg-HOSTNAME-keyrecovery angrenzend im Datenspeicher.", + "blobUploadImageCaption": "PBS UI — die gepaarten Backup-Gruppen. Die Hauptgruppe hält die Host-Backups; die -keyrecovery-Gruppe hält die passphrase-verpackte Schlüsseldatei.", + "recoverTitle": "Wiederherstellung auf einem neu installierten Host", + "recoverBody": "Fehlt die lokale Schlüsseldatei am kanonischen Pfad, etwa nach einer Neuinstallation oder ausdrücklichem Löschen, versucht die Wiederherstellung drei Quellen der Reihe nach und stoppt bei der ersten brauchbaren Schlüsseldatei. Erstens wird eine zum gewählten PBS-Repository passende, von PVE verwaltete Datei unter /etc/pve/priv/storage/<NAME>.enc ohne Nachfrage an den kanonischen Pfad kopiert. Zweitens wird, falls auf PBS eine Gruppe -keyrecovery vorhanden ist, deren neuester Snapshot heruntergeladen und die Wiederherstellungspassphrase zum Entschlüsseln abgefragt. Funktioniert beides nicht oder lehnt der Betreiber die Optionen ab, fragt der dritte Schritt nach dem absoluten Pfad der Schlüsseldatei auf diesem Host und kopiert sie an den kanonischen Pfad. Der Monitor bietet denselben Importablauf im Backup-Detaildialog. Oben erscheint ein gelbes Feld, wenn Wiederherstellen, Herunterladen oder Inhalt anzeigen eine nicht installierte Schlüsseldatei benötigen. Ist eine Datei installiert, passt aber nicht zum Backup, meldet PBS wrong key — manifest's key XX does not match provided key YY. Dieselben drei Aktionen zeigen dann ein strukturiertes gelbes Feld mit dem erforderlichen Manifest-Fingerabdruck an, damit der Betreiber die passende Schlüsseldatei für dieses Backup erkennen und importieren kann.", + "monitorManagementTitle": "Schlüsseldatei im Monitor verwalten", + "monitorManagementBody": "Der Monitor zeigt die Schlüsseldatei-Steuerung direkt in jeder PBS-Zielzeile der Backup-Konfigurationsseite. Es gibt drei Aktionen: Herunterladen exportiert die aktuelle Datei in den Browser des Betreibers; Hochladen ersetzt die installierte Datei durch eine bereitgestellte Datei, wobei die bisherige erst nach erfolgreicher Installation der neuen am kanonischen Pfad überschrieben wird; Löschen entfernt die Datei vom Host. Daneben befindet sich die Steuerung der Wiederherstellungskopie: eine Ja/Nein-Auswahl für Schlüssel auf PBS hochladen?, bei Ja ein Passphrase-Feld und eine passende Übernehmen-Schaltfläche mit Upload starten, Upload stoppen oder Passphrase aktualisieren. Jede Aktion nutzt dieselben Flask-Endpunkte wie die Terminaloberfläche. Die Wirkung auf Dateien und den PBS-Zustand ist deshalb bei beiden Oberflächen identisch." + }, + "restoreAccess": { + "heading": "Abruf auf der Wiederherstellungsseite", + "body": "Der Wiederherstellungsfluss erkennt ProxMenux Host-Backups auf PBS, indem er Backup-Gruppen unter dem konfigurierten Repository auflistet und nach Backup-ID-Muster filtert. Die Registerkarte Backups des Monitors stellt dieselbe Liste dar. Durch das Auswählen eines Backups wird proxmox-backup-client restore mit demselben Repository + Passwort + Fingerabdruck (und --keyfile, als das Backup verschlüsselt wurde) ausgelöst, wodurch das .pxar in ein Staging-Verzeichnis extrahiert wird, das _rs_check_layout dann der Standardwiederherstellungspipeline zuführt. Für das manuelle Abrufen außerhalb von ProxMenux extrahiert derselbe Befehl das Archiv auf einen beliebigen Pfad - der resultierende Baum kann inspiziert oder einer handgesteuerten Wiederherstellung zugeführt werden." + }, + "references": { + "heading": "Referenzen", + "intro": "Offizielle Dokumentation von Proxmox Backup Server zu den Komponenten, auf die ProxMenux aufbaut.", + "items": [ + { + "label": "Proxmox Backup Server Dokumentation", + "href": "https://pbs.proxmox.com/docs/", + "tail": "— Haupteinstiegspunkt für Installation, Verwaltung, Speicherung, Benutzer und Rollen." + }, + { + "label": "proxmox-backup-client", + "href": "https://pbs.proxmox.com/docs/backup-client.html", + "tail": " — das Befehlszeilenwerkzeug, das ProxMenux für jedes Backup und jede Wiederherstellung aufruft. Beschreibt Backup-IDs, Backup-Typen, Archive, Repository-Syntax und Umgebungsvariablen." + }, + { + "label": "Clientseitige Verschlüsselung", + "href": "https://pbs.proxmox.com/docs/backup-client.html#encryption", + "tail": " — Erzeugung der Schlüsseldatei, --kdf-Modi und das Verschlüsselungsmodell, das ProxMenux um eine passphrasengeschützte Wiederherstellungskopie erweitert." + }, + { + "label": "Datenspeicherverwaltung", + "href": "https://pbs.proxmox.com/docs/storage.html", + "tail": "Erstellen und Verwalten der Datenspeicher, die Host-Backups erhalten, einschließlich Chunk-Store-Layout und Berechtigungen." + }, + { + "label": "Aufbewahrungsregeln und Garbage Collection", + "href": "https://pbs.proxmox.com/docs/maintenance.html#pruning", + "tail": " — das Aufbewahrungsmodell hinter --keep-last / --keep-daily / --keep-weekly, das ProxMenux je geplantem Auftrag anwendet, sowie die Freigabe ungenutzter Blöcke durch PBS nach der Bereinigung." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/glossary.json b/web/messages/de/docs/backup-restore/glossary.json new file mode 100644 index 00000000..32a4be40 --- /dev/null +++ b/web/messages/de/docs/backup-restore/glossary.json @@ -0,0 +1,230 @@ +{ + "meta": { + "title": "Glossar - Backup & Restore | ProxMenux", + "description": "Begriffe des ProxMenux-Sicherungs- und Wiederherstellungssystems: Verschlüsselung, Schlüsseldatei, Passphrase, Wiederherstellungsumschlag, PBS, Chunks, rootfs, Manifest, Ausführungsprozess und Ergänzung zwischen Kernel-Versionen.", + "ogTitle": "Backup & Restore Glossar | ProxMenux", + "ogDescription": "Klare Definitionen für das Vokabular, das in der ProxMenux-Backup- und Wiederherstellungsdokumentation verwendet wird.", + "twitterTitle": "Backup & Restore Glossar | ProxMenux", + "twitterDescription": "Passphrase, Keyfile, Recovery-Umschlag, Rootfs, Manifest, PBS, Brocken — alle an einer Stelle definiert." + }, + "header": { + "title": "Glossar", + "description": "Begriffe, die in der Backup- und Wiederherstellungsdokumentation mit ihrer Definition verwendet werden.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "body": "Begriffe werden nach Themen gruppiert. Jeder Eintrag gibt den Begriff und eine kurze Definition." + }, + "groups": [ + { + "title": "Verschlüsselung und Key Recovery", + "intro": "Vokabular für den PBS-Verschlüsselungsblock. Es beinhaltet zwei verschiedene Geheimnisse: eine binäre Schlüsseldatei und eine geschriebene Passphrase.", + "entries": [ + { + "id": "passphrase-recuperacion", + "term": "Wiederherstellungspassphrase", + "def": "Textkennwort, das der Benutzer auswählt, um den Wiederherstellungsumschlag zu schützen. Es wird zweimal mit einer Übereinstimmungsprüfung aufgefordert und wird nur verwendet, um den Umschlag zu verschlüsseln und zu entschlüsseln, wenn die Schlüsseldatei jemals wiederhergestellt werden muss. Es ist nicht die Passphrase, die die Schlüsseldatei selbst entsperrt und wird niemals an PBS gesendet. Während eines normalen Backups verlässt es niemals den Host." + }, + { + "id": "clave-cifrado", + "term": "Keyfile (Verschlüsselungsschlüssel)", + "def": "Schlüsseldatei, mit der proxmox-backup-client die Chunks vor dem Hochladen zu PBS verschlüsselt. Ohne passenden Schlüssel sind verschlüsselte Daten nicht lesbar. ProxMenux speichert sie unter /usr/local/share/proxmenux/pbs-key.conf mit Modus 600. Schützen Sie sie; der Wiederherstellungsumschlag dient ihrer Wiederherstellung bei Verlust." + }, + { + "id": "contrasena-keyfile", + "term": "Keyfile Passphrase (KDF Passphrase)", + "def": "Optionale Passphrase, die die Keyfile selbst tragen kann, im Unterschied zur Recovery Passphrase. Es existiert nur, wenn die Keyfile mit --kdf scrypt erstellt wurde (normalerweise eine importierte Keyfile). Ohne sie kann die Keyfile nicht für die Verwendung entsperrt werden. ProxMenux behält es in verschlüsselter Form auf dem Host, so dass geplante Backups ohne interaktive Eingabeaufforderung ausgeführt werden können." + }, + { + "id": "sobre-recuperacion", + "term": "Wiederherstellungsumschlag", + "def": "Das keyfile ist in eine zusätzliche Schicht eingewickelt: Es ist mit der Wiederherstellungspassphrase unter Verwendung von AES-256-CBC und PBKDF2 verschlüsselt, bevor es den Host verlässt. Es wird auf PBS als -keyrecovery-Gruppe hochgeladen. Ohne die Passphrase ist der Umschlag undurchsichtig: Wenn Sie ihn von PBS herunterladen, wird die Schlüsseldatei nicht angezeigt." + }, + { + "id": "cifrado-cliente", + "term": "Clientseitige Verschlüsselung", + "def": "Verschlüsselungsmodell, bei dem Daten auf dem Quellhost verschlüsselt werden, bevor sie hochgeladen werden. PBS empfängt und speichert immer nur Geheimtext; nicht einmal ein PBS-Administrator mit Datenspeicherzugriff kann ihn ohne die Schlüsseldatei lesen. ProxMenux baut auf diesem Modell auf und fügt den Wiederherstellungsumschlag hinzu." + }, + { + "id": "kdf-none", + "term": "--kdf none", + "def": "Option auf proxmox-backup-client key create, die eine Schlüsseldatei ohne angehängte Passphrase erzeugt. Die Keyfile kann direkt ohne Aufforderung verwendet werden, was für geplante Aufträge, die unbeaufsichtigt ausgeführt werden, unerlässlich ist. Dies ist, was ProxMenux standardmäßig verwendet, wenn eine neue Keyfile erzeugt wird." + }, + { + "id": "kdf-scrypt", + "term": "--kdf scrypt", + "def": "Option auf proxmox-backup-client key create, die eine Schlüsseldatei erzeugt, die durch eine keyfile-Passphrase geschützt ist. Jede Verwendung der Keyfile erfordert das Entsperren mit dieser Passphrase. ProxMenux akzeptiert diese Art von Keyfile beim Import und speichert die Passphrase verschlüsselt, so dass geplante Backups immer noch unbeaufsichtigt ausgeführt werden können." + }, + { + "id": "aes-256-cbc", + "term": "AES-256-CBC", + "def": "Standardsymmetrische Chiffre. ProxMenux verwendet es (über openssl enc -aes-256-cbc), um die Schlüsseldatei mit der Wiederherstellungspassphrase zu wickeln und die Umschlag zu produzieren." + }, + { + "id": "pbkdf2", + "term": "PBKDF2", + "def": "Kryptografische Funktion, die eine vom Menschen typisierte Passphrase in einen für die Verschlüsselung geeigneten binären Schlüssel verwandelt. Es verlangsamt Brute-Force-Versuche, indem es viele Iterationen anwendet. ProxMenux verwendet es mit 600 000 Iterationen und einem Zufallssalz bei der Verschlüsselung des Recovery-Umschlags." + } + ] + }, + { + "title": "Backup-Struktur", + "intro": "Begriffe, die beschreiben, was sich in einem Backup befindet, unabhängig vom Zielort.", + "entries": [ + { + "id": "rootfs", + "term": "rootfs / Root-Dateisystem", + "def": "Dateibasierte Kopie des Dateisystems des Quellhosts, erstellt mit rsync nach dem Standardprofil und zusätzlichen eigenen Pfaden. Kein vollständiges Datenträgerabbild: Kopiert werden nur Pfade mit Konfiguration oder Zustand, die Proxmox nicht selbst wiederherstellen kann." + }, + { + "id": "manifiesto", + "term": "Manifest (manifest.json)", + "def": "Strukturiertes JSON-Dokument zur Beschreibung des Quell-Hosts zum Backup-Zeitpunkt: Hardware, Storage, Kernel, ProxMenux-installierte Komponenten und Inventar von VMs und LXCs. Es wird von sechs unabhängigen Erfassungsroutinen produziert. Der Wiederherstellungsfluss verwendet ihn, um Quelle und Ziel zu vergleichen." + }, + { + "id": "staging", + "term": "Stagingverzeichnis", + "def": "Temporäres Verzeichnis, in dem ProxMenux die drei Blöcke (rootfs, Metadaten und Manifest) zusammenstellt, bevor sie verpackt und zum Zielort hochgeladen werden. Es wird automatisch entfernt, wenn das Backup beendet ist, auch bei Abbruch." + }, + { + "id": "perfil-defecto", + "term": "Standardprofil", + "def": "Kuratierte Reihe von Pfaden, die ProxMenux standardmäßig in jedem Backup kopiert. Umfasst PVE-Konfiguration, Netzwerk, SSH, Kernel, apt, ProxMenux Tools und mehr. Es ist im Code definiert und auf der Seite Wie es funktioniert dokumentiert." + }, + { + "id": "rutas-personalizadas", + "term": "Benutzerdefinierte Pfade", + "def": "Zusätzliche Pfade, die der Benutzer zusätzlich zum default-Profil hinzufügt. Entweder persistent (gespeichert in backup-extra-paths.txt) oder per Run (im Custom-Modus ausgewählt)." + } + ] + }, + { + "title": "Ziele und Speicher", + "intro": "Begriffe, die an den Ort gebunden sind, an dem das Backup landet - PBS-Server, lokales Archiv oder Borg-Repository - und wie PBS die Daten organisiert.", + "entries": [ + { + "id": "repositorio-pbs", + "term": "PBS-Repository", + "def": "Kombination von Proxmox Backup Server + datastore + Benutzer, der das Backup erhält. Identifiziert durch den String user@realm@host:datastore. Ein einzelner PBS-Server kann mehrere Repositories hosten." + }, + { + "id": "datastore", + "term": "Datenspeicher", + "def": "Konkreter Datenspeicher auf einem PBS-Server für Sicherungen und ihre Chunks. Er liegt auf einem Dateisystem des PBS-Servers und besitzt eigene Berechtigungen und Richtlinien." + }, + { + "id": "chunk", + "term": "Chunk/Chunk-Deduplizierung", + "def": "PBS teilt jedes Backup in Blöcke variabler Größe auf, identifiziert jedes Stück durch seinen Hash und speichert jedes Stück nur einmal, auch wenn es in vielen verschiedenen Backups erscheint. Dies bedeutet, dass aufeinanderfolgende Backups desselben Hosts - oder über ähnliche Hosts - sehr wenig zusätzlichen Speicherplatz benötigen." + }, + { + "id": "fingerprint", + "term": "Fingerabdruck", + "def": "Kryptografischer Fingerabdruck des PBS-Servers TLS Zertifikat. Der Client kann überprüfen, ob er mit dem vorgesehenen Server spricht, ohne sich auf eine öffentliche Zertifizierungsstelle zu verlassen. ProxMenux leitet sie über die Umgebungsvariable PBS_FINGERPRINT an proxmox-backup-client weiter." + }, + { + "id": "grupo-copia", + "term": "Backup-Gruppe", + "def": "Logischer Container innerhalb von PBS, unter dem sich aufeinanderfolgende Backups desselben Assets ansammeln. Jedes neue Backup teilt die Deduplizierung mit früheren in derselben Gruppe. ProxMenux verwendet zwei Gruppen pro Host, wenn die Verschlüsselung aktiviert ist: eine für das Host-Backup und eine mit einem -keyrecovery-Suffix für den Wiederherstellungsumschlag." + }, + { + "id": "backup-id", + "term": "Backup-ID", + "def": "Name der backup-Gruppe. ProxMenux ist standardmäßig hostcfg-HOSTNAME und lässt den Benutzer es vor dem Hochladen bearbeiten; nicht unterstützte Zeichen werden automatisch entfernt." + }, + { + "id": "pxar", + "term": ".pxar", + "def": "PBS-natives Archivformat für Verzeichnisse. Es verhält sich wie ein für die Chunk-Deduplizierung optimierter tar-Archiv: Anstatt eine undurchsichtige Datei zu speichern, zerlegt PBS seinen Inhalt in wiederverwendbare Chunks." + } + ] + }, + { + "title": "Ausführung und Terminplanung", + "intro": "Begriffe, die vom ProxMenux-Planed-Jobs-System verwendet werden.", + "entries": [ + { + "id": "runner", + "term": "Ausführungsprozess", + "def": "Das run_scheduled_backup.sh-Script, das ein geplantes Backup ausführt. Es liest die Job-Konfiguration, startet das passende Backend (Local, PBS oder Borg), wendet nach Erfolg die Aufbewahrungsregeln an und sendet die konfigurierten Benachrichtigungen." + }, + { + "id": "trabajo-programado", + "term": "Geplanter Auftrag", + "def": "Beharrliche Konfiguration eines wiederkehrenden Backups. Behält Ziel, Anmeldeinformationen, Verschlüsselung, Zeitplan und Aufbewahrung. Lebt bei /var/lib/proxmenux/backup-jobs/<id>.env." + }, + { + "id": "modo-adjunto", + "term": "Anhängemodus", + "def": "Auftrag ohne eigenen Timer. Ein job-end-Hook von PVE-vzdump startet ihn nach Gastsicherungen bei passendem Zielspeicher. Der gewählte Elternauftrag liefert Speicher und Aufbewahrung, die bei jedem Lauf erneut aus dem Elternauftrag gelesen wird; der Hook vergleicht aber Speicher statt Auftrags-ID. Auch andere Aufgaben desselben Speichers können ihn auslösen. Nur Local und PBS." + }, + { + "id": "timer-systemd", + "term": "Systemd Timer", + "def": "Standard-systemd-Mechanismus, der einen Befehl in einem Kalender auslöst. ProxMenux erstellt einen Timer pro unabhängigem geplanten Job; attach-mode Jobs verwenden keinen Timer, sie laufen über den PVE-Skript-Hook." + }, + { + "id": "retencion", + "term": "Aufbewahrung (keep-last, keep-daily…)", + "def": "Richtlinie, die entscheidet, welche älteren Backups aufbewahrt und welche beschnitten werden sollen. Definiert pro Auftrag mit Parametern wie keep-last, keep-daily, keep-weekly, keep-monthly. Wird vom Ausführungsprozess nach jedem erfolgreichen Backup angewendet." + } + ] + }, + { + "title": "Wiederherstellen", + "intro": "Terme aus dem Wiederherstellungsfluss.", + "entries": [ + { + "id": "restauracion-universal", + "term": "Universale Wiederherstellung", + "def": "Der ProxMenux-Wiederherstellungsfluss extrahiert nicht nur das Dateisystem: Er liest das Manifest, um Unterschiede zwischen Quelle und Ziel zu erkennen, reproduziert die Systemkonfiguration und führt die Installateure für die ProxMenux-verwalteten Komponenten erneut aus. Ziel ist es, den Quellhost zu reproduzieren, nicht nur das Archiv." + }, + { + "id": "hidratacion", + "term": "Ergänzung zwischen Kernel-Versionen (Hydration)", + "def": "Wiederherstellungsschritt, der die eigene Quellkonfiguration (IOMMU-Parameter, VFIO-Geräte-IDs, eigene Anpassungen und GRUB-Schlüssel) in die frische Boot-Konfiguration des Ziels übernimmt, ohne kernelgebundene Quelldateien unverändert zu kopieren. Läuft, wenn der Zielkernel neuer als der der Sicherung ist." + }, + { + "id": "cross-kernel", + "term": "Kernübergreifende Wiederherstellung", + "def": "Wiederherstellung, bei der der Zielkernel vom Quellkernel abweicht und meist neuer ist. Der Ergänzungsschritt verhindert die Übernahme von Dateien, die mit dem Zielkernel inkompatibel sind." + }, + { + "id": "instalador-postboot", + "term": "Installateur nach dem Booten", + "def": "Komponenten, die nach dem ersten Booten des wiederhergestellten Hosts neu installiert wurden (NVIDIA-Treiber, Coral TPU-Treiber, AMD GPU-Tools, Intel GPU-Tools). Jeder Installer läuft gegen den Kernel des Ziels, so dass der wiederhergestellte Host nicht davon abhängt, ob der Kernel der Quelle vorhanden ist." + }, + { + "id": "compatibility-check", + "term": "Kompatibilitätsprüfung", + "def": "Vergleich zwischen dem Quellenmanifest und dem Zustand des Ziels, den ProxMenux zu Beginn der Wiederherstellung durchführt. Es entscheidet, welche Teile wortwörtlich kopiert werden, welche Hydratation benötigen und welche von einem Installateur neu erstellt werden." + }, + { + "id": "equipo-recien-instalado", + "term": "Frische Installation", + "def": "Host nur von Grund auf neu installiert, ohne lokale Schlüsseldatei. Um ein verschlüsseltes Backup wiederherzustellen, muss zunächst die Schlüsseldatei aus der Wiederherstellungsumschlag, die auf PBS gespeichert ist, mit der Wiederherstellungspassphrase wiederhergestellt werden." + } + ] + } + ], + "whereNext": { + "heading": "Wo als nächstes", + "items": [ + { + "label": "Wie es funktioniert", + "href": "/docs/backup-restore/how-it-works", + "tail": "- wo das Rootfs-, Manifest- und Anwendungsinventar in ein Backup passt." + }, + { + "label": "Proxmox-Backup-Server-Ziel", + "href": "/docs/backup-restore/destinations/pbs", + "tail": "- wobei clientseitige Verschlüsselung, Keyfile, Recovery-Passphrase und Envelope ausführlich dokumentiert sind." + }, + { + "label": "Wiederherstellung", + "href": "/docs/backup-restore/restoring", + "tail": " — Wiederherstellungsablauf und Verwendung des Manifests." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/how-it-works.json b/web/messages/de/docs/backup-restore/how-it-works.json new file mode 100644 index 00000000..6a8320b1 --- /dev/null +++ b/web/messages/de/docs/backup-restore/how-it-works.json @@ -0,0 +1,205 @@ +{ + "meta": { + "title": "Wie ProxMenux Backup intern funktioniert – Rootfs, Manifest, Anwendungen", + "description": "Detaillierte Aufschlüsselung dessen, was ein ProxMenux-Backup enthält: die Rootfs, die von rsync des Standardpfadprofils erzeugt werden, das strukturierte Manifest, das von sechs unabhängigen Sammlern erstellt wird, und das Anwendungsinventar, das die automatische Neuinstallation von Paketen und Komponenten nach einer Wiederherstellung steuert.", + "ogTitle": "Wie ProxMenux Backup intern funktioniert", + "ogDescription": "Die drei Nutzlasten eines ProxMenux-Backups wurden erklärt: Rootfs, Manifest und Anwendungen.", + "twitterTitle": "Wie ProxMenux Backup funktioniert | ProxMenux", + "twitterDescription": "Die drei Nutzlasten eines ProxMenux-Backups und wie die Wiederherstellung den Quellhost von ihnen reproduziert." + }, + "header": { + "title": "Wie es funktioniert", + "description": "Die interne Aufschlüsselung eines ProxMenux-Backups - Dateisystem, Manifest und Anwendungsinventar - und wie die Wiederherstellung alle drei verbraucht, um den Quellhost auf einem Ziel zu reproduzieren, das möglicherweise nicht den gleichen Kernel teilt.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Ein Archiv, drei Nutzlasten", + "body": "Jedes Backup erzeugt drei klar getrennte Datenblöcke unter einem gemeinsamen Vorbereitungsverzeichnis. Das hochgeladene Archiv (lokales .tar.zst, PBS-Backup oder Borg-Archiv) enthält genau diese Struktur. Die Wiederherstellung nutzt alle drei: Das Manifest wird vor Änderungen ausgewertet, um Abweichungen zu erkennen und Ausschlüsse festzulegen; rootfs liefert die Konfigurationsdateien, die direkt oder nach einem Neustart angewendet werden; das Anwendungsinventar steuert die Paketinstallation und die Neuinstallation der Komponenten nach dem Start. Jeder Block lässt sich unabhängig untersuchen oder entpacken." + }, + "layout": { + "heading": "Archivlayout", + "intro": "Jedes Archiv folgt dem gleichen Baumlayout, unabhängig vom Zielort. Das metadata/-Unterverzeichnis enthält die strukturierten Nutzlasten; das rootfs/-Unterverzeichnis enthält die Dateisystemkopie.", + "treeCaption": "Das beim Backup angelegte Vorbereitungsverzeichnis. Alle Ziele erhalten denselben Verzeichnisbaum in ihrem nativen Format: tar bei lokalen Zielen, PBS-Blöcke bei PBS und Borg-Segmente bei Borg.", + "tree": "backup-[timestamp]/\n├── manifest.json # structured host state\n├── metadata/\n│ ├── packages.manual.list # apt-mark showmanual\n│ ├── run_info.env # backup run identity + kernel version + pmxcfs method\n│ ├── paths_archived.txt # exact list of paths that reached rootfs/\n│ └── missing_paths.txt # paths from the profile absent on source\n└── rootfs/\n ├── etc/ # /etc/pve, /etc/network, /etc/systemd/network, /etc/ssh, …\n ├── root/ # /root (with volatile subdirs excluded)\n ├── usr/local/ # /usr/local/bin, /usr/local/share/proxmenux, …\n └── var/ # /var/lib/pve-cluster (config.db via sqlite3 .backup), /var/spool/cron/…" + }, + "rootfs": { + "heading": "Die Rootfs Nutzlast", + "intro": "Der Verzeichnisbaum rootfs/ ist eine einfache Dateisystemkopie, die rsync vom Quellhost erstellt. Er enthält ein ausgewähltes Standardprofil der für eine Proxmox-Wiederherstellung wichtigen Pfade sowie eigene Pfade, die der Benutzer zum Backup-Auftrag oder zur interaktiven Sitzung hinzufügt. Die Auswahl ist bewusst eng: nur Pfade, die Konfiguration enthalten oder Zustand speichern, den Proxmox nicht selbst wiederherstellen kann.", + "defaultProfileTitle": "Das Standardprofil", + "defaultProfileBody": "Das Standardprofil wird durch hb_default_profile_paths in lib_host_backup_common.sh definiert. Es umfasst acht feste Kategorien und bei Bedarf den ZFS-Zustand, die zusammen einen funktionierenden Proxmox-Host beschreiben:", + "categoriesTitle": "Pfadkategorien", + "categoryRows": [ + { + "category": "PVE-Kern", + "paths": "/etc/pve, /var/lib/pve-cluster, /etc/vzdump.conf", + "why": "Cluster-Dateisysteminhalte und vzdump-Standards. Die pmxcfs-Datenbank (config.db) wird als konsistenter Snapshot über sqlite3 .backup erfasst, ohne den Cluster zu stoppen, und die .db-wal / .db-shm Sidecars sind vom rsync ausgeschlossen. Wenn sqlite3 nicht verfügbar ist, wird stattdessen config.db.raw-fallback erstellt und bei der Wiederherstellung automatisch als Datenbank übernommen." + }, + { + "category": "Host-Identität und Netzwerk", + "paths": "/etc/hostname, /etc/hosts, /etc/timezone, /etc/resolv.conf, /etc/network, /etc/systemd/network", + "why": "Alles, was der Host für den Netzwerkstart mit derselben Identität benötigt. Die .link-Dateien unter /etc/systemd/network ordnen Netzwerkkartennamen ihren MAC-Adressen zu, damit Neuinstallationen und Hardwareänderungen die Schnittstellen nicht umbenennen." + }, + { + "category": "Zugriff und Authentifizierung", + "paths": "/etc/ssh, /etc/sudoers, /etc/sudoers.d, /etc/pam.d, /etc/security", + "why": "SSH Schlüssel, Sudo Regeln und PAM Konfiguration. Wenn Sie diese verlieren, wird der Benutzer aus dem wiederhergestellten Host ausgeschlossen." + }, + { + "category": "Kernel und Boot", + "paths": "/etc/default/grub, /etc/kernel, /etc/modules, /etc/modules-load.d, /etc/modprobe.d, /etc/sysctl.conf, /etc/sysctl.d, /etc/udev/rules.d, /etc/fstab, /etc/iscsi, /etc/multipath", + "why": "IOMMU-Token, Modul-Blacklists, VFIO-Geräte-IDs, Mount-Tabelle, Storage-Stack-Konfiguration." + }, + { + "category": "Shell und Gebietsschema", + "paths": "/etc/environment, /etc/bash.bashrc, /etc/inputrc, /etc/profile, /etc/profile.d, /etc/locale.gen, /etc/locale.conf", + "why": "Systemweite Shell-Einrichtung, Umgebungsvariablen und Konfiguration der Gebietsschema-Erzeugung." + }, + { + "category": "Paketverwaltung und cron", + "paths": "/etc/apt, /etc/cron.d, /etc/cron.{daily,hourly,weekly,monthly}, /etc/cron.allow, /etc/cron.deny, /var/spool/cron/crontabs", + "why": "APT-Quellen für konsistente Paketauflösung, geplante Aufgaben, die vom Benutzer definiert werden." + }, + { + "category": "ProxMenux Zustand & Werkzeuge", + "paths": "/etc/proxmenux, /etc/systemd/system, /etc/log2ram.conf, /etc/logrotate.conf, /etc/logrotate.d, /etc/lm-sensors, /etc/sensors3.conf, /etc/fail2ban, /etc/snmp, /etc/postfix, /etc/wireguard, /etc/openvpn, /etc/grafana, /etc/influxdb, /etc/prometheus, /etc/telegraf, /etc/zabbix", + "why": "Optionale, aber übliche Proxmox-Werkzeuge. Fehlende Pfade werden in metadata/missing_paths.txt vermerkt, ohne das Backup zu stoppen." + }, + { + "category": "ProxMenux Binärdateien und root", + "paths": "/usr/local/bin, /usr/local/sbin, /usr/local/share/proxmenux, /var/lib/proxmenux, /root (ausgenommen flüchtige Subdir)", + "why": "ProxMenux-installierte Binärdateien und Konfiguration pro Benutzer unter /root. Flüchtige Pfade (.bash_history, .cache/, tmp/, .local/share/Trash/) sind von der Kopie ausgeschlossen. /var/lib/proxmenux hält die geplanten Backup-Jobs und den Host-Status, der von ProxMenux beibehalten wird; das Wiederherstellungsstaging, die Rollback-Kopien und die Post-Restore-Marker sind ausgeschlossen, da sie zu dem Host gehören, der eine Wiederherstellung ausführt." + }, + { + "category": "ZFS-Zustand (bedingter Zustand)", + "paths": "/etc/zfs", + "why": "Nur enthalten, wenn der Quellhost ZFS ausführt. Enthält zpool.cache und hostid." + } + ], + "customTitle": "Erweiterung des Profils mit benutzerdefinierten Pfaden", + "customBody": "Neben dem Standardprofil bietet ProxMenux zwei Möglichkeiten, zusätzliche Pfade in ein Backup aufzunehmen. Sie komponieren ohne Konflikte und gelten beide für interaktive Backups und geplante Jobs.", + "customExtrasTitle": "1. Persistente Extras (Pro-Host-Datei)", + "customExtrasBody": "Eine Textdatei bei /usr/local/share/proxmenux/backup-extra-paths.txt enthält eine Liste absoluter Pfade, die der Benutzer auf diesem Host als \"immer einschließen\" markiert hat. Wenn ein Backup im Default-Modus ausgeführt wird, fügt ProxMenux diese Pfade automatisch an das Standardprofil an, ohne zu fragen. Die Datei wird von der Benutzeroberfläche aus bearbeitet - keine manuelle Bearbeitung erforderlich - und bleibt bei Neustarts und Updates bestehen. Ein absoluter Pfad pro Zeile; #-Kommentare sind zulässig.", + "customModeTitle": "2. Benutzerdefinierter Modus (per Lauf)", + "customModeBody": "Das Starten eines Backups im Custom-Modus ersetzt die automatische Anwendung des Standardprofils durch eine Checkliste, die jeden Pfad anzeigt: die Standardprofileinträge und die persistenten Extras (vorangestellt mit [+] und bereits ausgewählt). Der Benutzer aktiviert oder deaktiviert Einträge für diesen bestimmten Lauf und kann auch Eigenen Pfad hinzufügen drücken, um einen neuen Pfad einzugeben - der dann für zukünftige Backups auf der Extras-Datei verbleibt.", + "customMissingTitle": "Aus der Quelle fehlende Pfade", + "customMissingBody": "Jeder Pfad aus dem Profil (Standard oder hinzugefügt), der auf dem Quellhost nicht vorhanden ist, wird in metadata/missing_paths.txt im Archiv aufgezeichnet. Das Backup schlägt nicht fehl und stoppt nicht – der Benutzer sieht am Ende eine Zusammenfassung der archivierten Pfade und fehlenden Pfade. In der Praxis geschieht dies bei optionalen Werkzeugpfaden wie /etc/wireguard oder /etc/prometheus, wenn diese Werkzeuge nicht installiert sind." + }, + "manifest": { + "heading": "Der Manifest-Datenblock", + "intro": "manifest.json ist ein strukturiertes JSON-Dokument, das den Quellhost zum Zeitpunkt des Backups beschreibt. Es wird von sechs unabhängigen Erfassungsskripten orchestriert von build_manifest.sh produziert. Jeder Kollektor ist schreibgeschützt, erzeugt ein gut definiertes JSON-Fragment und fällt bei einem Ausfall auf einen sicheren leeren Standard zurück - das Manifest ist immer noch verwendbar, wenn ein Abschnitt unvollständig ist.", + "orchestratorCaption": "Die sechs Sammler verfassen das Manifest. Jeder Kollektor läuft in seinem eigenen Subprozess; ein Fehler in einem fällt auf einen dokumentierten sicheren Standard zurück und warnt, bricht aber das Backup nicht ab.", + "collectorRows": [ + { + "collector": "collect_source_host.sh", + "produces": "source_host", + "content": "Hostname, PVE-Version (pveversion), PBS-Version, wenn der Host die Backup-Server-Rolle ausführt, Kernel (uname -r), Boot-Modus (efi/bios), Root-Dateisystemtyp, CPU-Modell + Architektur, Speicher in KB." + }, + { + "collector": "collect_hardware.sh", + "produces": "hardware_inventory", + "content": "GPUs (mit Hersteller- und ProxMenux-Installationsprogramm), TPUs (Coral USB + M.2 mit lsusb/lspci-Erkennung), NICs (mit MAC-, PCI-Slot- und Bridge-Mitgliedschaft), drahtlose Geräte. GPU-Einträge tragen eine passthrough_eligible-Heuristik." + }, + { + "collector": "collect_storage.sh", + "produces": "storage_inventory", + "content": "ZFS-Pools (mit Pooltyp + Mitglieds-Festplatten, die nach /dev/disk/by-id/*, /dev/disk/by-partuuid/* oder rohen /dev/sdX-Pfaden aufgelöst wurden, je nachdem, wie jeder Pool ursprünglich erstellt wurde), LVM-Volumengruppen + dünne Pools, physische Festplatten mit SMART-Fähigkeit, PVE storage.cfg-Einträge, externe Mounts." + }, + { + "collector": "collect_kernel.sh", + "produces": "kernel_params", + "content": "Vom Betreiber hinzugefügte Parameter aus /proc/cmdline, ohne übliche Standardwerte wie BOOT_IMAGE=, root=, ro/rw, quiet und splash; beim Start geladene Module aus /etc/modules; sowie Pfade der Dateien /etc/modprobe.d/*.conf mit tatsächlichen Direktiven (options, blacklist, install, alias, softdep)." + }, + { + "collector": "collect_proxmenux_state.sh", + "produces": "proxmenux_installed_components", + "content": "Lesen Sie /usr/local/share/proxmenux/managed_installs.json (Registrierung von allem, was ProxMenux installiert hat) und installed_tools.json. Jeder Eintrag behält den Installationspfad (menu_script) bei, so dass die Wiederherstellung genau den gleichen Installationsfluss auslösen kann." + }, + { + "collector": "collect_guests.sh", + "produces": "vms_lxcs_at_backup", + "content": "Zählt VMs (qm list) und LXCs (pct list) auf, die zum Backup-Zeitpunkt vorhanden sind — VMID, Name, aktueller Status. Nur das Inventar: Die eigentlichen Gästedaten liegen in der Verantwortung von vzdump / PBS." + } + ], + "schemaTitle": "Validierung des Schemas", + "schemaBody": "Das Manifest bestätigt gegen scripts/backup_restore/schema/manifest.schema.json. Das Ausführen von build_manifest.sh --validate löst eine Python-seitige JSON-Schemavalidierung aus (erfordert python3 + jsonschema). Wenn das Modul nicht vorhanden ist, wird die Überprüfung still übersprungen - die Validierung ist in erster Linie eine Entwicklerhilfe, keine Laufzeitabhängigkeit." + }, + "applications": { + "heading": "Das Anwendungsinventar", + "intro": "Zwei Inventare erfassen die zusätzlich zu den Proxmox-VE-Basispaketen installierte Software: die Paketliste unter metadata/ und das Komponentenverzeichnis im rootfs, beide unten beschrieben. Die Wiederherstellung installiert die Benutzersoftware je nach ursprünglichem Installationsweg mit APT oder den ProxMenux-Installationsskripten neu.", + "packagesTitle": "packages.manual.list", + "packagesBody": "Eine alphabetisch sortierte Textliste aus apt-mark showmanual mit allen auf dem Quellhost ausdrücklich installierten APT-Paketen. Als Abhängigkeiten des Proxmox-VE-Basissystems installierte Pakete fehlen darin; APT auf dem Ziel zieht sie selbst nach. Bei der Wiederherstellung liest _rs_run_complete_extras die Liste und wendet einen dreistufigen Filter gegen Kaskadenentfernung an: dpkg -s erkennt bereits installierte Pakete, für Bibliotheken werden vorhandene andere Hauptversionen erkannt und apt-get install --simulate prüft das Risiko von Folgeentfernungen. Danach folgt apt-get install -y.", + "componentsTitle": "components_status.json (Teil des rootfs)", + "componentsBody": "Ein JSON-Verzeichnis unter /usr/local/share/proxmenux/ erfasst jede von ProxMenux installierte Komponente mit ihrem genauen Zustand: Version und ProxMenux-spezifische Merkmale (bei NVIDIA der boolesche Wert patched, bei Coral die DKMS-Version). Die Datei liegt im rootfs und nicht unter metadata/, weil sie erst gelesen wird, nachdem das rootfs auf das Ziel kopiert wurde. Der nach dem Start ausgeführte Dispatcher (apply_cluster_postboot.sh) geht alle Einträge durch und ruft den --auto-reinstall-Hook der jeweiligen Komponente auf. Dieser liest den gespeicherten Zustand und stellt die Installation für den aktuellen Kernel des Zielsystems wieder her.", + "componentInstallersTitle": "Installationsskripte für Komponenten", + "componentInstallersBody": "Vier ProxMenux-Installateure stellen derzeit einen --auto-reinstall-Einstiegspunkt frei:", + "installerRows": [ + { + "component": "nvidia_driver", + "installer": "gpu_tpu/nvidia_installer.sh", + "action": "Lesen Sie version + patched. Downloads der exakt gleichen NVIDIA-Runfile, baut DKMS-Module gegen den Kernel des Ziels und wendet den ProxMenux-Patch erneut an, wenn die Quelle ihn hatte." + }, + { + "component": "coral_driver", + "installer": "gpu_tpu/install_coral.sh", + "action": "Lesen Sie die Coral Driver Version. Kompiliert das DKMS-Modul gegen den Kernel des Ziels." + }, + { + "component": "amdgpu_top", + "installer": "gpu_tpu/amd_gpu_tools.sh", + "action": "Lesen Sie die aufgezeichnete Version. Laden Sie die genaue .deb aus dem GitHub-Release erneut herunter." + }, + { + "component": "intel_gpu_tools", + "installer": "gpu_tpu/intel_gpu_tools.sh", + "action": "APT installiert das Paket. Idempotent, wenn bereits vorhanden von packages.manual.list." + } + ] + }, + "restoreFlow": { + "heading": "Wie die Wiederherstellung die drei Nutzlasten verbraucht", + "intro": "Die Wiederherstellung ist eine sechsstufige Pipeline. Jede Stufe liest eine bestimmte Untergruppe des Archivs und aktualisiert den Zielhost. Keine Stufe erfordert, dass der Quellhost erreichbar ist - das Archiv ist vollständig in sich geschlossen.", + "stagesCaption": "Stufe 1 wertet das Manifest aus und legt fest, was geändert werden darf. Stufe 2 kopiert rootfs-Pfade, die auf dem laufenden System angewendet werden können. Stufe 3 bereitet riskante Pfade für den nächsten Start vor. Stufe 3b importiert ZFS-Datenpools ohne Root-Dateisystem, deren Datenträger vollständig vorhanden sind. Stufe 4 installiert Pakete. Stufe 5 installiert nach dem Neustart Komponenten für den Kernel des Ziels neu.", + "stageRows": [ + { + "stage": "1", + "name": "Kompatibilitätsprüfung", + "reads": "manifest.json", + "action": "Läuft hb_compat_check aus. Vergleicht Quelle und Zielhardware (NICs, Storage IDs), PVE-Version und Hauptkernel-Version. Setzt HB_COMPAT_KERNEL_DIRECTION (same, bk_newer oder bk_older) ein und füllt RS_SKIP_PATHS mit Hardware-Drift- und Cross-Kernel-Ausschlüssen." + }, + { + "stage": "2", + "name": "Anwenden im laufenden Betrieb", + "reads": "rootfs/ (nur sichere Pfade)", + "action": "_rs_apply … hot kopiert hb_classify_path=hot-Einträge direkt auf das Live-Ziel. Alles, was unter /etc/pve, /etc/network oder als reboot/dangerous klassifiziert ist, wird verschoben." + }, + { + "stage": "3", + "name": "Ausstehende Vorbereitung", + "reads": "rootfs/ (Reboot + gefährliche Pfade)", + "action": "_rs_prepare_pending_restore bereitet riskante Pfade unter /var/lib/proxmenux/pending-restore/ vor, schreibt plan.env, apply-on-boot.list und rs-skip-paths.txt und aktiviert proxmenux-restore-onboot.service für den nächsten Start." + }, + { + "stage": "3b", + "name": "Import von Datenpools", + "reads": "manifest.storage_inventory + ZFS-Zustand des Ziels", + "action": "_rs_import_data_pools importiert jeden Nicht-Root-ZFS-Pool, dessen vollständiger Satz von Festplatten auf dem Ziel vorhanden ist. Pools mit einem fremden Hostid werden mit zpool import -f getestet. Pools, denen eine Festplatte fehlt, werden mit einer Warnung übersprungen. Das Ergebnis pro Pool wird in /var/log/proxmenux/restore-datapools-<timestamp>.log aufgezeichnet." + }, + { + "stage": "4", + "name": "Paketinstallation", + "reads": "metadata/packages.manual.list", + "action": "_rs_run_complete_extras führt den kaskadensicheren Filter aus und ruft apt-get install -y mit der Paketliste auf. Die volle Ausgabe geht an /var/log/proxmenux/restore-apt-*.log." + }, + { + "stage": "5", + "name": "Postboot", + "reads": "rootfs (bereits angewendet) + components_status.json", + "action": "Nach dem Neustart spielt apply_pending_restore.sh die zurückgestellten Pfade ab und apply_cluster_postboot.sh läuft update-initramfs, update-grub (oder proxmox-boot-tool refresh) und iteriert über components_status.json und feuert den --auto-reinstall-Hook jeder Komponente." + } + ] + }, + "whyItWorks": { + "heading": "Warum der Drei-Payload-Split verwendet wird", + "body": "Die Aufteilung richtet sich nach dem Lebenszyklus der Daten. Dateisysteminhalte werden mit rsync übertragen: schnell, nachvollziehbar und atomar je Datei. Der vor Änderungen auszuwertende Konfigurationszustand liegt als strukturiertes JSON vor: unabhängig lesbar, per Schema versionierbar und maschinell mit dem Zielzustand vergleichbar. Neu zu installierende Software wird als Inventar mit Namen und Versionen erfasst. Der Paketmanager des Ziels und die ProxMenux-Installationsskripte wählen die passenden Binärdateien. Jeder Datenblock erfüllt seine eigene Aufgabe. Zusammen ermöglichen sie eine auf konsistente Wiederherstellung ausgerichtete, fehlertolerante Verarbeitung: Bei beschädigtem Manifest bleibt rootfs wiederherstellbar; ein fehlendes Paket verhindert die Komponenteninstallation nicht; scheitert ein Komponenteninstallationsskript an einem Eintrag, wird der nächste trotzdem bearbeitet." + } +} diff --git a/web/messages/de/docs/backup-restore/index.json b/web/messages/de/docs/backup-restore/index.json new file mode 100644 index 00000000..fa7bac95 --- /dev/null +++ b/web/messages/de/docs/backup-restore/index.json @@ -0,0 +1,93 @@ +{ + "meta": { + "title": "ProxMenux Sichern und Wiederherstellen – Übersicht | Vollständige Hostsicherung und -wiederherstellung für Proxmox VE", + "description": "ProxMenux Backup & Restore erfasst den vollständigen Zustand eines Proxmox-Hosts – Dateisystem, strukturiertes Konfigurationsmanifest und installierte Pakete/Komponenten – und reproduziert ihn auf demselben oder einem anderen Host. Das Backup und die Wiederherstellung sind in sich geschlossen: keine externen Abhängigkeiten und Kernel-übergreifende Wiederherstellungen werden durch einen richtungsbewussten Safe-Subset-Filter und eine Kernel-agnostische Hydratation unterstützt.", + "ogTitle": "ProxMenux Sicherung und Wiederherstellung – Übersicht", + "ogDescription": "Vollständige Hostsicherung und -wiederherstellung für Proxmox VE mit strukturiertem Manifest, Paketliste und Neuinstallationsprogrammen für Komponenten.", + "twitterTitle": "ProxMenux Sichern und Wiederherstellen | ProxMenux", + "twitterDescription": "Vollständige Hostsicherung und -wiederherstellung für Proxmox VE mit strukturiertem Manifest, Paketliste und Komponenten-Neuinstallationsprogrammen." + }, + "header": { + "title": "Sichern und Wiederherstellen", + "description": "Vollständige Hostsicherung und -wiederherstellung für Proxmox VE. Erfasst Dateisystem, Konfiguration und installierte Komponenten in einem einzigen Archiv und reproduziert den Host auf derselben oder einer anderen Proxmox-Installation ohne externe Abhängigkeiten.", + "section": "Sichern und Wiederherstellen" + }, + "intro": { + "title": "Was es ist, in einem Absatz", + "body": "Eine ProxMenux-Sicherung erfasst den vollständigen Zustand eines Proxmox-Hosts: das Dateisystem mit den relevanten Verzeichnissen unter /etc, /root und /var/lib/pve-cluster sowie optionalen eigenen Pfaden; ein strukturiertes Manifest im JSON-Format mit erkannter Hardware, Kernel-Parametern, Netzwerkkonfiguration, ZFS-Zustand, Benutzern und Cron-Einträgen; und ein Anwendungsinventar mit allen von APT als manuell installiert markierten Paketen und den von ProxMenux installierten Komponenten samt exakten Versionen. Alle drei unterstützten Ziele – lokales Archiv, Proxmox Backup Server (empfohlen) und Borg – erhalten denselben eigenständigen Datenbestand. Mit jedem davon lässt sich der Zielhost wiederherstellen, ohne dass der Quellhost während der Wiederherstellung erreichbar sein muss." + }, + "video": { + "heading": "Videoanleitung", + "intro": "Beobachten Sie Backup & Restore lieber in Aktion, bevor Sie es konfigurieren? Diese exemplarische Vorgehensweise umfasst eine vollständige End-to-End-Demo der Funktion in ProxMenux.", + "title": "ProxMenux – Anleitung zum Sichern und Wiederherstellen", + "caption": "Externes Video auf YouTube. Spielt im erweiterten Datenschutzmodus (keine Cookies, bis Sie auf „Play“ drücken)." + }, + "whatItIsNot": { + "heading": "Was es nicht ist", + "intro": "Dieser Abschnitt beschreibt Sicherung und Wiederherstellung auf Host-Ebene: die Proxmox-Installation selbst, nicht die darauf ausgeführten Anwendungen und Gastsysteme.", + "items": [ + "Dies ist kein Sicherungswerkzeug für VMs oder CTs. Gastfestplatten und RAM-Zustand werden nicht erfasst; dafür ist vzdump zuständig. Die Konfigurationsdateien der Gäste (/etc/pve/nodes/<node>/qemu-server/*.conf und lxc/*.conf) werden jedoch gesichert. Nach der Wiederherstellung erscheint das Gastinventar erneut, und die Festplatten können aus einer vorhandenen PBS- oder lokalen Sicherung wiederhergestellt und zugeordnet werden.", + "Dies ist kein clusterweiter Vorgang. Jeder Knoten sichert sich selbst. Die Clusterzugehörigkeit wird mit /etc/pve erfasst, sodass ein wiederhergestellter Knoten erneut aufgenommen werden kann. Die Wiederherstellung eines ganzen Clusters erfordert jedoch die Abstimmung zwischen den einzelnen Knoten.", + "Dies ist kein vollständiges Festplattenabbild. Kernel-Binärdateien, initramfs und Boot-Partition werden nicht gesichert. Bei der Wiederherstellung nutzt ProxMenux die Boot-Dateien des Zielhosts, die mit update-initramfs und dem Bootloader-Werkzeug automatisch neu erzeugt werden, und installiert passende Treiber für den dort laufenden Kernel." + ] + }, + "threePillars": { + "heading": "Die drei Säulen eines Backups", + "intro": "Ein ProxMenux-Archiv ist um drei in sich geschlossene Nutzdaten herum strukturiert. Bei der Wiederherstellung werden alle drei zusammen verwendet, um den Quellhost auf einem Ziel zu reproduzieren, auf dem möglicherweise nicht einmal derselbe Kernel installiert ist.", + "diagramCaption": "Jedes Backup versendet unabhängig vom Ziel dieselben drei Payloads. Die Wiederherstellung verbraucht alle drei: rootfs zum Ablegen von Dateien, Manifest zum Erkennen von Abweichungen und Kernel-übergreifenden Unterschieden und Anwendungsinventar zum Neuinstallieren von Paketen und Komponenten im eigenen Kernel des Ziels.", + "pillar1Label": "Dateisystem", + "pillar1Detail": "rootfs/\n(rsync von /etc,\n/etc/systemd/network,\n/root sowie ein\nsqlite3-Snapshot\nvon /var/lib/pve-cluster,\n+ optionale Pfade)", + "pillar2Label": "Manifest", + "pillar2Detail": "manifest.json\n(Hardware, Kernel-\nParameter, Netzwerk,\nZFS, Benutzer, Cron,\nZFS-Pools, Speicher)", + "pillar3Label": "Anwendungen", + "pillar3Detail": "packages.manual.list\n+ components_status.json\n(manuelle APT-Pakete + ProxMenux-\nInstaller mit Versionen)" + }, + "restoreIsUniversal": { + "heading": "Die Wiederherstellung reproduziert den Quellhost, nicht das Archiv", + "body": "Beim Wiederherstellen eines ProxMenux-Backups wird nicht nur das Dateisystem extrahiert. Der Wiederherstellungsfluss liest das Manifest, um Abweichungen zwischen der Quelle und dem Ziel zu erkennen (Hardwareunterschiede, NIC-Umbenennungen, Kernelversion, ZFS-Pool-Identität), spielt das Dateisystem erneut ab und löst dann das richtige Komponenteninstallationsprogramm für jeden Dienst aus, der auf der Quelle installiert wurde (NVIDIA-Treiber, Coral TPU, AMD GPU-Tools, Intel-GPU-Tools). Jedes Installationsprogramm wird für den aktuellen Kernel des Ziels ausgeführt, sodass der wiederhergestellte Host nicht davon abhängt, dass der Kernel der Quelle vorhanden ist. Wenn der Kernel des Ziels neuer ist als der des Backups, führt ein Kernel-unabhängiger Hydration-Durchgang die eigene Optimierung des Benutzers (IOMMU-Tokens, VFIO-Geräte-IDs, benutzerdefinierte Sonderanpassungen, GRUB-Schlüssel) in die neue Startkonfiguration des Ziels ein, ohne Kernel-gebundene Dateien wörtlich zu kopieren." + }, + "twoInterfaces": { + "heading": "Zwei Schnittstellen, ein Backend", + "intro": "Alle Schritte der Sicherung und Wiederherstellung sind über zwei Oberflächen erreichbar. Beide rufen dieselbe Shell-Bibliothek auf, erstellen identische Archive und verwenden dasselbe Auftragsverzeichnis.", + "cliLabel": "ProxMenux Scripts (TUI)", + "cliDetail": "Menü → Dienstprogramme →\nHost-Sicherung/Wiederherstellung\n\nDialogbasierter Ablauf,\nSSH-freundlich, skriptfähig\nzur unbeaufsichtigten Nutzung.", + "webLabel": "ProxMenux Monitor (Web UI)", + "webDetail": "Registerkarte Backups in der\nWeboberfläche von Monitor.\n\nWiederherstellung mit einem Klick,\nlaufende Protokollanzeige, integriert\nmit Benachrichtigungen und der\nAnzeige von Rollback-Differenzen." + }, + "whereNext": { + "heading": "Wohin geht es von hier aus?", + "intro": "Jeder Unterabschnitt unten behandelt einen Aspekt des Arbeitsablaufs ausführlich. Beginnen Sie mit Wie es funktioniert, wenn das Ziel darin besteht, zu verstehen, was das Archiv enthält und warum. Wechseln Sie zu Ziele, um das Ziel für die Kopie zu konfigurieren. Lesen Sie Wiederherstellung und Cross-Kernel-Wiederherstellung für die Wiederherstellungsseite.", + "items": [ + { + "label": "So funktioniert es", + "href": "/docs/backup-restore/how-it-works", + "tail": "– die drei Payloads (rootfs, Manifest, Anwendungen) im Detail, mit den Collectors, die sie erzeugen, und dem Format jeder Datei." + }, + { + "label": "Ziele", + "href": "/docs/backup-restore/destinations", + "tail": "– Vergleich von lokal, Proxmox Backup Server (empfohlen) und Borg und wie man sie jeweils konfiguriert." + }, + { + "label": "Backups erstellen", + "href": "/docs/backup-restore/creating-backups", + "tail": "– einmalige Sicherungen, das Standardpfadprofil, Hinzufügen benutzerdefinierter Pfade, Verschlüsselung für PBS." + }, + { + "label": "Geplante Jobs", + "href": "/docs/backup-restore/scheduled-jobs", + "tail": "– neue Jobs im Vergleich zum Anhängen an einen vorhandenen PVE-vzdump-Job, Planungsformate, das Jobdetail-Modal." + }, + { + "label": "Wiederherstellen", + "href": "/docs/backup-restore/restoring", + "tail": "– die drei Aktionen für ein Archiv (Anzeigen, Herunterladen, Wiederherstellen), vollständige vs. benutzerdefinierte Wiederherstellung, der Post-Boot-Dispatcher und warum die letzten zehn Minuten wichtig sind." + }, + { + "label": "Kernelübergreifende Wiederherstellung", + "href": "/docs/backup-restore/cross-kernel", + "tail": " — vom Versionswechsel abhängiges Verhalten bei unterschiedlichen Quell- und Zielkerneln, der Filter für eine sichere Teilmenge und die vier Hydratationsphasen." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/restoring.json b/web/messages/de/docs/backup-restore/restoring.json new file mode 100644 index 00000000..0804b2dd --- /dev/null +++ b/web/messages/de/docs/backup-restore/restoring.json @@ -0,0 +1,282 @@ +{ + "meta": { + "title": "Wiederherstellen eines Backups – vollständige und benutzerdefinierte Abläufe | ProxMenux", + "description": "Der komplette Wiederherstellungsworkflow. Dokumentiert die drei Aktionen, die für jedes Backup verfügbar sind (Ansicht, Download, Wiederherstellung), die Kompatibilitätsprüfung und ihre Ergebnisse, die vollständigen und benutzerdefinierten Wiederherstellungsmodi, die Pfadklassifizierung, die entscheidet, was live gilt und was auf den nächsten Start wartet, den On-Boot-Dispatcher und den Post-Boot-Komponentenneuinstallationspass.", + "ogTitle": "ProxMenux Backup — Wiederherstellung", + "ogDescription": "Vollständige und benutzerdefinierte Wiederherstellungsflüsse mit Kompatibilitätsprüfung, Pfadklassifizierung, On-Boot-Dispatcher und Post-Boot-Neuinstallation.", + "twitterTitle": "Wiederherstellen eines Backups | ProxMenux", + "twitterDescription": "Wie ein ProxMenux-Host-Backup wieder in einen funktionierenden Proxmox-Host umgewandelt wird." + }, + "header": { + "title": "Wiederherstellen eines Backups", + "description": "Der vollständige Wiederherstellungsworkflow: vom Auswählen eines Archivs bis zum Arbeitshost. Dokumentiert die Kompatibilitätsprüfung, die beiden Wiederherstellungsmodi, den On-Boot-Dispatcher, den Post-Boot-Reinstallationspass und die Mechanismen, die Cross-Host- und Cross-Kernel-Wiederherstellungen vorhersehbar machen.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Die Wiederherstellung reproduziert die Quelle, nicht nur die Dateien", + "body": "Ein ProxMenux-Backup wiederherzustellen umfasst mehr als das Entpacken von Dateien. Vor Änderungen wertet die Wiederherstellung das Manifest aus, erkennt Unterschiede zwischen Quelle und Ziel, schließt möglicherweise startgefährdende Pfade aus und passt Benutzereinstellungen an, die nicht unverändert kopiert werden können. Nach dem erforderlichen Neustart versucht sie, die erfassten unterstützten Komponenten (NVIDIA-Treiber, Coral-TPU und GPU-Werkzeuge) für den laufenden Zielkernel neu zu installieren. Ein ausgewähltes Archiv steuert Kompatibilitätsprüfung, Pfadklassifizierung und die Arbeiten beim und nach dem Start. Prüfen Sie die Komponentenergebnisse und Protokolle." + }, + "threeActions": { + "heading": "Drei Aktionen auf einem Backup", + "intro": "Durch Auswählen eines Backups aus der Liste wird ein Menü mit drei Aktionen geöffnet.", + "actionRows": [ + { + "action": "Ansicht", + "detail": "Öffnet eine schreibgeschützte Ansicht des Archivs: manifest.json-Inhalte (Quellhostname, PVE-Version, Kernel, Hardware, installierte Komponenten), die Liste der Pfade innerhalb von rootfs/ und ein Vergleich dessen, was sich auf dem aktuellen Host ändern würde, wenn das Backup angewendet würde." + }, + { + "action": "Herunterladen", + "detail": "Exportiert das Archiv als tragbare .tar.zst-Datei (tar komprimiert mit zstd). Es kann auf jedem Linux-System mit tar --zstd -xf FILE.tar.zst extrahiert werden, oder auf macOS / Windows mit jedem Tool, das zstd unterstützt (7-Zip, PeaZip, Keka ...). Der extrahierte Baum enthält manifest.json, metadata/ und rootfs/ - genau das gleiche Layout, das die Wiederherstellung verbraucht. Nützlich für die Offline-Inspektion oder für die Wiederherstellung auf einem Host ohne Zugriff auf das ursprüngliche Ziel (PBS, Borg)." + }, + { + "action": "Wiederherstellen", + "detail": "Der Schreibpfad. Extrahiert das Archiv in ein Staging-Verzeichnis, führt die Kompatibilitätsprüfung aus und präsentiert den Moduswähler (Full oder Custom)." + } + ] + }, + "compatibilityCheck": { + "heading": "Die Vereinbarkeitsprüfung", + "intro": "Bevor eine Datei geschrieben wird, vergleicht hb_compat_check den im Manifest beschriebenen Zustand mit dem Zielhost. Der Check läuft schreibgeschützt und erzeugt vier unabhängige Ausgänge, die den Rest der Wiederherstellung steuern.", + "outputRows": [ + { + "output": "Kernel-Richtungskennzeichen", + "detail": "HB_COMPAT_KERNEL_DIRECTION — eines von same, bk_newer oder bk_older. Vergleicht die Hauptkernelversion des Backups mit der des Ziels. Steuert den Cross-Kernel-Safe-Subset-Filter (feuert nur auf bk_older) und den Hydratationspass, der auf der Cross-Kernel-Seite dokumentiert ist." + }, + { + "output": "Liste der Skip Paths", + "detail": "RS_SKIP_PATHS - jeder Pfad, den die Wiederherstellung NICHT anwenden darf. Bevölkert durch zwei Mechanismen: Hardware-Drift (fehlende NIC, fehlende Speicher-ID, ausländischer ZFS-Pool) und - wenn die Richtung bk_older ist - die kernübergreifende Liste der unsicheren Pfade." + }, + { + "output": "NIC-Remapplan", + "detail": "Wenn ein NIC auf dem Ziel den gleichen MAC wie auf der Quelle, aber einen anderen Namen hat (typischerweise nach einem Motherboard-Swap), registriert die Kompatibilitätsprüfung einen Umbenennungsplan (HB_NIC_REMAP), der /etc/network/interfaces während der Wiederherstellung umschreibt." + }, + { + "output": "Rollback-Plan", + "detail": "Berechnet durch compute_rollback_plan.sh. Listet VMs, LXCs und Komponenten auf, die auf dem Ziel, aber nicht im Backup vorhanden sind. Im Bestätigungsdialog kann das Entfernen zusätzlicher Gäste vor dem Neustart gewählt werden. Komponenten müssen manuell entfernt werden; auch das Löschen von Gästen kann scheitern. Eine exakte Übereinstimmung wird daher nicht garantiert." + } + ], + "reportBody": "Der Check sendet auch einen strukturierten Bericht (HB_COMPAT_RESULTS) aus, der als PASS / INFO / WARN / FAIL kategorisiert ist. WARN- und FAIL-Einträge tauchen im Pre-Restore-Bereich auf; die Wiederherstellung weigert sich, nur dann fortzufahren, wenn ein FAIL vorhanden ist, den der Benutzer nicht durch Klicken auf Weiter beheben kann." + }, + "twoModes": { + "heading": "Vollständige Wiederherstellung vs Custom Wiederherstellung", + "intro": "Nach der Kompatibilitätsprüfung erscheint die Modusauswahl. Sie bestimmt, was angewendet wird und nicht, wie: Beide Modi verwenden denselben Ablauf.", + "modeRows": [ + { + "mode": "Vollständige Wiederherstellung", + "detail": "Wendet jeden Pfad im Archiv an, der die Drift- und Kreuzkernfilter überlebt. Führt auch die Paketinstallation und den automatischen Komponentenneuinstallationspass aus. Dies ist der Standard und die empfohlene Wahl - das Ziel ist es, die Quelle zu reproduzieren, nicht Stücke auszuwählen." + }, + { + "mode": "Benutzerdefinierte Wiederherstellung", + "detail": "Öffnet eine Checkliste, die jeden Pfad zeigt, den das Archiv führt. Der Benutzer tickt eine Teilmenge. Durch den Kreuzkernfilter blockierte Pfade erscheinen ausgegraut und können nicht ausgewählt werden. Paketinstallation und automatische Neuinstallation von Komponenten werden standardmäßig im benutzerdefinierten Modus übersprungen - der Benutzer signalisiert, dass er eine teilweise Anwendung und keine vollständige Reproduktion wünscht." + } + ] + }, + "pathClassification": { + "heading": "Wie Pfade klassifiziert werden", + "intro": "Jeder für die Wiederherstellung ausgewählte Pfad wird von hb_classify_path in eine von drei Kategorien eingestuft. Die Kategorie bestimmt, wann der Pfad auf das System angewendet wird und warum.", + "rows": [ + { + "class": "hot", + "detail": "Pfade angewendet sofort auf das laufende System. Der Dienst, der sie verbraucht, nimmt die Änderung selbst oder beim nächsten Neuladen auf - kein Neustart erforderlich. Dies ist der Hauptteil eines Backups: /etc/ssh, /etc/apt, /etc/cron.*, /root, /usr/local/bin, allgemeine Servicekonfigurationsdateien usw." + }, + { + "class": "reboot", + "detail": "Pfade, die dieser Ablauf für den nächsten Start zurückstellt, statt sie sofort anzuwenden. Sie betreffen die Startkonfiguration: Die Bootloader-Erzeugung liest /etc/default/grub, das Einhängen nutzt /etc/fstab und das Laden von Modulen /etc/modules. Nach dem Neustart werden die vorgemerkten Dateien angewendet und die nötigen Startartefakte neu erzeugt. Beispiele: /etc/default/grub, /etc/kernel, /etc/modules, /etc/fstab, /etc/zfs, /etc/modprobe.d." + }, + { + "class": "dangerous", + "detail": "Pfade, die NICHT im laufenden System angewendet werden: Ein Schreibzugriff zur Laufzeit könnte den Zustand beschädigen oder die aktive Verbindung trennen. Sie werden vorgemerkt: /etc/network wird früh beim nächsten Start geschrieben, bevor das Netzwerk startet, und /etc/pve schreibt der Post-Boot-Dispatcher, sobald das Cluster-Dateisystem bereit ist. Beispiele: /etc/pve (pmxcfs ist ein aktiver FUSE-Mount mit laufender Clusterkonfiguration), /var/lib/pve-cluster (aktive Clusterdaten, nur zur manuellen Wiederherstellung im Wiederherstellungsverzeichnis abgelegt), /etc/network (könnte die für die SSH-Verbindung genutzte Schnittstelle neu konfigurieren und die Sitzung trennen)." + } + ] + }, + "fullFlow": { + "heading": "Der vollständige Wiederherstellungsworkflow", + "intro": "Die komplette Pipeline von der Archivauswahl bis zu einem funktionierenden wiederhergestellten Host. Jede Stufe füttert die nächste; der von früheren Stufen geschriebene Zustand wird von späteren verbraucht.", + "diagram": "┌─────────────────────────────────────────────────────────────────┐\n│ Archive → staging_root/ │\n│ manifest.json metadata/ rootfs/ │\n└──────────────────────────────┬──────────────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 1. hb_compat_check │\n │ · hardware drift → RS_SKIP_PATHS │\n │ · kernel direction → same / bk_newer / bk_older │\n │ · NIC remap plan │\n │ · hydration plan (bk_older only) │\n │ · rollback plan │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 2. Confirmation dialog │\n │ Shows: hot count, pending count, drift, hydration, │\n │ NIC remap, cross-kernel note, reinstalls preview │\n └──────────────────────────────┬──────────────────────────┘\n │ (accepted)\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 3. Apply hot paths → LIVE │\n │ _rs_apply rsyncs each hot path │\n │ from staging_root/rootfs to / │\n │ (skips paths in RS_SKIP_PATHS) │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4. _rs_prepare_pending_restore │\n │ /var/lib/proxmenux/pending-restore/ │\n │ ├── apply-on-boot.list │\n │ ├── plan.env │\n │ ├── rs-skip-paths.txt │\n │ └── rootfs/ (deferred paths) │\n │ Enable proxmenux-restore-onboot.service │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 4b. _rs_import_data_pools │\n │ Imports non-root ZFS pools whose disks are all │\n │ present; retries with -f on foreign hostid; │\n │ skips with a warning when disks are missing. │\n │ Log: /var/log/proxmenux/restore-datapools-*.log │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 5. _rs_run_complete_extras │\n │ packages.manual.list │\n │ → cascade-safe filter │\n │ → apt-get install │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 6. REBOOT │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 7. apply_pending_restore.sh (on early boot) │\n │ Reads plan.env │\n │ Applies apply-on-boot.list (filtered) │\n │ Installs the postboot systemd unit │\n └──────────────────────────────┬──────────────────────────┘\n │\n ▼\n ┌─────────────────────────────────────────────────────────┐\n │ 8. apply_cluster_postboot.sh (~10 minutes) │\n │ Runs after pve-cluster is up │\n │ · Applies /etc/pve entries to pmxcfs │\n │ · update-initramfs -u -k all │\n │ · update-grub / proxmox-boot-tool refresh │\n │ · component --auto-reinstall (nvidia, coral, ...) │\n │ · Boot sanity check │\n │ · Notification: \"Host restore finished\" │\n └─────────────────────────────────────────────────────────┘" + }, + "pendingMachinery": { + "heading": "Die Mechanik der vorgemerkten Wiederherstellung", + "intro": "Pfade, die als reboot oder dangerous klassifiziert sind, und Hydratationsschreiben, werden für den nächsten Boot inszeniert, anstatt live angewendet zu werden. Dies geschieht durch einen kleinen, in sich geschlossenen Satz von Dateien unter /var/lib/proxmenux/pending-restore/:", + "rows": [ + { + "file": "apply-on-boot.list", + "content": "Ein relativer Pfad je Zeile: die genauen Pfade, die apply_pending_restore.sh aus dem vorbereiteten rootfs anwendet. Diese Datei zeigt, was beim nächsten Start geändert wird." + }, + { + "file": "plan.env", + "content": "Umgebungsvariablen, die über das On-Boot-Skript bezogen werden: Wiederherstellungs-ID, Kompatibilitätskennzeichen (HB_COMPAT_CROSS_VERSION, HB_COMPAT_KERNEL_DIRECTION, HB_HYDRATION_APPLIED), Rollback-Opt-in-Flag, Clusteroptionen." + }, + { + "file": "rs-skip-paths.txt", + "content": "Die endgültige RS_SKIP_PATHS-Liste blieb bestehen, so dass apply_pending_restore.sh nach dem Neustart die gleichen Ausschlüsse anwendet, die während des interaktiven Schritts berechnet wurden. Verhindert, dass driftbeeinflusste oder kernübergreifend unsichere Pfade beim Booten wiederhergestellt werden." + }, + { + "file": "rootfs/", + "content": "Die inszenierten Dateien selbst - die genauen Bytes, die festgelegt werden. Behielt auf dem gleichen Dateisystem wie /, so dass der On-Boot rsync schnell ist und nicht davon abhängt, dass externer Speicher beim Booten noch erreichbar ist." + } + ], + "unitBody": "Die systemd-Einheit proxmenux-restore-onboot.service wird am Ende des interaktiven Schritts aktiviert. Der einmalige Dienst startet früh beim nächsten Boot, ruft apply_pending_restore.sh auf und deaktiviert sich anschließend. Die Bedingung ConditionPathExists=/var/lib/proxmenux/pending-restore/state sorgt dafür, dass er bei Starts ohne vorgemerkte Wiederherstellung nichts ausführt." + }, + "postbootDispatcher": { + "heading": "Der Postboot-Dispatcher — apply_cluster_postboot.sh", + "intro": "Wo der interaktive Schritt endet und wo die eigentliche Host-Reproduktion stattfindet. apply_cluster_postboot.sh wird als zweites One-Shot-systemd-Dienst (proxmenux-apply-cluster-postboot.service) installiert, dessen After=/Wants=-Ziele sicherstellen, dass es nur nach dem Ausführen von Pve-Cluster und Netzwerk-Online ausgeführt wird. Hier landen die host-visiblen Wiederherstellungsaktionen.", + "tasks": [ + { + "task": "Anwendung von /etc/pve", + "detail": "/etc/pve ist ein Live-Pmxcfs FUSE-Mount - es kann nicht beim frühen Booten geschrieben werden. Der Dispatcher kopiert Dateien von den ausstehenden Rootfs in das Live-/etc/pve, sobald das Cluster-Dateisystem aktiviert ist, eine Datei nach der anderen, ohne den pve-Cluster neu zu starten. Das erfasste /var/lib/pve-cluster/config.db aus dem Backup (oder das geförderte config.db.raw-fallback) wird mit dem systemctl stop pve-cluster → Atomdateikopie → systemctl start pve-cluster Muster materialisiert, so dass pmxcfs den wiederhergestellten Zustand auf dem Weg zurück nach oben erneut liest." + }, + { + "task": "Rebuild Initramfs und Bootloader", + "detail": "Führt update-initramfs -u -k all über jeden installierten Kernel aus, dann entweder update-grub (GRUB-Installationen) oder proxmox-boot-tool refresh (systemd-Boot / ZFS-Installationen). Überspringt, wenn der interaktive Schritt feststellte, dass sich nichts an den Pfaden geändert hat, die sich auf diese Werkzeuge auswirken." + }, + { + "task": "Auto-Reinstallation der Komponente", + "detail": "Liest die wiederhergestellte components_status.json und durchläuft die unterstützten Installationsskripte (nvidia_driver, coral_driver, amdgpu_top, intel_gpu_tools). Jedes ausgewählte Skript läuft mit --auto-reinstall, liest den erfassten Zustand und installiert für die Zielumgebung neu, einschließlich nötiger Kernelmodule. Dies erfolgt nach dem Neustart, damit der dann laufende Zielkernel verwendet wird. Einzelfehler werden protokolliert und stoppen die nächste Komponente nicht." + }, + { + "task": "Prüfung der Startkonfiguration", + "detail": "Bevor Sie die Abschlussbenachrichtigung starten, überprüfen Sie, ob proxmox-boot-tool status ein konfiguriertes ESP anzeigt, jedes /boot/vmlinuz-* ein passendes /lib/modules/<ver>-Verzeichnis hat und /vmlinuz auflöst. Jede inkonsistenz wird in der benachrichtigung aufgetaucht, anstatt versteckt zu werden." + }, + { + "task": "Abschlussmeldung", + "detail": "Sendet das Host restore finished-Event über hb_notify_lifecycle. Enthält die Gesamtdauer, die Anzahl der angewandten Pfade, gegebenenfalls Warnungen vor der Sanitätsprüfung und einen Link zum Protokoll bei /var/log/proxmenux/proxmenux-cluster-postboot-*.log. Wenn Benachrichtigungen nicht konfiguriert sind, ist das Ereignis still – das Protokoll zeichnet immer noch alles auf." + } + ] + }, + "liveProgress": { + "heading": "Live-Fortschritt auf der Registerkarte Backups des Monitors", + "body": "Nach dem Neustart wird auf der Registerkarte Backups von ProxMenux Monitor eine Live-Post-Restore-Karte angezeigt, die den Status von proxmenux-apply-cluster-postboot.service während der Ausführung widerspiegelt. Die Karte wird von /var/lib/proxmenux/restore-state.json gesteuert, die der Dispatcher bei jedem Meilenstein aktualisiert (Cluster-Konfiguration anwenden, Initramfs neu erstellen, Bootloader aktualisieren, pro Komponente neu installieren, Boot-Sanity-Check, finalisieren).", + "fields": [ + { + "name": "Statusabzeichen", + "detail": "Einer der Zustände Wiederherstellung läuft, Wiederherstellung abgeschlossen oder Wiederherstellung fehlgeschlagen. Die Karte fragt die Statusdatei während der Ausführung alle 2 Sekunden und danach alle 30 Sekunden ab." + }, + { + "name": "Fortschrittsbalken + Schrittzähler", + "detail": "Zeigt N/M steps mit dem Label des aktuellen Schritts an. Während die Wiederherstellung ausgeführt wird, wird eine geschätzte Zeit aus abgeschlossenen Schritten und verstrichener Zeit abgeleitet." + }, + { + "name": "Komponentenliste", + "detail": "Eine Zeile pro Treiber wird neu installiert (NVIDIA, Intel GPU tools, AMD tools, Coral TPU) mit installing / ok / failed plus einem Link zum Pro-Komponenten-Log bei /var/log/proxmenux/component-*.log." + }, + { + "name": "Import von Datenpools", + "detail": "Der Block data_pools_import zeigt fünf farbige Zeilen: Importiert, Erzwungen (mit -f wegen fremdem hostid), Unvollständig übersprungen (nur einige Datenträger vorhanden), Fehlend übersprungen (keine Datenträger vorhanden) und Fehlgeschlagen. Der Zustand liegt in /var/lib/proxmenux/restore-state.json, das vollständige Laufprotokoll in /var/log/proxmenux/restore-datapools-<timestamp>.log." + }, + { + "name": "Warnungen zur Boot-Sicherheit", + "detail": "Der Dispatcher prüft auf fehlende /lib/modules/<kernel>, fehlende ESP-Konfiguration und baumelnde /vmlinuz-Symlinks. Jede Warnung taucht hier als farbiges Banner auf." + }, + { + "name": "Rollback-Delta", + "detail": "Listen Sie VMs, LXCs und Komponenten auf, die auf dem Ziel vorhanden sind, aber nicht im wiederhergestellten Backup waren - die gleichen Einträge tauchten im destruktiven Rollback-Dialog vor der Wiederherstellung auf. Jede Zeile trägt den manuellen Bereinigungsbefehl." + }, + { + "name": "Letzte Protokollzeilen", + "detail": "Die letzten 600-Zeilen von proxmenux-cluster-postboot-<ts>.log, mit einem Nur Probleme Filter, der nur Zeilen hält, die mit error, warning, failed, ✗ oder traceback übereinstimmen." + } + ], + "dismissBody": "Ist die Wiederherstellung abgeschlossen oder fehlgeschlagen, klappt Ausblenden die Karte zu. Die Statusdatei bleibt erhalten; Verlauf öffnet sie und frühere Wiederherstellungen erneut. Die letzten 20 Läufe liegen unter /var/lib/proxmenux/restore-history/.", + "imageAlt": "Detaildialog des Wiederherstellungsfortschritts im ProxMenux Monitor während einer laufenden Wiederherstellung: blaues Statuszeichen, aktueller Schritt „Initramfs neu erstellen“, geschätzte Restzeit, teilweise gefüllter Fortschrittsbalken und aktuelle Protokollzeilen.", + "imageCaption": "Die Details modal, während der Post-Boot-Dispatcher noch läuft - Abzeichen, aktueller Schritt, verbleibende Zeit und Live-Log-Tail.", + "detailsImageAlt": "Die gleichen Details modal, nachdem die Wiederherstellung abgeschlossen ist, mit dem grünen Wiederherstellungsabzeichen, der Gesamtlaufdauer (0m53s), einem vollständig gefüllten 6 / 6-Fortschrittsbalken, dem leeren Rollback-Delta und dem archivierten Postboot-Protokoll.", + "detailsImageCaption": "Das gleiche Modal, wenn der Lauf beendet ist - Abzeichen, Gesamtdauer, gefüllter Fortschrittsbalken und das archivierte Postboot-Protokoll." + }, + "postbootExample": { + "heading": "Wie der Post-Boot auf der Konsole aussieht", + "body": "Auf der physischen Host-Konsole endet ein erfolgreich beendeter Dispatcher-Prozess mit [ OK ] Finished proxmenux-apply-cluster-postboot.service, oft neben den OK-Meldungen für multi-user.target und graphical.target. Dies markiert das Prozessende. Einzelne Komponentenfehler oder Startwarnungen können trotzdem vorliegen. Prüfen Sie daher Komponentenstatus und Protokolle im Monitor, bevor Sie den Host als vollständig wiederhergestellt betrachten.", + "imageAlt": "Physische Proxmox-Konsole mit '[ OK ] Finished proxmenux-apply-cluster-postboot.service - ProxMenux Apply Cluster Configs (post-boot).', gefolgt von OK für multi-user.target und graphical.target; darüber die Host-Anmeldeaufforderung.", + "imageCaption": "Physische Konsole nach Prozessende. 'Finished proxmenux-apply-cluster-postboot.service' markiert den Abschluss, den auch 'Host restore finished' im Monitor meldet. Komponentenergebnisse und Warnungen müssen weiterhin geprüft werden." + }, + "tenMinutes": { + "heading": "Warum die letzten zehn Minuten wichtig sind", + "intro": "Der Neustart selbst dauert Sekunden, aber der Post-Boot-Dispatcher verbringt etwa zehn Minuten damit, die Wiederherstellung abzuschließen. Während dieses Fensters ist der Host erreichbar und die Anmeldung funktioniert, aber einige Dienste (hauptsächlich von GPU-Treibern abhängige) sind noch nicht verfügbar. Zeitaufschlüsselung für einen typischen Proxmox-Host mit NVIDIA + Coral installiert:", + "rows": [ + { + "stage": "Boot + pve-Cluster bereit", + "time": "~30 s", + "detail": "Standard Proxmox Boot. SSH, Web-UI und Login sind am Ende dieser Phase verfügbar." + }, + { + "stage": "/etc/pve auf pmxcfs anwenden", + "time": "~10 s", + "detail": "Schnell – kleine Konfigurationsdateien, die nacheinander in das Live-Cluster-Dateisystem kopiert wurden." + }, + { + "stage": "Initramfs über Kernel hinweg neu aufbauen", + "time": "3–5 min", + "detail": "update-initramfs -u -k all stellt pro installiertem Kernel ein Initramfs-Image wieder her. Masse des Wartens." + }, + { + "stage": "Neuaufbau der Bootloader-Konfiguration", + "time": "10–30 s", + "detail": "update-grub oder proxmox-boot-tool refresh. Schnell sogar auf ZFS." + }, + { + "stage": "NVIDIA-Treiber neu installieren (falls zutreffend)", + "time": "5–10 min", + "detail": "Laden Sie die aufgezeichnete Treiberversion herunter, kompilieren Sie die DKMS-Module mit dem aktuellen Kernel und wenden Sie den ProxMenux-Patch an, wenn die Quelle ihn hatte. Längste Einzelaufgabe." + }, + { + "stage": "Neuinstallation von Coral und anderen Komponenten", + "time": "3–5 min", + "detail": "DKMS-Kompilierung für Coral, apt install für intel-gpu-tools und .deb-Download mit Installation für amdgpu_top." + }, + { + "stage": "Startprüfung und Benachrichtigung", + "time": "~2 s", + "detail": "Geringer Aufwand. Die Benachrichtigung meldet dem Benutzer unmittelbar den Abschluss des Laufs." + } + ], + "outroBody": "Während dieser Zeit können nach der Anmeldung Werkzeuge fehlen (nvidia-smi nicht gefunden, coral nicht erkannt), weil die Neuinstallation im Hintergrund läuft. Die Abschlussmeldung markiert ihr Ende. Prüfen Sie Komponentenergebnisse, Warnungen und Protokolle auf einzelne Fehler." + }, + "destructiveRollback": { + "heading": "Der optionale destruktive Rollback", + "body": "Die Wiederherstellung ist standardmäßig additiv: Zusätzliche VMs, LXCs und Komponenten bleiben erhalten. Bei Unterschieden bietet der Dialog Destruktiven Rollback ausführen? mit Nein als Vorgabe an. Zustimmung ruft vor dem Neustart _rs_execute_rollback auf, um die aufgelisteten zusätzlichen VMs und LXCs zu löschen. Komponenten werden zur manuellen Deinstallation markiert, da automatische Deinstallation nicht implementiert ist. Fehler beim Löschen von Gästen werden gemeldet; die Verarbeitung läuft weiter. Eine exakte Übereinstimmung mit dem Backup wird nicht garantiert. Prüfen Sie vor dieser destruktiven Wahl Namen und VMIDs/CTIDs." + }, + "logs": { + "heading": "Speicherorte der Protokolle", + "rows": [ + { + "log": "/var/log/proxmenux/restore-YYYYMMDD_HHMMSS.log", + "detail": "Vom interaktiven Schritt geschrieben. Enthält Kompatibilitätsergebnisse, Filter für Abweichungen, Anpassungsplan, Ausgabe der direkten Anwendung und Vorbereitung vorgemerkter Änderungen sowie Paketinstallation." + }, + { + "log": "/var/log/proxmenux/apply-pending-YYYYMMDD_HHMMSS.log", + "detail": "Von apply_pending_restore.sh beim frühen Start geschrieben. Enthält das Einlesen von plan.env, das Abarbeiten von apply-on-boot und die Pfadausschlüsse." + }, + { + "log": "/var/log/proxmenux/proxmenux-cluster-postboot-YYYYMMDD_HHMMSS.log", + "detail": "Geschrieben von apply_cluster_postboot.sh. Enthält: pve-cluster apply, Initramfs und Bootloader-Ausgabe, Ausgabe pro Komponente Installateur, Sanity-Check, Benachrichtigung Nutzlast." + }, + { + "log": "/var/log/proxmenux/component--YYYYMMDD_HHMMSS.log", + "detail": "Ein Protokoll pro Komponenteninstallationsprogramm (nvidia, coral usw.), das im Post-Boot-Durchlauf ausgeführt wurde. Nützlich, wenn die Neuinstallation einer bestimmten Komponente fehlgeschlagen ist und der Benutzer die eigene Ausgabe des Tools benötigt." + }, + { + "log": "/var/log/proxmenux/restore-datapools-YYYYMMDD_HHMMSS.log", + "detail": "Geschrieben von _rs_import_data_pools. Enthält: Aufzählung von Nicht-Root-Pools aus dem Manifest, Pro-Pool-Ergebnis (sauber importiert, mit -f für ausländische Hostids importiert, aufgrund fehlender Festplatten übersprungen oder fehlgeschlagen) und die Rohausgabe jedes zpool import-Aufrufs." + } + ] + }, + "whereNext": { + "heading": "Wohin Sie als nächstes gehen", + "items": [ + { + "label": "Kernübergreifende Wiederherstellung", + "href": "/docs/backup-restore/cross-kernel", + "tail": "- der richtungsbewusste Safe-Subset-Filter und der Hydratationspass, der aufgerufen wird, wenn sich der Zielkernel vom Backup unterscheidet." + }, + { + "label": "Wie es funktioniert", + "href": "/docs/backup-restore/how-it-works", + "tail": "- das Archivlayout, Manifest und Anwendungsinventar, das die Wiederherstellung verbraucht." + }, + { + "label": "Geplante Aufträge", + "href": "/docs/backup-restore/scheduled-jobs", + "tail": "— der unbeaufsichtigte Fluss, der die Archive produziert, die diese Seite verbraucht." + } + ] + } +} diff --git a/web/messages/de/docs/backup-restore/scheduled-jobs.json b/web/messages/de/docs/backup-restore/scheduled-jobs.json new file mode 100644 index 00000000..dbe92ef5 --- /dev/null +++ b/web/messages/de/docs/backup-restore/scheduled-jobs.json @@ -0,0 +1,152 @@ +{ + "meta": { + "title": "Geplante Aufträge — Timer, Attachment-Modus, Retention | ProxMenux", + "description": "Geplante ProxMenux Host-Backup-Jobs. Zwei Erstellungsmodi (eigener systemd-Timer oder Attachment zu einem vorhandenen PVE vzdump-Job), Retentionswerte, die pro Job über proxmox-backup-client/borg/local prune angewendet werden, Storage-Layout unter /var/lib/proxmenux/backup-jobs und das Runner-Script, das Archive erzeugt, die mit dem interaktiven Fluss identisch sind.", + "ogTitle": "ProxMenux Backup — geplante Aufträge", + "ogDescription": "Unbeaufsichtigte geplante Host-Backups mit Attachment-Modus, Retention und den gleichen drei Zielen wie der interaktive Fluss.", + "twitterTitle": "Geplante Jobs | ProxMenux", + "twitterDescription": "Unbeaufsichtigte geplante Host-Backup-Jobs mit Attach-Modus und Retention." + }, + "header": { + "title": "Geplante Aufträge", + "description": "Unbeaufsichtigte Host-Backup-Jobs. Zwei Erstellungsmodelle: ein neuer unabhängiger Job mit einem eigenen Zeitplan oder ein Job, der an eine bestehende PVE vzdump-Aufgabe angehängt ist, die den Zeitplan und die Aufbewahrung dieser Aufgabe erbt. Beide produzieren Archive, die mit dem interaktiven Fluss identisch sind.", + "section": "Sicherung und Wiederherstellung" + }, + "intro": { + "title": "Die beiden planmäßigen Job-Modelle", + "body": "Ein geplanter Job kann nach einem von zwei Modellen erstellt werden:", + "modelsList": [ + "Neuer unabhängiger job — ProxMenux definiert den Jobplan selbst mit einem eigenen systemd-Timer, unabhängig von anderen Aufgaben auf dem Host. Kompatibel mit allen drei Destinationen: Local, PBS und Borg.", + "An eine bestehende PVE vzdump-Task anhängen – der Job hat keinen eigenen Zeitplan; er wird automatisch ausgeführt, wenn die übergeordnete PVE vzdump-Task, die bereits die VMs und LXCs auf diesem Host sichert, ausgelöst wird. Erbt den Zeitplan und die Beibehaltung von der übergeordneten Aufgabe. Kompatibel mit Local und PBS (Borg wird nicht unterstützt, da PVE keinen nativen Scheduler für Borg hat)." + ] + }, + "attachBadge": { + "title": "Attach-Modus — empfohlen, wenn bereits eine PVE vzdump-Task existiert", + "body": "Wenn bereits ein PVE-vzdump-Auftrag für die VMs und LXCs dieses Knotens eingerichtet ist, wird das angehängte Host-Backup nach dem Gast-Backup im selben Sicherungszyklus ausgeführt. Bei der Wiederherstellung stammen die Gastkonfigurationen aus dem Host-Backup (unter /etc/pve) und die Gastfestplatten aus den vzdump-Backups dieses Zyklus. Dies erleichtert eine koordinierte Wiederherstellung des gesamten Knotens, ist aber kein atomarer Snapshot: Änderungen während des Zyklus können Unterschiede zwischen Hostkonfiguration und Gastfestplattenzustand hinterlassen." + }, + "frequencyBadge": { + "title": "Empfohlene Häufigkeit", + "body": "Um die Kompatibilität zwischen einem Host-Backup und seiner zukünftigen Wiederherstellung zu maximieren, führen Sie häufig Host-Backups aus. Je näher das Backup dem aktuellen Zustand des Hosts ist, desto weniger Konfigurations-, Paket- und Komponentendrift muss die Wiederherstellung in Einklang bringen." + }, + "modes": { + "heading": "Die beiden Modi", + "rows": [ + { + "mode": "Neuer geplanter Auftrag", + "backends": "Lokal, PBS, Borg", + "schedule": "Eigener OnCalendar-Ausdruck (systemd-Kalendersyntax, z. B. daily oder Mon..Fri 03:00).", + "retention": "Separat abgefragt: keep-last, keep-hourly, keep-daily, keep-weekly, keep-monthly, keep-yearly. Wird vom Ausführungsskript nach jedem erfolgreichen Backup angewendet." + }, + { + "mode": "Anhängen an einen PVE vzdump Job", + "backends": "Lokal, PBS (Borg hat keinen PVE-seitigen Scheduler)", + "schedule": "Vererbt vom Eltern-PVE-Job. Auf der ProxMenux-Seite ist kein systemd-Timer installiert.", + "retention": "Aus der prune-backups-Konfiguration des übergeordneten Auftrags übernommen (über hb_pve_prune_to_keep_env eins zu eins den KEEP_*-Variablen des Ausführungsskripts zugeordnet)." + } + ] + }, + "attachDetail": { + "heading": "Wie Attach Mode funktioniert", + "intro": "PVE speichert vzdump-Aufträge in /etc/pve/jobs.cfg — einen Abschnitt pro Auftrag mit dem Speicherziel für VM- und LXC-Backups. Der Attach-Modus erfordert ein von ProxMenux unterstütztes Backend (Lokal oder PBS). Das Host-Backup wird nach Abschluss des Gast-Backups über den job-end-Hook ausgeführt.", + "steps": [ + { + "step": "1", + "detail": "Während der Auftragserstellung listet ProxMenux kompatible übergeordnete PVE-Aufgaben über hb_pve_list_vzdump_jobs_for_backend auf. Der Benutzer wählt einen aus." + }, + { + "step": "2", + "detail": "Der .env des Jobs wird mit PVE_PARENT_JOB, PVE_STORAGE und den geerbten KEEP_*-Werten geschrieben; es wird kein systemd-Timer erstellt." + }, + { + "step": "3", + "detail": "hb_install_vzdump_hook registriert einen Skript-Hook in /etc/vzdump.conf. In der job-end-Phase jedes vzdump-Auftrags vergleicht er den übergebenen $STOREID mit PVE_STORAGE aller aktivierten angehängten Aufträge und startet das Ausführungsskript bei Übereinstimmung. Der Abgleich erfolgt nach Speicher, nicht nach der ID des gewählten übergeordneten Auftrags." + }, + { + "step": "4", + "detail": "Das Archiv landet im selben Speicher, den die vzdump-Dumps gerade geschrieben haben: path/dump/ für Local oder das PBS-Repository, das auf dem PVE-Speichereintrag konfiguriert ist." + } + ], + "outroBody": "Der Attach-Modus hat keinen eigenen Timer. Das Deaktivieren oder Löschen des gewählten übergeordneten PVE-Auftrags stoppt dessen Auslösung; andere vzdump-Aufträge mit demselben Speicher können das Host-Backup weiterhin auslösen. Deaktivieren oder löschen Sie den ProxMenux-Auftrag selbst, um dies zu verhindern. Beim Deaktivieren bleibt der Eintrag auf der Festplatte und kann später erneut aktiviert oder angehängt werden." + }, + "storageLayout": { + "heading": "Dateien, die einen Job ausmachen", + "intro": "Jeder geplante Job wird vollständig durch drei oder vier Dateien auf der Festplatte beschrieben. Das Lesen gibt die vollständige Konfiguration des Jobs, ohne sich auf die Benutzeroberfläche zu verlassen.", + "rows": [ + { + "path": "/var/lib/proxmenux/backup-jobs/JOB_ID.env", + "content": "Backend, Backup-ID oder Ziel, Zeitplan (Neuer Auftrag) oder übergeordneter PVE-Auftrag (Attach-Modus), Aktivierungsstatus, Aufbewahrungswerte KEEP_* und Verweise auf Zugangsdaten." + }, + { + "path": "/var/lib/proxmenux/backup-jobs/JOB_ID.paths", + "content": "Ein absoluter Pfad pro Zeile - die eingefrorene Auswahl wurde zum Zeitpunkt der Joberstellung aus dem Profil aufgelöst. Das Bearbeiten der Datei führt den Job mit der neuen Auswahl beim nächsten Auslösen des Timers erneut aus." + }, + { + "path": "/etc/systemd/system/proxmenux-backup-JOB_ID.timer + .service", + "content": "Nur neuer Modus. Der Dienst ruft run_scheduled_backup.sh JOB_ID auf. Der Timer plant es mit Persistent=true (verpasste Ausführungen werden beim nächsten Boot nachgeholt) und einem kleinen RandomizedDelaySec=120, um die Last zu verteilen, wenn mehrere Jobs den gleichen OnCalendar-Ausdruck teilen." + }, + { + "path": "/etc/vzdump.conf script-hook", + "content": "Nur Attach-Modus. Einmal durch hb_install_vzdump_hook installiert; vergleicht PVE_STORAGE jedes angehängten Auftrags mit $STOREID, das PVE dem Hook-Skript übergibt." + } + ] + }, + "runner": { + "heading": "Was beim Start eines Auftrags geschieht", + "intro": "Jeder Job - ob New-Model oder Attachment - führt die gleiche dreistufige Sequenz aus:", + "steps": [ + "Backup vorbereiten. Liest die Job-Konfiguration (Ziel, Profil, Pfade) und stellt den Archivbaum genau so zusammen, wie es der interaktive Fluss tut. Das Ergebnis ist das gleiche Archiv, das ein manuelles Backup mit diesen Einstellungen erzeugen würde.", + "An das Ziel schreiben. Überträgt oder schreibt das Archiv in das konfigurierte Ziel – eine lokale .tar.zst-Datei, ein PBS-Backup oder ein Borg-Archiv – mit genau den gleichen Tools und Anmeldeinformationen, die ein manuelles Backup für dasselbe Ziel verwenden würde.", + "Aufbewahrungsregeln anwenden. Entfernt alte Backups vom Zielort, wobei die Anzahl der keep-last / keep-daily / keep-weekly für den Auftrag konfiguriert bleibt. Das Beschneiden wird vom Zielort selbst durchgeführt: PBS wendet seine eigene Aufbewahrung an, Borg führt borg prune aus und bei Lokal löscht ProxMenux die Dateien, die außerhalb des keep-last-Fensters liegen." + ] + }, + "management": { + "heading": "Aufträge verwalten", + "intro": "Das Scheduler-Menü (Scripts TUI) und die Registerkarte Monitor Backups zeigen die gleichen Aktionen bei jedem Job.", + "rows": [ + { + "action": "Jetzt ausführen", + "detail": "Ruft das Ausführungsskript sofort auf und umgeht Timer bzw. vzdump-Hook. Damit lässt sich die Auftragskonfiguration prüfen, ohne auf die nächste geplante Ausführung zu warten." + }, + { + "action": "Aktivieren/Deaktivieren", + "detail": "Neuer Modus: systemctl enable/disable --now der Timer. Attach-Modus: Dreht das ENABLED-Flag im Job Env um — das Hook-Skript berücksichtigt es beim nächsten PVE-Aufruf." + }, + { + "action": "Bearbeiten", + "detail": "Öffnet Ziel, Zeitplan, Profil und Aufbewahrungsaufforderungen neu und schreibt die Jobdateien neu. Bewahrt die Job ID auf." + }, + { + "action": "Löschen", + "detail": "Entfernt die Job env, Pfaddatei, systemd Timer + Service (Neuer Modus) und deaktiviert die Hakenbindung (Attach-Modus). Berührt keine Archive bereits am Zielort." + }, + { + "action": "Protokoll anzeigen", + "detail": "Zeigt laufend an: /var/log/proxmenux/backup-jobs/JOB_ID-YYYYMMDD_HHMMSS.log – eine Protokolldatei pro Lauf. Der Runner gibt außerdem eine kompakte Statuszeile an journald unter dem Dienst systemd aus." + } + ] + }, + "notifications": { + "heading": "Benachrichtigungen", + "body": "Jeder geplante Lauf feuert die gleichen hb_notify_lifecycle-Ereignisse ab wie der interaktive Fluss (start, complete, fail). Wenn Benachrichtigungskanäle im Monitor konfiguriert sind, tauchen unbeaufsichtigte Jobs auf die gleiche Weise wie manuelle Backups auf - ein Benutzer muss das Protokoll nicht überprüfen, um zu wissen, ob ein Job erfolgreich war." + }, + "whereNext": { + "heading": "Wohin Sie als nächstes gehen", + "items": [ + { + "label": "Backups erstellen", + "href": "/docs/backup-restore/creating-backups", + "tail": "- der interaktive Fluss, der das Backend mit dem Scheduler teilt." + }, + { + "label": "Ziele", + "href": "/docs/backup-restore/destinations", + "tail": "Per-Backend-Konfiguration, die sowohl vom interaktiven Flow als auch vom Scheduler verwendet wird." + }, + { + "label": "Wiederherstellung", + "href": "/docs/backup-restore/restoring", + "tail": " — der Ablauf zur Wiederherstellung der von den Aufträgen erzeugten Backups." + } + ] + } +} diff --git a/web/messages/de/docs/create-vm/index.json b/web/messages/de/docs/create-vm/index.json new file mode 100644 index 00000000..6b7a4a98 --- /dev/null +++ b/web/messages/de/docs/create-vm/index.json @@ -0,0 +1,175 @@ +{ + "meta": { + "title": "VMs auf Proxmox VE erstellen: Synology, Windows, Linux und NAS-Assistenten | ProxMenux", + "description": "Erstellen und konfigurieren Sie VMs auf Proxmox VE mit ProxMenux. Assistenten für NAS-Systeme (Synology DSM, TrueNAS, OpenMediaVault, XigmaNAS usw.), Windows, Linux-Distributionen und ein externer macOS-Installer. Sinnvolle Voreinstellungen für Hardware, Speicher und Netzwerk.", + "ogTitle": "VMs auf Proxmox VE erstellen: Assistenten für Synology, Windows, Linux und NAS", + "ogDescription": "ProxMenux-Assistenten für VMs mit Synology DSM, TrueNAS, OMV, XigmaNAS, Windows und Linux auf Proxmox VE.", + "ogImageAlt": "ProxMenux-Menü „VM erstellen“", + "twitterTitle": "Erstellen von VMs auf Proxmox VE | ProxMenux", + "twitterDescription": "Assistenten für VMs mit Synology, TrueNAS, OMV, XigmaNAS, Windows und Linux auf Proxmox VE." + }, + "header": { + "title": "VM erstellen", + "description": "ProxMenux gruppiert die VM-Erstellung zu einem interaktiven Dispatcher. Wählen Sie die Betriebssystemfamilie und der passende Wizard übernimmt ISO-Auswahl, CPU, RAM, BIOS, Netzwerk, Speicher und - optional - GPU Passthrough.", + "section": "Virtuelle Maschinen" + }, + "intro": { + "title": "Wofür dieses Menü ist", + "body": "Das Menü „VM erstellen“ verzweigt zu mehreren Assistenten. Es fragt nach der Betriebssystemfamilie und lädt ISO-Auswahl und Konfigurator. Der Standardablauf nutzt sinnvolle ProxMenux-Voreinstellungen; der erweiterte Ablauf zeigt alle verfügbaren Optionen vor der Erstellung." + }, + "opening": { + "heading": "Öffnen des Menüs", + "body": "Wählen Sie im ProxMenux-Hauptmenü VM erstellen. Es erscheint:", + "imageAlt": "Menü zur VM-Erstellung mit drei ProxMenux-Optionen (NAS/Windows/Linux) und Community Scripts darunter (macOS/Andere)" + }, + "families": { + "heading": "Drei OS-Familien", + "intro": "Die drei ProxMenux-Einträge oben im Menü teilen sich die gleiche zugrunde liegende Engine und den gleichen Post-ISO-Flow - sie leiten nur zu verschiedenen ISO-Selektoren und wenden Standardwerte an, die auf jede Familie abgestimmt sind. Wählen Sie diejenige, die zu dem Gast passt, den Sie installieren möchten.", + "routes": [ + { + "key": "nas", + "title": "NAS-System", + "icon": "HardDrive", + "href": "/docs/create-vm/system-nas", + "accent": "border-emerald-300 bg-emerald-50", + "iconBg": "bg-emerald-100 text-emerald-700", + "description": "Acht NAS-Betriebssysteme als eigene VMs. Die meisten nutzen den automatischen ISO-Ablauf; Synology DSM verwendet den spezialisierten Loader-Assistenten.", + "bullets": [ + "Synology DSM (Arc / RR / Alpine RedPill M-shell Loader)", + "TrueNAS SCALE und CORE (ZFS)", + "OpenMediaVault (Debian-basiert)", + "XigmaNAS (FreeBSD ZFS)", + "Rockstor (openSUSE / Btrfs), ZimaOS, FygoOS" + ] + }, + { + "key": "windows", + "title": "Windows-System", + "icon": "MonitorCog", + "href": "/docs/create-vm/system-windows", + "accent": "border-blue-300 bg-blue-50", + "iconBg": "bg-blue-100 text-blue-700", + "description": "Windows 10, 11 und Server-Editionen mit Windows-freundlichen Standardeinstellungen (q35 + OVMF + TPM 2.0) und VirtIO Treiber vormontiert.", + "bullets": [ + "UUP Dump für aktuelle ISOs", + "Oder lokale ISO von /var/lib/vz/template/iso", + "TPM 2.0 automatisch hinzugefügt", + "VirtIO Treiber ISO angehängt an ide3", + "Gastagent-Kanal aktiviert" + ] + }, + { + "key": "linux", + "title": "Linux-System", + "icon": "Laptop", + "href": "/docs/create-vm/system-linux", + "accent": "border-amber-300 bg-amber-50", + "iconBg": "bg-amber-100 text-amber-700", + "description": "Kuratierter Katalog offizieller Linux-ISOs, die automatisch aus dem Upstream heruntergeladen werden, sowie ein lokaler ISO-Picker für alles andere.", + "bullets": [ + "Ubuntu (Desktop / Server, 20.04–25.10)", + "Debian (Desktop / Netinst, 11–13)", + "Fedora, Arch, Rocky, Mint, openSUSE", + "Alpine, Kali, Manjaro", + "Lokaler ISO Pfad für benutzerdefinierte Images" + ] + } + ] + }, + "community": { + "title": "Community-Scripts (nicht hier dokumentiert)", + "intro": "Die beiden Einträge unter dem Community Scripts-Separator führen Code aus, der nicht Teil von ProxMenux ist, so dass sie absichtlich außerhalb dieser Dokumentation bleiben:", + "macosRich": "macOS-System: führt den externen Installer OSX-PROXMOX aus. Er klont das Originalrepository, bereitet den Host vor und startet ihn nach Abschluss automatisch neu. Beachten Sie die Dokumentation dieses Projekts.", + "othersRich": "System Sonstige (Linux-basiert) lädt eine kuratierte Liste von Linux-basierten Installateuren der Community (Home-Server-Betriebssysteme, Router-/Firewall-Appliances usw.). Jedes Community-Script wird außerhalb von ProxMenux gepflegt." + }, + "afterPick": { + "heading": "Was passiert, nachdem Sie ein OS ausgewählt haben", + "intro": "Sobald die ISO ausgewählt ist, nimmt der Dispatcher den gleichen Weg unabhängig von der OS-Familie:", + "items": [ + "Standard oder Erweitert. Bestätigen Sie die ProxMenux-Vorgaben oder öffnen Sie den vollständigen Konfigurator für CPU-Modell und Kerne, RAM, BIOS/UEFI, Netzwerkkartenmodell und Maschinentyp.", + "Speicherplan. Kombinieren Sie virtuelle Laufwerke, importierte Laufwerke und PCI-Passthrough-Geräte wie ganze Controller oder NVMe in derselben VM.", + "Optionales GPU-Passthrough. Wird eine geeignete GPU erkannt, bietet ProxMenux nach der VM-Erstellung den GPU-Assistenten an. Ein Neustart kann erforderlich sein.", + "VM-Erstellung. Die VM wird erstellt, getaggt und mit einer formatierten HTML-Beschreibung für schnellen Zugriff in der Proxmox-Oberfläche versehen." + ], + "tipTitle": "Bevorzugt Advanced für VMs, die Sie behalten möchten", + "tipBody": "Default Presets funktionieren gut für schnelle Tests. Für einen VM, den Sie behalten möchten, können Sie mit dem Advanced-Pfad den richtigen BIOS (SeaBIOS vs OVMF/UEFI), den Maschinentyp (i440fx vs q35) und den Festplattencontroller (VirtIO SCSI single vs SATA) im Voraus auswählen - diese müssen später schmerzhaft geändert werden." + }, + "scripts": { + "heading": "Involvierte Skripte", + "intro": "Der Einstieg lädt mehrere gemeinsame Komponentenskripte per source, für die es keine eigene Dokumentationsseite gibt. Sie sind hier als Referenz aufgelistet. Jede Unterseite verlinkt das Hauptskript ihres Ablaufs.", + "headerScript": "Skript", + "headerRole": "Rolle", + "rows": [ + { + "path": "menus/create_vm_menu.sh", + "role": "Einstieg und Weiterleitung." + }, + { + "path": "vm/select_nas_iso.sh", + "role": "ISO/Appliance Selektor für NAS-Systeme (Synology, TrueNAS, OMV, Rockstor, XigmaNAS, ZimaOS, FygoOS)." + }, + { + "path": "vm/select_windows_iso.sh", + "role": "Windows ISO Selector. Delegierte an den UUP Dump Builder, wenn nötig." + }, + { + "path": "vm/select_linux_iso.sh", + "role": "Linux ISO-Selektor für den Linux-Flow (Mainstream-Distributionen)." + }, + { + "path": "vm/synology.sh", + "role": "Synology DSM-Flow: holt Loader und DSM-Bild und bereitet das VM-Profil vor." + }, + { + "path": "vm/zimaos.sh", + "role": "ZimaOS flow: lädt das Image herunter und verbindet es mit der VM-Definition." + }, + { + "path": "vm/vm_configurator.sh", + "role": "Standard vs Advanced Wizard: CPU Modell/Kerne, RAM, BIOS, NIC, Maschinentyp." + }, + { + "path": "vm/disk_selector.sh", + "role": "Speicherplan: virtuelle Festplatte, Festplattenimport oder Controller/NVMe Passthrough." + }, + { + "path": "vm/vm_creator.sh", + "role": "Endgültige VM-Erstellung: wendet die Konfiguration an, fügt Speicher an, schreibt die HTML-Beschreibung." + }, + { + "path": "vm/guest_agent_config.sh", + "role": "Optionaler QEMU Guest Agent Setup Hook, der von mehreren Flows verwendet wird." + } + ] + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager", + "label": "Festplattenmanager", + "tail": "Anfügen vorhandener physischer Festplatten oder Importieren von Festplattenabbildern in VMs." + }, + { + "href": "/docs/utils/UUp-Dump-ISO-Creator", + "label": "UUP-Dump-ISO-Erstellung", + "tail": "— erzeugen Sie Windows-Installations-ISOs, die als Boot-Medien verwendet werden können." + }, + { + "href": "/docs/utils/import-vm", + "label": "VM von OVA / OVF importieren", + "tail": "— vorgefertigte VMs von VMware / VirtualBox / ProxMenux-Exporten einbringen." + }, + { + "href": "/docs/hardware/gpu-vm-passthrough", + "label": "GPU zu VM hinzufügen (Passthrough)", + "tail": "- einen GPU in einen frisch erstellten VM zu übergeben." + }, + { + "href": "/docs/help-info/vm-ct-commands", + "label": "VM und CT Verwaltungsbefehle", + "tail": "— qm-Referenz für die CLI-Seite." + } + ] + } +} diff --git a/web/messages/de/docs/create-vm/synology.json b/web/messages/de/docs/create-vm/synology.json new file mode 100644 index 00000000..2c169b84 --- /dev/null +++ b/web/messages/de/docs/create-vm/synology.json @@ -0,0 +1,453 @@ +{ + "title": "Skript zum Erstellen einer Synology-VM", + "stepLabel": "Schritt", + "intro": { + "heading": "Einleitung", + "intro": "ProxMenux bietet ein automatisiertes Skript, das eine virtuelle Maschine (VM) erstellt und konfiguriert, um Synology DSM (DiskStation Manager) auf Proxmox VE zu installieren. Dieses Skript vereinfacht den Prozess, indem es einen der verfügbaren Loader zum VM-Boot herunterlädt und hinzufügt und Ihnen die Option zwischen vier verschiedenen Optionen bietet:", + "loaders": [ + { + "name": "AuxXxilium Arc", + "url": "https://github.com/AuxXxilium/arc", + "extra": "" + }, + { + "name": "RedPill RR", + "url": "https://github.com/RROrg/rr", + "extra": "" + }, + { + "name": "Alpine RedPill M-shell (TinyCore-Projekt)", + "url": "https://github.com/PeterSuh-Q3/tinycore-redpill", + "extra": "" + } + ], + "customLoader": "Benutzerdefinierter Loader - Option zur Verwendung eines benutzerdefinierten Loaders, wenn Sie es vorziehen, Ihre eigene Konfiguration zu ändern oder zu erstellen", + "simplifiesIntro": "Das Skript vereinfacht den VM-Erstellungsprozess durch die folgenden Optionen:", + "simplifies": [ + "Auswahl der Standard- oder erweiterten Konfiguration", + "Konfiguration von CPU, RAM, BIOS und Maschinentyp", + "Virtuelles Laufwerk oder Passthrough eines physischen Laufwerks" + ] + }, + "config": { + "heading": "Standard- und erweiterte Konfiguration", + "intro": "Das Skript bietet zwei Konfigurationsmodi:", + "defaultHeading": "Standardkonfiguration", + "defaultIntro": "Wenn Sie die Standardkonfiguration auswählen, wendet das Skript automatisch die folgenden Werte an:", + "headerParam": "Parameter", + "headerValue": "Standardwert", + "defaultRows": [ + { + "param": "Maschinentyp", + "value": "q35" + }, + { + "param": "BIOS-Typ", + "value": "OVMF (UEFI)" + }, + { + "param": "CPU-Typ", + "value": "Host" + }, + { + "param": "Anzahl der Kerne", + "value": "2" + }, + { + "param": "RAM-Größe", + "value": "4096 MB" + }, + { + "param": "Brücke", + "value": "vmbr0" + }, + { + "param": "MAC-Adresse", + "value": "Automatisch generiert" + }, + { + "param": "VM nach Abschluss starten", + "value": "Nein" + } + ], + "defaultOutro": "Wenn Sie die Konfiguration anpassen möchten, wählen Sie die Option Erweiterte Einstellungen im Menü aus.", + "advancedHeading": "Erweiterte Konfiguration", + "advancedIntro": "Wenn Sie die erweiterte Konfiguration auswählen, können Sie mit dem Skript jeden Parameter anpassen:", + "headerOptions": "Optionen", + "advancedRows": [ + { + "param": "Maschinentyp", + "options": "q35 oder i440fx" + }, + { + "param": "BIOS-Typ", + "options": "OVMF (UEFI) oder SeaBIOS (Legacy)" + }, + { + "param": "CPU-Typ", + "options": "Host oder KVM64" + }, + { + "param": "Anzahl der Kerne", + "options": "Anzahl CPU-Kerne" + }, + { + "param": "RAM-Größe", + "options": "Der VM zugewiesene Speichermenge" + }, + { + "param": "Brücke", + "options": "Netzwerkbrücke für die Verbindung" + }, + { + "param": "MAC-Adresse", + "options": "Eigene MAC-Adresse" + }, + { + "param": "VLAN", + "options": "VLAN-Tag (falls verwendet)" + }, + { + "param": "MTU", + "options": "Maximale Übertragungseinheit" + } + ] + }, + "diskSelection": { + "heading": "Auswahl der Datenträger", + "intro": "Im Speicherplan können Sie virtuelle und physische Laufwerke sowie PCI-Controller oder NVMe-Geräte hinzufügen. Die Laufwerksoptionen werden unten erläutert.", + "virtualHeading": "Virtuelle Festplatte", + "virtualItems": [ + "Das Skript listet die in Proxmox verfügbaren Speicheroptionen auf", + "Der Benutzer wählt die Festplatte und Größe in GB", + "Virtuelle Laufwerke werden als SATA zugewiesen (sata0, sata1 usw.). Virtuelle und physische Datenlaufwerke teilen die sechs SATA-Anschlüsse 0–5; der Loader nutzt einen eigenen IDE-Anschluss." + ], + "physicalHeading": "Passthrough eines physischen Laufwerks", + "physicalItems": [ + "Das Skript erkennt alle verfügbaren physischen Festplatten", + "Der Benutzer wählt die physische Festplatte oder die Festplatten aus, die er verwenden möchte.", + "Physische Laufwerke werden als SATA-Geräte zugewiesen. Sie teilen die sechs SATA-Anschlüsse 0–5 mit virtuellen Datenlaufwerken; es gibt kein zusätzliches Kontingent von sechs physischen Laufwerken." + ] + }, + "loaderInstall": { + "heading": "Loader installieren", + "intro1": "Das Skript lädt und extrahiert den Loader automatisch aus dem Repository des Entwicklers. Wenn der Download fehlschlägt, zeigt das Skript eine Fehlermeldung an.", + "intro2Rich": "AuxXxilium Arc, RedPill RR und Alpine RedPill M-shell werden automatisch heruntergeladen und entpackt.", + "customRich": "Für Custom Loader sucht das Skript nach Dateien in /var/lib/vz/template/iso. Wenn mehrere Dateien gefunden werden, werden Sie aufgefordert, die gewünschte Datei auszuwählen.", + "uploadIntro": "Sie können benutzerdefinierte Loader über die lokalen Speicheroptionen hochladen:", + "imageAlt": "Custom Loader hinzufügen", + "imageCaption": "Custom Loader hinzufügen" + }, + "vmCreation": { + "heading": "VM erstellen", + "intro": "Sobald der Loader heruntergeladen wurde, erstellt das Skript den VM mit den folgenden Befehlen:", + "items": [ + "qm create – Erstellt die virtuelle Maschine mit den konfigurierten Parametern", + "qm importdisk importiert das Loader-Abbild in den Proxmox-Speicher. Danach weist das Skript es der VM aus Kompatibilitätsgründen als ide0 zu.", + "qm set – Zuweisung von Konfigurationswerten wie CPU, RAM und Speicher", + "qm set VMID --boot order=ide0 legt den Loader als Bootgerät fest. Ersetzen Sie VMID durch die VM-ID." + ] + }, + "stepGuide": { + "heading": "Schrittweise Anleitung zur Loader-Konfiguration", + "intro": "Jeder Loader hat eigene Menüs und Konfigurationsmethoden. Die sechs Schritte unten erklären den gemeinsamen Ablauf; bei Abweichungen folgen Sie den aktuellen Loader-Anweisungen. Die M-shell-Bilder zeigen eine ältere TinyCore-Oberfläche, während das aktuelle Skript Alpine RedPill M-shell herunterlädt.", + "selectorHeading": "Wählen Sie Ihren Loadertyp:", + "loaderButtons": { + "arc": "Arc Loader", + "rr": "RR Loader", + "tinycore": "TinyCore Loader" + } + }, + "steps": [ + { + "id": "step1", + "title": "Starten Sie die VM und öffnen Sie das Hauptmenü", + "intro": "Sobald der VM erstellt wurde, starten Sie ihn. Beim ersten Booten des VM greifen Sie auf das Hauptmenü des Loaders zu, um das DSM-Modell auszuwählen und zu konfigurieren, das Sie erstellen möchten. Sobald der Loader erstellt wurde, wird dieser Schritt übersprungen, es sei denn, Sie erzwingen manuell eine Rekonfiguration vom Bootmonitor. Alle Loader haben auch die Möglichkeit, den Loader über ein Webinterface zu konfigurieren.", + "loaders": { + "arc": [ + { + "htmlBefore": "Web-Schnittstelle, Um auf die Web-Schnittstelle zuzugreifen, öffnen Sie einfach einen Webbrowser und geben Sie die IP-Adresse ein, die in der Konsolenausgabe des VM angezeigt wird. Zum Beispiel in unserem Fall: http://192.169.0.32.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_0_1.png", + "alt": "Arc-Loader-Weboberfläche", + "caption": "Arc-Loader-Weboberfläche" + }, + { + "htmlBefore": "Terminaloberfläche: Öffnen Sie sie direkt in der VM-Konsole.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_1_1.png", + "alt": "Arc-Loader-Terminaloberfläche", + "caption": "Arc-Loader-Terminaloberfläche" + } + ], + "rr": [ + { + "htmlBefore": "Web-Schnittstelle, um auf die Web-Schnittstelle zuzugreifen, öffnen Sie einfach einen Webbrowser und geben Sie die IP-Adresse ein, die in der Konsolenausgabe des VM angezeigt wird, gefolgt von Port 7681. Zum Beispiel in unserem Fall: http://192.169.0.33:7681.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_0_2.png", + "alt": "RR Befehlsbeispiel", + "caption": "RR-Loader-Weboberfläche" + }, + { + "htmlBefore": "Terminaloberfläche: Geben Sie in der VM-Konsole menu.sh ein, um sie zu öffnen.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_1_1.png", + "alt": "Oberfläche von RR Loader", + "caption": "RR-Loader-Terminaloberfläche" + } + ], + "tinycore": [ + { + "htmlBefore": "Web-Schnittstelle, um auf die Web-Schnittstelle zuzugreifen, öffnen Sie einfach einen Webbrowser und geben Sie die IP-Adresse ein, die in der Konsolenausgabe des VM angezeigt wird, gefolgt von Port 7681. Zum Beispiel in unserem Fall: http://192.169.0.35:7681.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_0_1.png", + "alt": "TinyCore-Loader-Oberfläche", + "caption": "TinyCore-Loader-Weboberfläche" + }, + { + "htmlBefore": "Terminaloberfläche: Öffnen Sie sie direkt in der VM-Konsole. Achten Sie auf Aufforderungen, eine Taste zum Fortfahren zu drücken oder die Sprache zu ändern.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_1_1.png", + "alt": "TinyCore-Loader-Oberfläche", + "caption": "Terminaloberfläche von TinyCore Loader" + } + ] + } + }, + { + "id": "step2", + "title": "Modell auswählen", + "intro": "Wählen Sie nach dem Laden des Menüs das Synology DSM-Modell aus, das Sie installieren möchten. Abhängig vom Loader müssen Sie manchmal die Optionen erweitern, um mehr Modelle zu sehen.", + "outro": "In unserem Beispiel wählen wir das SA6400-Modell.", + "loaders": { + "arc": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_2_1.png", + "alt": "Arc Model Auswahl", + "caption": "Arc Model Auswahl" + } + ], + "rr": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_2_1.png", + "alt": "RR Modellauswahl", + "caption": "RR Modellauswahl" + } + ], + "tinycore": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_2_1.png", + "alt": "TinyCore Modellauswahl", + "caption": "TinyCore Modellauswahl" + } + ] + } + }, + { + "id": "step3", + "title": "Wählen Sie DSM-Version", + "intro": "Nachdem Sie das Modell ausgewählt haben, müssen Sie die DSM-Version auswählen, die Sie installieren möchten.", + "loaders": { + "arc": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_3_1.png", + "alt": "Versionsauswahl in Arc – Schritt 1", + "caption": "Versionsauswahl in Arc – Schritt 1" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_3_2.png", + "alt": "Versionsauswahl in Arc – Schritt 2", + "caption": "Versionsauswahl in Arc – Schritt 2" + } + ], + "rr": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_3_1.png", + "alt": "RR Versionsauswahl - Schritt 1", + "caption": "RR Versionsauswahl - Schritt 1" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_3_2.png", + "alt": "RR Versionsauswahl - Schritt 2", + "caption": "RR Versionsauswahl - Schritt 2" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_3_3.png", + "alt": "RR Versionsauswahl - Schritt 3", + "caption": "RR Versionsauswahl - Schritt 3" + } + ], + "tinycore": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_3_1.png", + "alt": "TinyCore Versionsauswahl - Schritt 1", + "caption": "TinyCore Versionsauswahl - Schritt 1" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_3_2.png", + "alt": "TinyCore Versionsauswahl - Schritt 2", + "caption": "TinyCore Versionsauswahl - Schritt 2" + } + ] + } + }, + { + "id": "step4", + "title": "Addons auswählen", + "intro": "Mit diesem Schritt können Sie dem Loader zusätzliche Funktionen oder benutzerdefinierte Konfigurationen hinzufügen.", + "loaders": { + "arc": [ + { + "htmlBefore": "Arc bietet Ihnen die Möglichkeit, die Einstellungen automatisch zu konfigurieren oder manuell anzupassen. Wenn die automatische Konfiguration ausgewählt ist, beginnt der Loader mit der Anwendung der erforderlichen Einstellungen und startet automatisch neu, sobald der Prozess abgeschlossen ist.", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_4_1.png", + "alt": "Arc Auto Konfiguration", + "caption": "Arc Auto Konfiguration" + }, + { + "htmlBefore": "Wenn wir uns dafür entscheiden, den automatischen Modus nicht zu verwenden, betreten wir das Menü, um verschiedene Optionen zu konfigurieren, die für den Loader erforderlich sind:", + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_4_2.png", + "alt": "Arc Manual Konfiguration", + "caption": "Arc Manual Konfiguration" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_4_3.png", + "alt": "Arc SN/Mac Konfiguration", + "caption": "Arc SN/Mac Konfiguration" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_4_4.png", + "alt": "Arc Sata Portmap", + "caption": "Arc Sata Portmap (verwenden Sie die empfohlene Option)" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_4_5.png", + "alt": "Arc Addons Auswahl", + "caption": "Arc Addons Auswahl" + } + ], + "rr": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_4_1.png", + "alt": "RR Addon Schritt 1", + "caption": "RR Addon Schritt 1" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_4_2.png", + "alt": "RR Addon Schritt 2", + "caption": "RR-Addon – Schritt 2: Zum Hinzufügen von Addons klicken." + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_4_3.png", + "alt": "RR Addon Schritt 3", + "caption": "RR Addon Schritt 3 - Wählen Sie das gewünschte aus, indem Sie darauf klicken. Wenn Sie mehr hinzufügen möchten, wiederholen Sie den Vorgang aus den Bildern 2.4.2 und 2.4.3." + } + ], + "tinycore": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_4_1.png", + "alt": "TinyCore SN Konfiguration", + "caption": "TinyCore SN Konfiguration" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_4_2.png", + "alt": "TinyCore Zufallsoption", + "caption": "TinyCore-Zufallsoption – die zufällige Auswahl wird empfohlen." + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_4_3.png", + "alt": "TinyCore MAC Konfiguration", + "caption": "TinyCore MAC Konfiguration" + }, + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_4_4.png", + "alt": "TinyCore VM MAC", + "caption": "TinyCore VM MAC - Wählen Sie den MAC Ihres VM oder einen zufälligen MAC" + } + ] + } + }, + { + "id": "step5", + "title": "Bauen Sie den Loader", + "intro": "Sobald Sie das Modell, die DSM-Version und die Addons ausgewählt haben, erstellen Sie den Loader. Dieser Vorgang kann je nach Loader und ausgewählter Konfiguration einige Minuten dauern. Um zu starten, wählen Sie die Option \"Build the Loader\".", + "loaders": { + "arc": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_5_1.png", + "alt": "Loader in Arc erstellen", + "caption": "Loader in Arc erstellen" + } + ], + "rr": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_5_1.png", + "alt": "Loader in RR erstellen", + "caption": "Loader in RR erstellen" + } + ], + "tinycore": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_5_1.png", + "alt": "Loader in TinyCore erstellen", + "caption": "Loader in TinyCore erstellen" + } + ] + } + }, + { + "id": "step6", + "title": "Loader starten", + "intro": "Sobald der Loader gebaut wurde, werden Sie zum Booten aufgefordert. Der VM wird mit der von Ihnen erstellten Konfiguration neu gestartet und die DSM-Installation gestartet.", + "loaders": { + "arc": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/arc/arc_1_6_1.png", + "alt": "Loader in Arc starten", + "caption": "Loader in Arc starten" + } + ], + "rr": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/rr/rr_2_6_1.png", + "alt": "Loader in RR starten", + "caption": "Loader in RR starten" + } + ], + "tinycore": [ + { + "src": "https://macrimi.github.io/ProxMenux/vm/synology/tinycore/tinycore_3_6_1.png", + "alt": "Loader in TinyCore starten", + "caption": "Loader in TinyCore starten" + } + ] + } + } + ], + "dsmInstall": { + "heading": "Starten der DSM Installation", + "intro": "Sobald der Loader gebootet ist, können Sie Ihr Synology-Gerät finden, indem Sie:", + "afterCode": "Folgen Sie den Anweisungen auf dem Bildschirm, um die DSM-Installation abzuschließen.", + "setupAlt": "DSM-Einrichtung", + "setupCaption": "DSM-Einrichtungsbildschirm", + "patience": "Bitte haben Sie Geduld - der Prozess kann einige Minuten dauern. Der prozentuale Fortschritt wird automatisch aktualisiert, wenn das Setup fortschreitet. Ein Countdown beginnt, sobald die Installation kurz vor dem Abschluss steht.", + "finishAlt": "Installation abgeschlossen", + "finishCaption": "Installation abgeschlossen" + }, + "tips": { + "heading": "Tipps", + "introItem": "Beachten Sie, dass sich die verfügbaren Optionen je nach Loaderversion und Entwicklerupdates ändern können. Wenn während des Loader-Erstellungsprozesses Probleme auftreten, lesen Sie die Loader-Dokumentation:", + "docLinks": [ + { + "label": "Arc-Dokumentation", + "url": "https://github.com/AuxXxilium/arc" + }, + { + "label": "RR Dokumentation", + "url": "https://github.com/RROrg/rr" + }, + { + "label": "TinyCore Dokumentation", + "url": "https://github.com/PeterSuh-Q3/tinycore-redpill" + } + ], + "olderModels": "Einige ältere DSM-Modelle können Probleme beim Erkennen von Festplatten oder der Netzwerkkarte haben. Es wird empfohlen, neuere Modelle zu verwenden.", + "updateLabel": "Aktualisieren:", + "updateBody": "Einige Loader bieten die Möglichkeit, den Loader direkt aus dem Menü zu aktualisieren.", + "importantLabel": "Wichtig:", + "importantBody": "ProxMenux bietet keine Unterstützung für die verschiedenen Loader." + } +} diff --git a/web/messages/de/docs/create-vm/system-linux.json b/web/messages/de/docs/create-vm/system-linux.json new file mode 100644 index 00000000..0c25299f --- /dev/null +++ b/web/messages/de/docs/create-vm/system-linux.json @@ -0,0 +1,292 @@ +{ + "meta": { + "title": "VM erstellen: Linux-System | ProxMenux-Dokumentation", + "description": "Erstellen Sie Linux-VMs auf Proxmox VE mit ProxMenux. Beschreibt den ausgewählten Katalog offizieller ISO-Abbilder (Ubuntu, Debian, Fedora, Arch, Rocky, Mint, openSUSE, Alpine, Kali, Manjaro), den gemeinsamen Speicherplan, optionales GPU-Passthrough und Tipps nach der Installation.", + "ogTitle": "VM erstellen: Linux-System | ProxMenux-Dokumentation", + "ogDescription": "Erstellen Sie Linux-VMs auf Proxmox VE mit ProxMenux. Kuratierter ISO-Katalog, einheitlicher Speicherplan, optionaler GPU-Durchlauf und Tipps nach der Installation.", + "ogImageAlt": "ProxMenux Linux VM Menü" + }, + "header": { + "title": "VM erstellen: Linux-System", + "description": "Erstellen Sie eine Linux-VM auf Proxmox VE. ProxMenux bietet einen ausgewählten Katalog offizieller Linux-ISOs zum Herunterladen und eine Auswahl lokaler ISOs. Alle Wege führen durch den gemeinsamen VM-Assistenten, den Speicherplan und den optionalen GPU-Passthrough-Assistenten.", + "section": "Virtuelle Maschinen" + }, + "intro": { + "title": "Was dieses Skript macht", + "body": "Wählt eine offizielle Linux-ISO zum automatischen Herunterladen oder eine bereits hochgeladene lokale ISO. Danach führt der gemeinsame Assistent durch CPU, RAM und BIOS, den Speicherplan, optionales GPU-Passthrough und die VM-Erstellung." + }, + "image": { + "alt": "ProxMenux Linux Installationsoptionen", + "caption": "Linux Installationsoptionen" + }, + "config": { + "heading": "Standard- oder erweiterte Konfiguration", + "intro": "Nachdem die ISO ausgewählt wurde, fragt ProxMenux, ob das Standardprofil VM verwendet oder der erweiterte Wizard geöffnet werden soll.", + "defaultHeading": "Standardkonfiguration", + "headerParam": "Parameter", + "headerValue": "Standardwert", + "defaultRows": [ + { + "param": "Maschinentyp", + "valueRich": "q35" + }, + { + "param": "BIOS", + "value": "OVMF (UEFI)" + }, + { + "param": "CPU-Typ", + "value": "Host" + }, + { + "param": "Kerne", + "value": "2" + }, + { + "param": "RAM", + "value": "4096 MB" + }, + { + "param": "Brücke", + "valueRich": "vmbr0" + }, + { + "param": "MAC-Adresse", + "value": "Autogeneriert" + }, + { + "param": "Nach Abschluss starten", + "value": "Nein" + } + ], + "advancedHeading": "Erweiterte Konfiguration", + "advancedIntro": "Der erweiterte Wizard stellt jeden Parameter einzeln dar:", + "headerOptions": "Optionen", + "advancedRows": [ + { + "param": "Maschinentyp", + "optionsRich": "q35 oder i440fx" + }, + { + "param": "BIOS", + "options": "OVMF (UEFI) oder SeaBIOS (Legacy)" + }, + { + "param": "CPU-Typ", + "options": "Host, KVM64 und weitere aufgeführte CPU-Modelle" + }, + { + "param": "Kerne", + "options": "Anzahl CPU-Kerne" + }, + { + "param": "RAM", + "options": "Speicher zugewiesen VM" + }, + { + "param": "Brücke", + "options": "Netzbrücke" + }, + { + "param": "MAC-Adresse", + "options": "Eigene MAC-Adresse" + }, + { + "param": "VLAN", + "options": "VLAN-Tag (fakultativ)" + }, + { + "param": "MTU", + "options": "MTU-Größe" + } + ] + }, + "storagePlan": { + "heading": "Speicherplan", + "body": "Nach CPU / RAM öffnet ProxMenux das Storage Plan-Menü, in dem Sie virtuelle Festplatten, importierte Festplatten und PCI-Durchgangsgeräte im selben VM kombinieren können, indem Sie Elemente einzeln hinzufügen und beenden, wenn Sie fertig sind.", + "virtualDiskTitle": "a. Virtuelle Festplatte hinzufügen", + "virtualDiskItems": [ + "Listet die auf dem Host verfügbaren Proxmox-Speicher auf.", + "Sie wählen den Zielspeicher und die Größe in GB aus.", + "Das Laufwerk nutzt den gewählten Bus: SCSI (Standard), SATA, VirtIO oder IDE. SATA hat sechs Anschlüsse von 0 bis 5: sata0, sata1, …." + ], + "importDiskTitle": "b. Physisches Laufwerk hinzufügen", + "importDiskItems": [ + "Erkennt physische Festplatten, die sicher importiert werden können (System und geschützte Festplatten sind verborgen).", + "Sie wählen eine oder mehrere Festplatten über eine Checkliste aus.", + "Importierte Laufwerke nutzen den gewählten Bus und werden über qm set zugewiesen." + ], + "pciTitle": "c. Controller oder NVMe hinzufügen (PCI-Passthrough)", + "pciItems": [ + "Erkennt SATA-/SAS-HBAs und NVMe-Geräte und ermöglicht die Zuweisung eines ganzen Controllers an die VM. IOMMU muss aktiv sein; das Skript bietet die Aktivierung und bei Bedarf einen Neustart an.", + "Ein Bestätigungsschritt warnt vor einem kontrollerweiten Risiko (die gesamte Karte, einschließlich jeder Festplatte, verlässt den Host).", + "PCI-Geräte werden mit Befehlen wie qm set VMID --hostpci0 PCI_ADDRESS,pcie=1 zugewiesen. Ersetzen Sie VMID und PCI_ADDRESS durch VM-ID und PCI-Adresse und verwenden Sie für jedes Gerät einen freien hostpci-Index." + ], + "resetTitle": "Zurücksetzen und abschließen", + "resetBody": "Das Menü bietet r zum Zurücksetzen und Neubeginnen sowie d zum Abschließen und Fortfahren. Ein leerer Plan kann nicht abgeschlossen werden." + }, + "gpu": { + "heading": "Optionaler GPU-Durchgang", + "body": "Nach dem Speicherplan bietet ProxMenux bei einer geeigneten GPU den Passthrough-Assistenten an. Siehe GPU zu einer VM hinzufügen (Passthrough). Ein Host-Neustart kann erforderlich sein." + }, + "autoFeatures": { + "heading": "Automatische VM-Funktionen", + "efiTitle": "EFI-Laufwerk", + "efiBody": "Wenn OVMF (UEFI) ausgewählt ist, erstellt ProxMenux eine 4 MB EFI-Systemplatte auf dem von Ihnen ausgewählten Speicher, sodass die Firmware einen Platz zum Speichern ihrer Variablen hat.", + "isoTitle": "ISO-Anschluss", + "isoBody": "Die Installation ISO ist als CD-ROM an ide2 angebracht, bereit für den ersten Boot.", + "guestTitle": "QEMU Gastagent", + "guestBody": "Der Gastagentenkanal ist in der VM-Konfiguration aktiviert. Sie müssen den Agenten immer noch im Gast installieren - siehe unten." + }, + "installOptions": { + "heading": "Linux Installationsoptionen", + "officialHeading": "Offizielle ISO (traditionelle Installation)", + "officialBody": "ProxMenux bietet eine ausgewählte Liste offizieller Linux-ISOs. Wählen Sie eine aus; das Skript lädt sie direkt vom Spiegelserver der Distribution nach /var/lib/vz/template/iso und bindet sie in die VM ein.", + "officialImageAlt": "Auswahl der Linux-Distribution", + "officialImageCaption": "Auswahl der Linux-Distribution", + "localHeading": "Lokale ISO-Datei", + "localBody": "Verwendet eine Linux-ISO, die bereits unter /var/lib/vz/template/iso liegt. Das Skript listet alle gefundenen *.iso-Dateien zur Auswahl auf.", + "localImageAlt": "Lokales ISO-Auswahlmenü", + "localImageCaption": "Lokales ISO-Auswahlmenü", + "distros": [ + { + "name": "Ubuntu", + "variants": [ + "25.10 Desktop", + "24.04 Desktop", + "22.04 Desktop", + "20.04 Desktop", + "25.10 Server", + "24.04 Server", + "22.04 Server", + "20.04 Server" + ] + }, + { + "name": "Debian", + "variants": [ + "13 Desktop", + "12 Desktop", + "11 Desktop", + "13 Netinst", + "12 Netinst", + "11 Netinst" + ] + }, + { + "name": "Fedora", + "variants": [ + "Workstation 42" + ] + }, + { + "name": "Arch Linux", + "variants": [ + "neueste Version" + ] + }, + { + "name": "Rocky Linux", + "variants": [ + "9" + ] + }, + { + "name": "Linux Mint", + "variants": [ + "22.1 Cinnamon" + ] + }, + { + "name": "openSUSE", + "variants": [ + "Leap 15.6" + ] + }, + { + "name": "Alpine Linux", + "variants": [ + "3.21" + ] + }, + { + "name": "Kali Linux", + "variants": [ + "2026.1" + ] + }, + { + "name": "Manjaro", + "variants": [ + "25.0 GNOME" + ] + } + ] + }, + "endToEnd": { + "heading": "Was das Skript Ende-zu-Ende macht", + "items": [ + "Erhält die Linux ISO (offizieller Download oder lokale Datei).", + "Wendet die Standard- oder erweiterte CPU / RAM / BIOS / Maschinentypkonfiguration an.", + "Öffnet den Speicherplan und verbindet die ausgewählten virtuellen Festplatten, importierten Festplatten und PCI-Geräte.", + "Erstellt die EFI-Festplatte, wenn UEFI ausgewählt ist.", + "Montiert die Linux ISO auf ide2.", + "Legt die Bootreihenfolge fest: zuerst Laufwerk, dann ISO.", + "Aktiviert den QEMU Guest Agent Kanal.", + "Optional läuft der GPU Passthrough-Assistent.", + "Generiert eine stilisierte HTML-Beschreibung, die dem VM beigefügt ist.", + "Startet die VM, wenn Sie dies gewählt haben." + ] + }, + "postInstall": { + "heading": "Tipps nach der Installation", + "guestAgentHeading": "Installieren Sie den QEMU Guest Agent in der VM", + "guestAgentBody": "ProxMenux aktiviert den Gastagentenkanal in der VM-Konfiguration; den Agenten selbst müssen Sie im Gast installieren. Er unterstützt kontrolliertes Herunterfahren und das Einfrieren von Dateisystemen während Backups, um die Datenkonsistenz zu verbessern.", + "debian": "Debian / Ubuntu", + "fedora": "Fedora / Rocky / RHEL", + "arch": "Arch Linux", + "opensuse": "openSUSE", + "virtioHeading": "VirtIO-Unterstützung in Linux", + "virtioBody": "Moderne Linux-Distributionen enthalten normalerweise VirtIO-Treiber für Laufwerke und Netzwerk. Wählen Sie virtio für VirtIO-Blockgeräte oder scsi mit dem konfigurierten VirtIO-SCSI-Controller; der Netzwerkadapter nutzt ebenfalls virtio. Prüfen Sie die Treiberunterstützung bei älteren Installern.", + "virtioWarnTitle": "Sehr alte Distributionen", + "virtioWarnBody": "Sehr alte Installer können die benötigten VirtIO-Treiber nicht enthalten. Verwenden Sie dann einen emulierten SATA-/SCSI-Controller oder stellen Sie passende Treiber bereit.", + "trimHeading": "TRIM / discard bei SSD-Speicher aktivieren", + "trimBody": "Liegt die VM auf SSD-basiertem Proxmox-Speicher und wurde das Laufwerk mit dem Flag discard zugewiesen, können Sie TRIM-Befehle aus dem Gast weiterleiten. Zwei Möglichkeiten:", + "trimItems": [ + "Führen Sie fstrim -av regelmäßig aus (oder aktivieren Sie die fstrim.timer-Einheit in systemd-Distributionen).", + "Oder mounten Sie Dateisysteme mit der Option discard in /etc/fstab." + ], + "balloonHeading": "Dynamische Speicherzuweisung (Ballooning)", + "balloonBody": "Moderne Linux-Kernel enthalten normalerweise den Treiber virtio_balloon. Mit aktiviertem Balloon-Gerät kann Proxmox Speicherstatistiken erfassen. Dynamische Rückgewinnung setzt zudem einen Mindestwert unterhalb des konfigurierten Maximums voraus; gleiche Werte ergeben eine feste Speicherzuweisung." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/import-disk-image-vm", + "label": "Disk-Image in VM importieren", + "tail": " — ein vorhandenes Linux-Abbild im Format .qcow2 / .img / .raw einer neuen VM zuweisen." + }, + { + "href": "/docs/utils/import-vm", + "label": "VM von OVA / OVF importieren", + "tail": " — eine aus VMware / VirtualBox / Proxmox exportierte Linux-VM importieren." + }, + { + "href": "/docs/hardware/gpu-vm-passthrough", + "label": "GPU zu VM hinzufügen (Passthrough)", + "tail": "- für Linux-VMs, die GPU-Beschleunigung benötigen." + }, + { + "href": "/docs/help-info/vm-ct-commands", + "label": "VM und CT Verwaltungsbefehle", + "tail": "— qm CLI Referenz." + }, + { + "href": "/docs/create-vm", + "label": "Übersicht zur VM-Erstellung", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/create-vm/system-nas/index.json b/web/messages/de/docs/create-vm/system-nas/index.json new file mode 100644 index 00000000..5111b5c4 --- /dev/null +++ b/web/messages/de/docs/create-vm/system-nas/index.json @@ -0,0 +1,147 @@ +{ + "meta": { + "title": "VM erstellen: System NAS | ProxMenux Dokumentation", + "description": "Erstellen Sie virtuelle NAS-Maschinen auf Proxmox VE mit ProxMenux. Unterstützt Synology DSM (Loader), TrueNAS SCALE / CORE, OpenMediaVault, XigmaNAS, Rockstor, ZimaOS und FygoOS. Umbrel OS ist über ein Community-Script verfügbar.", + "ogTitle": "VM erstellen: System NAS | ProxMenux Dokumentation", + "ogDescription": "Erstellen Sie virtuelle NAS-Maschinen auf Proxmox VE mit ProxMenux. Unterstützt Synology DSM (Loader), TrueNAS SCALE / CORE, OpenMediaVault, XigmaNAS, Rockstor, ZimaOS und FygoOS.", + "ogImageAlt": "ProxMenux System NAS Menü" + }, + "header": { + "title": "VM erstellen: System NAS", + "description": "ProxMenux unterstützt acht NAS-Betriebssysteme als dedizierte VMs sowie einen optionalen Umbrel OS-Pfad über ein Community-Skript. Die meisten Systeme teilen sich den gleichen Auto-ISO-Flow; Synology DSM verwendet einen spezialisierten Loader-Assistenten.", + "section": "Virtuelle Maschinen" + }, + "intro": { + "title": "Wie der NAS-Selektor funktioniert", + "body": "Im allgemeinen ISO-Ablauf versucht die NAS-Auswahl, die neueste stabile Version vom Projektspiegel zu ermitteln, und nutzt bei Bedarf eine Ersatzversion. Nach dem ISO-Download öffnet sich der gemeinsame Konfigurator für CPU, RAM, Laufwerke und Netzwerk. Synology DSM und ZimaOS verwenden eigene Installationsabläufe." + }, + "image": { + "alt": "ProxMenux System NAS-Selektor", + "caption": "System-NAS-Auswahl" + }, + "flowBadges": { + "loader": "Loader-Ablauf", + "auto-iso": "Automatischer ISO-Ablauf", + "dedicated": "Dediziertes Skript" + }, + "labels": { + "base": "Basis", + "fileSystem": "Dateisystem", + "viewDetails": "Details anzeigen" + }, + "supported": { + "heading": "Unterstützte NAS-Systeme", + "cards": [ + { + "name": "Synology DSM", + "tagline": "DiskStation Manager mit dem ARC / RR-Loader. Erfordert die Konfiguration des Loaders vor dem ersten Booten.", + "icon": "HardDrive", + "base": "Linux (Custom)", + "fileSystem": "Btrfs, ext4", + "href": "/docs/create-vm/system-nas/synology", + "flow": "loader" + }, + { + "name": "TrueNAS SCALE", + "tagline": "Debian-basiertes TrueNAS mit Docker-Apps in aktuellen Versionen und ZFS als primärem Dateisystem. Ältere SCALE-Versionen nutzten Kubernetes.", + "icon": "Database", + "base": "Debian Linux", + "fileSystem": "ZFS", + "href": "/docs/create-vm/system-nas/system-nas-others#truenas-scale", + "flow": "auto-iso" + }, + { + "name": "TrueNAS CORE", + "tagline": "FreeBSD-basiertes TrueNAS mit Schwerpunkt ZFS-Speicher. Jails und Plugins in CORE 13.3 sind veraltet und werden nicht unterstützt.", + "icon": "Database", + "base": "FreeBSD", + "fileSystem": "ZFS", + "href": "/docs/create-vm/system-nas/system-nas-others#truenas-core", + "flow": "auto-iso" + }, + { + "name": "OpenMediaVault", + "tagline": "Schlankes Debian-basiertes NAS mit modularer Plugin-Architektur. Gut für ältere Hardware.", + "icon": "Server", + "base": "Debian Linux", + "fileSystem": "ext4, XFS, Btrfs", + "href": "/docs/create-vm/system-nas/system-nas-others#openmediavault", + "flow": "auto-iso" + }, + { + "name": "XigmaNAS", + "tagline": "FreeBSD-basierte NAS - der ursprüngliche Nachfolger von FreeNAS, konzentriert auf ZFS und Einfachheit.", + "icon": "Database", + "base": "FreeBSD", + "fileSystem": "ZFS, UFS", + "href": "/docs/create-vm/system-nas/system-nas-others#xigmanas", + "flow": "auto-iso" + }, + { + "name": "Rockstor", + "tagline": "openSUSE-basierte NAS rund um Btrfs. Rock-ons (Docker Apps) erweitern die Funktionalität.", + "icon": "HardDrive", + "base": "openSUSE Leap", + "fileSystem": "Btrfs", + "href": "/docs/create-vm/system-nas/system-nas-others#rockstor", + "flow": "auto-iso" + }, + { + "name": "ZimaOS", + "tagline": "Schlankes NAS-Betriebssystem für Medien, Docker-Apps und Hausautomation. Verwendet einen eigenen Installer.", + "icon": "MonitorIcon", + "base": "Buildroot (aus CasaOS hervorgegangen)", + "fileSystem": "ext4", + "href": "/docs/create-vm/system-nas/system-nas-others#zimaos", + "flow": "dedicated" + }, + { + "name": "FygoOS", + "tagline": "Internationale Ausgabe von fnOS: ein Debian-basiertes NAS mit Speichervolumen, Apps und Medien-Streaming, das über seine Web-Schnittstelle und die Fygo-Apps verwaltet wird.", + "icon": "Server", + "base": "Debian Linux", + "fileSystem": "Btrfs, ext4", + "href": "/docs/create-vm/system-nas/system-nas-others#fygoos", + "flow": "auto-iso" + } + ] + }, + "umbrel": { + "title": "Gemeinschaftsoption: Umbrel OS", + "bodyRich": "Der neunte NAS-Menüeintrag ist Umbrel OS. Er startet einen außerhalb von ProxMenux gepflegten community-scripts-Installer und ist hier nicht dokumentiert. Der Installer übernimmt den gesamten Ablauf und zeigt am Ende die Standardzugangsdaten an." + }, + "zfsMem": { + "title": "Faustregel für den ZFS-Arbeitsspeicher", + "bodyRich": "Weisen Sie TrueNAS für den Grundbetrieb mindestens 8 GB RAM zu, bei höherer Last mehr. ZFS nutzt RAM für den ARC-Cache; eine allgemeine Leistungsgrenze bei 8 GB gibt es aber nicht für jedes ZFS-NAS. Beachten Sie die Anforderungen der Distribution sowie Laufwerke, Dienste und Apps. Schlanke OMV- oder Rockstor-Systeme können weniger benötigen." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/create-vm/system-nas/synology", + "label": "Synology-VM", + "tail": "— dediziertes Installationsprogramm für Synology DSM (Xpenology) auf Proxmox." + }, + { + "href": "/docs/create-vm/system-nas/system-nas-others", + "label": "Sonstige System-NAS", + "tail": "TrueNAS, OpenMediaVault, Rockstor, XigmaNAS, FygoOS und ähnliches." + }, + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": "- reale physische Festplatten an den NAS VM übergeben (empfohlen über virtuelle Festplatten für die Produktion)." + }, + { + "href": "/docs/help-info/zfs-commands", + "label": "ZFS Verwaltungsbefehle", + "tail": "— nützlich, wenn Ihre NAS-Distribution ZFS darunter verwendet." + }, + { + "href": "/docs/create-vm", + "label": "Übersicht zur VM-Erstellung", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/create-vm/system-nas/synology.json b/web/messages/de/docs/create-vm/system-nas/synology.json new file mode 100644 index 00000000..46c60c81 --- /dev/null +++ b/web/messages/de/docs/create-vm/system-nas/synology.json @@ -0,0 +1,400 @@ +{ + "meta": { + "title": "VM erstellen: Synology DSM | ProxMenux-Dokumentation", + "description": "Erstellen Sie eine Synology-DSM-VM auf Proxmox VE. ProxMenux lädt den gewählten Loader (Arc, RR, Alpine RedPill M-shell oder ein eigenes Abbild), importiert ihn als Bootlaufwerk der VM und konfiguriert CPU, RAM und Speicher mit dem Standard- oder erweiterten Assistenten.", + "ogTitle": "VM erstellen: Synology DSM | ProxMenux-Dokumentation", + "ogDescription": "Erstellen Sie eine Synology DSM virtuelle Maschine auf Proxmox VE mit ProxMenux." + }, + "header": { + "title": "VM erstellen: Synology DSM", + "description": "Erstellen Sie eine Synology-DSM-VM auf Proxmox VE. ProxMenux lädt den gewählten Loader (Arc, RR, Alpine RedPill M-shell oder ein eigenes Abbild), importiert ihn als Bootlaufwerk der VM und konfiguriert CPU, RAM und Speicher mit dem Standard- oder erweiterten Assistenten.", + "section": "Virtuelle Maschinen · NAS" + }, + "loaderLabels": { + "arc": "Arc Loader", + "rr": "RR Loader", + "tinycore": "TinyCore Loader" + }, + "whatThisDoes": { + "title": "Was das macht", + "bodyRich": "ProxMenux automatisiert die VM-Vorbereitung für Synology DSM: Es lädt einen unterstützten Loader, importiert ihn als IDE-Bootlaufwerk in eine neue VM und übernimmt die gewählten CPU-, RAM-, Netzwerk- und Speichereinstellungen. Loader-Erstellung, DSM-Modellwahl und DSM-Installation erfolgen innerhalb der VM über die Web- oder Terminaloberfläche des Loaders. Die Community pflegt diese sich häufig ändernden Schritte; diese Seite bietet eine Ausgangsbasis." + }, + "supportedLoaders": { + "heading": "Unterstützte Loader", + "intro": "Mit dem Skript können Sie zwischen vier Loader-Quellen wählen:", + "loaders": [ + { + "name": "AuxXxilium Arc", + "url": "https://github.com/AuxXxilium/arc" + }, + { + "name": "RedPill RR", + "url": "https://github.com/RROrg/rr" + }, + { + "name": "Alpine RedPill M-shell (TinyCore-Projekt)", + "url": "https://github.com/PeterSuh-Q3/tinycore-redpill" + } + ], + "customRich": "Custom Loader – legen Sie Ihr eigenes Loader-Image in /var/lib/vz/template/iso und das Skript wird es abholen." + }, + "config": { + "heading": "Standard- oder erweiterte Konfiguration", + "intro": "Wenn Sie das Skript ausführen, wird gefragt, ob Sie das ProxMenux-Standardprofil VM verwenden oder den erweiterten Assistenten öffnen möchten. Standardwerte sind auf DSM abgestimmt und funktionieren für die meisten Installationen.", + "defaultHeading": "Standardkonfiguration", + "headerParam": "Parameter", + "headerValue": "Standardwert", + "defaultRowsRich": [ + { + "param": "Maschinentyp", + "valueRich": "q35" + }, + { + "param": "BIOS", + "valueRich": "OVMF (UEFI)" + }, + { + "param": "CPU-Typ", + "valueRich": "Host" + }, + { + "param": "Kerne", + "valueRich": "2" + }, + { + "param": "RAM", + "valueRich": "4096 MB" + }, + { + "param": "Brücke", + "valueRich": "vmbr0" + }, + { + "param": "MAC-Adresse", + "valueRich": "Autogeneriert" + }, + { + "param": "Nach Abschluss starten", + "valueRich": "Nein" + } + ], + "advancedHeading": "Erweiterte Konfiguration", + "advancedIntro": "Der erweiterte Assistent zeigt jede Einstellung einzeln. Sie können die folgenden Werte ändern:", + "headerOptions": "Optionen", + "advancedRowsRich": [ + { + "param": "Maschinentyp", + "optionsRich": "q35 oder i440fx" + }, + { + "param": "BIOS", + "optionsRich": "OVMF (UEFI) oder SeaBIOS (Legacy)" + }, + { + "param": "CPU-Typ", + "optionsRich": "Host oder KVM64" + }, + { + "param": "Kerne", + "optionsRich": "Anzahl CPU-Kerne" + }, + { + "param": "RAM", + "optionsRich": "Der VM zugewiesener Speicher" + }, + { + "param": "Brücke", + "optionsRich": "Netzbrücke" + }, + { + "param": "MAC-Adresse", + "optionsRich": "Eigene MAC-Adresse" + }, + { + "param": "VLAN", + "optionsRich": "VLAN-Tag (fakultativ)" + }, + { + "param": "MTU", + "optionsRich": "MTU-Größe" + } + ] + }, + "storagePlan": { + "heading": "Speicherplan", + "introRich": "Nach CPU und RAM öffnet ProxMenux das Menü Speicherplan. Dort können Sie virtuelle und importierte Laufwerke sowie PCI-Passthrough-Geräte in derselben VM kombinieren. Fügen Sie die Elemente einzeln hinzu und schließen Sie den Plan danach ab.", + "virtualHeading": "a. Virtuelle Festplatte hinzufügen", + "virtualItemsRich": [ + "Listet die auf dem Host verfügbaren Proxmox-Speicher auf.", + "Sie wählen den Zielspeicher und die Größe in GB aus.", + "Das Laufwerk wird als SATA zugewiesen (sata0, sata1, …). Virtuelle und physische Datenlaufwerke teilen sich die sechs SATA-Anschlüsse 0–5." + ], + "importHeading": "b. Physisches Laufwerk hinzufügen", + "importItemsRich": [ + "Erkennt physische Festplatten, die sicher importiert werden können (System und geschützte Festplatten sind verborgen).", + "Sie wählen eine oder mehrere Festplatten über eine Checkliste aus.", + "Importierte Festplatten werden als SATA über qm set angehängt." + ], + "pciHeading": "c. Controller oder NVMe hinzufügen (PCI-Passthrough)", + "pciItemsRich": [ + "Erkennt SATA-/SAS-HBAs und NVMe-Geräte und ermöglicht die Zuweisung eines ganzen Controllers an die VM. IOMMU muss aktiv sein; das Skript bietet die Aktivierung und bei Bedarf einen Neustart an.", + "Ein Bestätigungsschritt warnt vor einem kontrollerweiten Risiko (die gesamte Karte, einschließlich jeder Festplatte, verlässt den Host).", + "PCI-Geräte werden mit Befehlen wie qm set VMID --hostpci0 PCI_ADDRESS,pcie=1 zugewiesen. Ersetzen Sie VMID und PCI_ADDRESS durch VM-ID und PCI-Adresse und verwenden Sie für jedes Gerät einen freien hostpci-Index." + ], + "resetCalloutTitle": "Zurücksetzen und abschließen", + "resetCalloutBodyRich": "Das Menü bietet r zum Zurücksetzen der Auswahl und d zum Abschließen und Fortfahren. Ein leerer Plan ist nicht zulässig; Synology benötigt mindestens ein Laufwerk für DSM." + }, + "gpu": { + "heading": "Optionaler GPU-Durchgang", + "bodyRich": "Nach dem Speicherplan fragt das Skript bei einer geeigneten GPU nach deren Zuweisung an die VM. Siehe GPU zu einer VM hinzufügen (Passthrough). Ein Host-Neustart kann erforderlich sein." + }, + "loaderInstall": { + "heading": "Loader installieren", + "intro1Rich": "Arc, RR und Alpine RedPill M-shell werden automatisch aus ihren ursprünglichen GitHub-Repositories heruntergeladen und entpackt. Schlägt der Download fehl, bricht das Skript mit einer klaren Meldung ab.", + "intro2Rich": "Für die Option Custom Loader scannt das Skript /var/lib/vz/template/iso. Wenn mehr als ein Kandidat gefunden wird, werden Sie aufgefordert, den gewünschten auszuwählen.", + "uploadIntro": "Sie können benutzerdefinierte Loader aus dem lokalen Proxmox-Speicher hochladen:", + "imageAlt": "Custom Loader hinzufügen", + "imageCaption": "Custom Loader hinzufügen" + }, + "vmCreation": { + "heading": "VM-Erstellung – ausgeführte Skriptbefehle", + "introRich": "Sobald Sie den Loader bestätigt haben, führt ProxMenux das Äquivalent dieser qm-Befehle aus:", + "itemsRich": [ + "qm create — erstellt die VM mit den gewählten CPU-, RAM-, BIOS- und Maschineneinstellungen.", + "qm importdisk importiert das Loader-Abbild in den Proxmox-Speicher. Danach weist das Skript es der VM als IDE-Laufwerk an ide0 zu.", + "qm set VMID --sata0 DISK_REFERENCE weist ein geplantes Datenlaufwerk zu. Ersetzen Sie VMID und DISK_REFERENCE durch VM-ID und Laufwerksreferenz; nutzen Sie einen freien SATA-Index 0–5. Der Netzwerkadapter wird mit qm create konfiguriert.", + "qm set VMID --hostpci0 PCI_ADDRESS,pcie=1 weist einen gewählten PCI-Controller oder ein NVMe-Gerät zu. Ersetzen Sie VMID und PCI_ADDRESS durch VM-ID und PCI-Adresse und nutzen Sie einen freien hostpci-Index pro Gerät.", + "qm set VMID --boot order=ide0 legt den Loader als Bootgerät fest. Ersetzen Sie VMID durch die VM-ID." + ] + }, + "stepByStep": { + "heading": "Schrittweise Loader-Konfiguration", + "intro": "Die sechs gemeinsamen Schritte sind: VM starten, Modell, DSM-Version und Addons wählen, Loader erstellen und starten. Menüs und Optionen ändern sich je nach Version. Die M-shell-Bilder zeigen eine ältere TinyCore-Oberfläche; das aktuelle Skript lädt Alpine RedPill M-shell. Folgen Sie dessen aktuellen Anweisungen.", + "warnCalloutTitle": "Die Loader werden nicht von ProxMenux gepflegt", + "warnCalloutBody": "Arc, RR und Alpine RedPill M-shell sind Community-Projekte mit eigener Release-Kadenz. Wenn ein Schritt anders aussieht als die Screenshots unten, hat der Loader wahrscheinlich aktualisiert - überprüfen Sie die eigene Dokumentation des Loaders (Links unten auf dieser Seite)." + }, + "stepBadge": "Schritt", + "step1": { + "title": "Starten Sie die VM und öffnen Sie das Hauptmenü", + "intro": "Starten Sie die erstellte VM. Beim ersten Start öffnet sich die Loader-Konfiguration, in der Sie das DSM-Modell auswählen und erstellen. Spätere Starts überspringen diesen Schritt, sofern Sie keine neue Konfiguration im Bootmenü erzwingen. Alle drei Loader bieten Terminal- und Weboberflächen.", + "arc": { + "webRich": "Web-Schnittstelle. Öffnen Sie einen Browser und gehen Sie zu der in der VM-Konsole gezeigten IP (in unserem Beispiel: http://192.168.0.32).", + "webAlt": "Arc-Loader-Weboberfläche", + "webCaption": "Arc-Loader-Weboberfläche", + "termRich": "Terminaloberfläche: Öffnen Sie sie direkt in der VM-Konsole.", + "termAlt": "Arc-Loader-Terminaloberfläche", + "termCaption": "Arc-Loader-Terminaloberfläche" + }, + "rr": { + "webRich": "Web-Schnittstelle. Öffnen Sie einen Browser und gehen Sie zu der IP, die in der VM-Konsole angezeigt wird, gefolgt von Port 7681 (Beispiel: http://192.168.0.33:7681).", + "webAlt": "RR-Loader-Weboberfläche", + "webCaption": "RR-Loader-Weboberfläche", + "termRich": "Terminaloberfläche. Geben Sie in der VM-Konsole menu.sh ein, um sie zu öffnen.", + "termAlt": "RR-Loader-Terminaloberfläche", + "termCaption": "RR-Loader-Terminaloberfläche" + }, + "tinycore": { + "webRich": "Web-Schnittstelle. Öffnen Sie einen Browser und gehen Sie zu der IP, die in der VM-Konsole angezeigt wird, gefolgt von Port 7681 (Beispiel: http://192.168.0.35:7681).", + "webAlt": "TinyCore Loader-Web-Schnittstelle", + "webCaption": "TinyCore Loader-Web-Schnittstelle", + "termRich": "Terminaloberfläche: Öffnen Sie sie direkt in der VM-Konsole. Achten Sie auf Aufforderungen, eine Taste zum Fortfahren zu drücken oder die Sprache zu ändern.", + "termAlt": "Terminaloberfläche von TinyCore Loader", + "termCaption": "Terminaloberfläche von TinyCore Loader" + } + }, + "step2": { + "title": "Modell auswählen", + "introRich": "Wählen Sie das Synology DSM-Modell aus, das Sie installieren möchten. Abhängig vom Loader müssen Sie möglicherweise die Optionen erweitern, um die vollständige Liste zu sehen. In den folgenden Screenshots verwenden wir das Modell SA6400 als Beispiel.", + "arc": { + "alt": "Modellauswahl in Arc", + "caption": "Modellauswahl in Arc" + }, + "rr": { + "alt": "Auswahl des RR-Modells", + "caption": "Auswahl des RR-Modells" + }, + "tinycore": { + "alt": "TinyCore Modellauswahl", + "caption": "TinyCore Modellauswahl" + } + }, + "step3": { + "title": "Wählen Sie DSM-Version", + "intro": "Nachdem Sie das Modell ausgewählt haben, wählen Sie die zu installierende DSM-Version aus.", + "arc": [ + { + "alt": "Versionsauswahl in Arc – Schritt 1", + "caption": "Versionsauswahl in Arc – Schritt 1" + }, + { + "alt": "Versionsauswahl in Arc – Schritt 2", + "caption": "Versionsauswahl in Arc – Schritt 2" + } + ], + "rr": [ + { + "alt": "RR Versionsauswahl - Schritt 1", + "caption": "RR Versionsauswahl - Schritt 1" + }, + { + "alt": "RR Versionsauswahl - Schritt 2", + "caption": "RR Versionsauswahl - Schritt 2" + }, + { + "alt": "RR Versionsauswahl - Schritt 3", + "caption": "RR Versionsauswahl - Schritt 3" + } + ], + "tinycore": [ + { + "alt": "TinyCore Versionsauswahl - Schritt 1", + "caption": "TinyCore Versionsauswahl - Schritt 1" + }, + { + "alt": "TinyCore Versionsauswahl - Schritt 2", + "caption": "TinyCore Versionsauswahl - Schritt 2" + } + ] + }, + "step4": { + "title": "Addons auswählen", + "intro": "Mit diesem Schritt können Sie dem Loader zusätzliche Funktionen oder benutzerdefinierte Konfiguration hinzufügen.", + "arc": { + "autoRich": "Arc bietet einen automatischen Modus (empfohlen) und einen manuellen Modus. Im automatischen Modus wendet der Loader sinnvolle Standardwerte an und startet selbstständig neu, wenn er fertig ist.", + "autoAlt": "Arc Auto Konfiguration", + "autoCaption": "Arc Auto Konfiguration", + "manualRich": "Wenn Sie manuell auswählen, gehen Sie eine nach der anderen durch die Optionen:", + "manualAlt": "Manuelle Konfiguration in Arc", + "manualCaption": "Manuelle Konfiguration in Arc", + "snMacAlt": "Arc SN/MAC", + "snMacCaption": "Arc SN / MAC-Konfiguration", + "portmapAlt": "Arc SATA portmap", + "portmapCaption": "Arc SATA Portmap (verwenden Sie die empfohlene Option)", + "addonsAlt": "Arc-Addons", + "addonsCaption": "Arc Addons Auswahl" + }, + "rr": [ + { + "alt": "RR-Addon — Schritt 1", + "caption": "RR-Addon — Schritt 1" + }, + { + "alt": "RR-Addon — Schritt 2", + "caption": "RR-Addon — Schritt 2: Drücken Sie, um ein Addon hinzuzufügen" + }, + { + "alt": "RR-Addon — Schritt 3", + "caption": "RR-Addon - Schritt 3: Klicken Sie auf das gewünschte Addon. Wiederholen Sie die beiden vorherigen Screenshots, um mehr hinzuzufügen." + } + ], + "tinycore": [ + { + "alt": "TinyCore SN-Konfiguration", + "caption": "TinyCore SN-Konfiguration" + }, + { + "alt": "TinyCore Zufallsoption", + "caption": "TinyCore Zufallsoption (empfohlen)" + }, + { + "alt": "TinyCore MAC Konfiguration", + "caption": "TinyCore MAC Konfiguration" + }, + { + "alt": "TinyCore VM MAC", + "caption": "TinyCore — Wählen Sie den MAC des VM oder einen zufälligen" + } + ] + }, + "step5": { + "title": "Bauen Sie den Loader", + "introRich": "Wenn das Modell, die DSM-Version und die Addons ausgewählt sind, wählen Sie Build the Loader. Dies kann je nach Loader und den ausgewählten Optionen einige Minuten dauern.", + "arc": { + "alt": "Loader in Arc erstellen", + "caption": "Loader in Arc erstellen" + }, + "rr": { + "alt": "Loader in RR erstellen", + "caption": "Loader in RR erstellen" + }, + "tinycore": { + "alt": "Loader in TinyCore erstellen", + "caption": "Loader in TinyCore erstellen" + } + }, + "step6": { + "title": "Loader starten", + "intro": "Sobald der Loader erstellt ist, startet der VM mit der von Ihnen erstellten Konfiguration neu und die DSM-Installation beginnt.", + "arc": { + "alt": "Loader in Arc starten", + "caption": "Loader in Arc starten" + }, + "rr": { + "alt": "Loader in RR starten", + "caption": "Loader in RR starten" + }, + "tinycore": { + "alt": "Loader in TinyCore starten", + "caption": "Loader in TinyCore starten" + } + }, + "dsmInstall": { + "heading": "Starten der DSM-Installation", + "intro": "Nach dem Loader-Start finden Sie Ihre DSM-VM, indem Sie Folgendes öffnen:", + "afterCode": "Folgen Sie den Anweisungen auf dem Bildschirm, um die DSM-Installation abzuschließen.", + "setupAlt": "DSM-Einrichtung", + "setupCaption": "DSM-Einrichtungsbildschirm", + "patience": "Seien Sie geduldig - der Prozess kann mehrere Minuten dauern. Fortschritt wird automatisch aktualisiert und ein Countdown wird angezeigt, wenn sich die Installation dem Abschluss nähert.", + "finishAlt": "Installation abgeschlossen", + "finishCaption": "Installation abgeschlossen" + }, + "tips": { + "heading": "Tipps", + "recentTitle": "Verwenden Sie aktuelle DSM-Modelle", + "recentBody": "Einige ältere DSM-Modelle haben Schwierigkeiten, Festplatten oder die Netzwerkkarte zu erkennen. Bevorzugt aktuelle Modelle (z.B. SA6400) für ein reibungsloseres First-Boot-Erlebnis.", + "updateTitle": "Aktualisieren des Loaders", + "updateBody": "Einige Loader bieten die Möglichkeit, sich direkt aus ihrem Konfigurationsmenü zu aktualisieren - Sie müssen den VM nicht zerstören und neu erstellen.", + "warnTitle": "ProxMenux unterstützt die Loader nicht", + "warnBody": "ProxMenux automatisiert die VM-Seite. Loader-Probleme (Modell nicht gefunden, Add-on nicht installiert, DSM weigert sich zu booten) sind hier außerhalb des Umfangs - verwenden Sie den eigenen Issue-Tracker und die Dokumentation jedes Loaders.", + "docsHeading": "Loader-Dokumentation", + "docLinks": [ + { + "label": "Arc-Dokumentation", + "url": "https://github.com/AuxXxilium/arc" + }, + { + "label": "RR-Dokumentation", + "url": "https://github.com/RROrg/rr" + }, + { + "label": "TinyCore Dokumentation", + "url": "https://github.com/PeterSuh-Q3/tinycore-redpill" + } + ] + }, + "related": { + "heading": "Verwandte Themen", + "itemsRich": [ + { + "href": "/docs/create-vm/system-nas", + "label": "Übersicht über System NAS", + "tail": "— Vergleich von Synology vs TrueNAS / OMV / Rockstor / XigmaNAS." + }, + { + "href": "/docs/create-vm/system-nas/system-nas-others", + "label": "Sonstige System-NAS", + "tail": " — andere NAS-Distributionen als Synology." + }, + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": "- Reale Festplatten an Synology VM übergeben (empfohlen für die Speicherleistung)." + }, + { + "href": "/docs/create-vm", + "label": "Übersicht zur VM-Erstellung", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/create-vm/system-nas/system-nas-others.json b/web/messages/de/docs/create-vm/system-nas/system-nas-others.json new file mode 100644 index 00000000..c7ee363a --- /dev/null +++ b/web/messages/de/docs/create-vm/system-nas/system-nas-others.json @@ -0,0 +1,312 @@ +{ + "meta": { + "title": "VM erstellen: Andere NAS-Systeme | ProxMenux Dokumentation", + "description": "Erstellen Sie TrueNAS SCALE, TrueNAS CORE, OpenMediaVault, XigmaNAS, Rockstor, ZimaOS und FygoOS virtuelle Maschinen auf Proxmox VE mit ProxMenux. Deckt die Standard- / erweiterte Konfiguration, den neuen Speicherplan, den optionalen GPU-Durchgang und die empfohlenen Spezifikationen pro System ab.", + "ogTitle": "VM erstellen: Andere NAS-Systeme | ProxMenux Dokumentation", + "ogDescription": "Erstellen Sie TrueNAS SCALE / CORE, OpenMediaVault, XigmaNAS, Rockstor, ZimaOS und FygoOS VMs auf Proxmox VE mit ProxMenux.", + "ogImageAlt": "ProxMenux NAS-Selektor" + }, + "header": { + "title": "VM erstellen: Andere NAS-Systeme", + "description": "ProxMenux erstellt eine installationsbereite VM für TrueNAS SCALE, TrueNAS CORE, OpenMediaVault, XigmaNAS, Rockstor, ZimaOS und FygoOS. Diese Seite beschreibt den gemeinsamen Ablauf mit Standard- und erweiterten Einstellungen, Speicherplan und optionalem GPU-Passthrough sowie Hinweise und Ressourcenempfehlungen pro System.", + "section": "Virtuelle Maschinen · NAS" + }, + "intro": { + "title": "Wie diese Systeme entstehen", + "bodyRich": "Alle sieben Systeme teilen sich den gleichen ProxMenux-Assistenten. Der NAS-Selektor wählt den passenden Installer aus (erkennt automatisch die neueste stabile Version für TrueNAS, OMV, XigmaNAS, Rockstor und FygoOS) und übergibt dann den generischen VM-Konfigurator und den Speicherplan. ZimaOS verwendet ein eigenes Installationsskript (vm/zimaos.sh), folgt jedoch der gleichen CPU/RAM/Speicherung/optional-GPU-Sequenz." + }, + "config": { + "heading": "Standard- oder erweiterte Konfiguration", + "intro": "Wenn ein NAS ausgewählt ist, fragt ProxMenux, ob das standardmäßige VM-Profil verwendet oder der erweiterte Wizard geöffnet werden soll. Die Standardeinstellungen sind auf NAS-Workloads abgestimmt - mehr RAM als ein einfaches Linux VM, da ZFS-basierte Systeme es für den ARC-Cache benötigen.", + "defaultHeading": "Standardkonfiguration", + "headerParam": "Parameter", + "headerValue": "Standardwert", + "defaultRowsRich": [ + { + "param": "Maschinentyp", + "valueRich": "q35" + }, + { + "param": "BIOS", + "valueRich": "OVMF (UEFI) — SeaBIOS für OpenMediaVault" + }, + { + "param": "CPU-Typ", + "valueRich": "Host" + }, + { + "param": "Kerne", + "valueRich": "2" + }, + { + "param": "RAM", + "valueRich": "8192 MB — 4096 MB für ZimaOS" + }, + { + "param": "Brücke", + "valueRich": "vmbr0" + }, + { + "param": "MAC-Adresse", + "valueRich": "Autogeneriert" + }, + { + "param": "Nach Abschluss starten", + "valueRich": "Nein" + } + ], + "advancedHeading": "Erweiterte Konfiguration", + "advancedIntro": "Der erweiterte Wizard stellt jeden Parameter einzeln dar:", + "headerOptions": "Optionen", + "advancedRowsRich": [ + { + "param": "Maschinentyp", + "optionsRich": "q35 oder i440fx" + }, + { + "param": "BIOS", + "optionsRich": "OVMF (UEFI) oder SeaBIOS (Legacy)" + }, + { + "param": "CPU-Typ", + "optionsRich": "Host, KVM64 und weitere aufgeführte CPU-Modelle (ZimaOS: Host oder KVM64)" + }, + { + "param": "Kerne", + "optionsRich": "Anzahl CPU-Kerne" + }, + { + "param": "RAM", + "optionsRich": "Speicher zugewiesen VM" + }, + { + "param": "Brücke", + "optionsRich": "Netzbrücke" + }, + { + "param": "MAC-Adresse", + "optionsRich": "Eigene MAC-Adresse" + }, + { + "param": "VLAN", + "optionsRich": "VLAN-Tag (fakultativ)" + }, + { + "param": "MTU", + "optionsRich": "MTU-Größe" + } + ], + "zfsCalloutTitle": "ZFS und RAM", + "zfsCalloutBody": "TrueNAS SCALE/CORE und XigmaNAS nutzen ZFS, dessen ARC-Cache von RAM profitiert. TrueNAS benötigt mindestens 8 GB für den Grundbetrieb; 16 GB oder mehr helfen bei anspruchsvollen Diensten und Apps. Bemessen Sie die VM nach Distribution und Last; 8 GB sind kein allgemeines ZFS-Minimum. Schlanke OMV-, Rockstor- oder ZimaOS-Systeme können weniger benötigen." + }, + "storagePlan": { + "heading": "Speicherplan", + "intro": "Nach CPU / RAM öffnet ProxMenux das Speicherplanmenü, in dem Sie virtuelle Festplatten, importierte Festplatten und PCI-Durchlaufgeräte im selben VM kombinieren können, indem Sie Elemente einzeln hinzufügen und beenden, wenn Sie fertig sind.", + "virtualHeading": "a. Virtuelle Festplatte hinzufügen", + "virtualItemsRich": [ + "Listen Sie die auf dem Host verfügbaren Proxmox-Speichervolumen auf.", + "Sie wählen den Zielspeicher und die Größe in GB aus.", + "Das Laufwerk nutzt den gewählten Bus: SCSI (Standard), SATA, VirtIO oder IDE (ZimaOS: SCSI oder SATA). SATA hat sechs Anschlüsse von 0 bis 5: sata0, sata1, …." + ], + "importHeading": "b. Physisches Laufwerk hinzufügen", + "importItemsRich": [ + "Erkennt physische Festplatten, die sicher importiert werden können (System und geschützte Festplatten sind verborgen).", + "Sie wählen eine oder mehrere Festplatten über eine Checkliste aus.", + "Importierte Laufwerke nutzen den gewählten Bus und werden über qm set zugewiesen." + ], + "pciHeading": "c. Controller oder NVMe hinzufügen (PCI-Passthrough)", + "pciItemsRich": [ + "Erkennt SATA-/SAS-HBAs und NVMe-Geräte und ermöglicht die Zuweisung eines ganzen Controllers an die VM. IOMMU muss aktiv sein; das Skript bietet die Aktivierung und bei Bedarf einen Neustart an.", + "Ein Bestätigungsschritt warnt vor einem kontrollerweiten Risiko (die gesamte Karte, einschließlich jeder Festplatte, verlässt den Host).", + "PCI-Geräte werden mit Befehlen wie qm set VMID --hostpci0 PCI_ADDRESS,pcie=1 zugewiesen. Ersetzen Sie VMID und PCI_ADDRESS durch VM-ID und PCI-Adresse und verwenden Sie für jedes Gerät einen freien hostpci-Index." + ], + "resetCalloutTitle": "Zurücksetzen und abschließen", + "resetCalloutBodyRich": "Das Menü bietet r zum Zurücksetzen und Neubeginnen sowie d zum Abschließen und Fortfahren. Ein leerer Plan kann nicht abgeschlossen werden." + }, + "gpu": { + "heading": "Optionaler GPU-Durchgang", + "bodyRich": "Nach dem Speicherplan bietet ProxMenux bei einer geeigneten GPU den Passthrough-Assistenten an. Siehe GPU zu einer VM hinzufügen (Passthrough). Ein Host-Neustart kann erforderlich sein." + }, + "autoFeatures": { + "heading": "Automatische VM-Funktionen", + "efiTitle": "EFI-Laufwerk", + "efiBody": "Bei OVMF (UEFI) erstellt ProxMenux automatisch ein 4-MB-EFI-Laufwerk auf dem gewählten Speicher, mit einem zum Speichertyp passenden Format, etwa raw bei Verzeichnisspeicher.", + "isoTitle": "ISO-Anschluss", + "isoBodyRich": "Die Installations-ISO wird heruntergeladen (falls erforderlich) und als CD-ROM an ide2 angehängt, bereit zum ersten Booten.", + "guestTitle": "QEMU Gastagent", + "guestBody": "Der Gastagentenkanal ist in der VM-Konfiguration aktiviert. Sie müssen den Agenten immer noch im Gast installieren, nachdem das NAS-Betriebssystem aktiviert ist." + }, + "endToEnd": { + "heading": "Was das Skript Ende-zu-Ende macht", + "itemsRich": [ + "Erkennt und lädt die neueste stabile ISO herunter (oder verwendet die Fallback-Version, wenn der Upstream nicht erreichbar ist).", + "Wendet den standardmäßigen oder erweiterten CPU / RAM / BIOS / Maschinentyp an.", + "Öffnet den Speicherplan und verbindet die ausgewählten virtuellen Festplatten, importierten Festplatten und PCI-Geräte.", + "Erstellt eine EFI-Disk, wenn UEFI ausgewählt ist.", + "Befestigt die Installation ISO auf ide2.", + "Legt die Bootreihenfolge fest: zuerst Laufwerk, dann ISO.", + "Aktiviert den QEMU Guest Agent Kanal.", + "Optional läuft der GPU Passthrough-Assistent.", + "Generiert eine stilisierte HTML-Beschreibung, die dem VM beigefügt ist.", + "Startet den VM, wenn Sie sich dafür entschieden haben." + ] + }, + "perSystem": { + "heading": "Systembezogene Anmerkungen", + "shellLabel": "Shell-Schnittstelle", + "webLabel": "Webschnittstelle" + }, + "systems": { + "truenasScale": { + "id": "truenas-scale", + "title": "TrueNAS SCALE", + "icon": "Database", + "officialName": "TrueNAS SCALE", + "officialUrl": "https://www.truenas.com/truenas-scale/", + "description": "Debian-basiertes TrueNAS mit Docker-Apps in aktuellen Versionen, Linux KVM und ZFS als primärem Dateisystem. Ältere SCALE-Versionen nutzten Kubernetes. Es verbindet ZFS-Speicher mit einem Container-Ökosystem.", + "specs": [ + "Empfohlene Schnittstelle: SATA oder SCSI (beide unterstützen Discard/Trim)", + "RAM: 8 GB (16 GB+ für echte Workloads empfohlen)", + "Mindestens CPU Kerne: 2 (4+ empfohlen)", + "UEFI (OVMF) Boot empfohlen", + "VirtIO Netzwerkadapter für besten Durchsatz" + ], + "shellImg": "/vm/truenas/truenas-scale-shell.png", + "webImg": "/vm/truenas/truenas-scale-web.png", + "shellAlt": "TrueNAS SCALE Shell-Schnittstelle", + "webAlt": "TrueNAS SCALE Web-Schnittstelle" + }, + "truenasCore": { + "id": "truenas-core", + "title": "TrueNAS CORE", + "icon": "Database", + "officialName": "TrueNAS CORE", + "officialUrl": "https://www.truenas.com/truenas-core/", + "description": "FreeBSD-basiertes TrueNAS, früher FreeNAS, mit Schwerpunkt ZFS. Jails, Plugins und Virtualisierung in CORE 13.3 sind veraltet und nicht unterstützt; die Entwicklung konzentriert sich auf Linux-basierte Ausgaben.", + "specs": [ + "Empfohlenes Interface: SATA", + "RAM: 8 GB (16 GB+ empfohlen)", + "Mindestens CPU Kerne: 2 (4+ empfohlen)", + "UEFI (OVMF) Boot empfohlen", + "VirtIO Netzwerkadapter für besten Durchsatz" + ], + "shellImg": "/vm/truenas/truenas-core-shell.png", + "webImg": "/vm/truenas/truenas-core-web.png", + "shellAlt": "TrueNAS CORE Shell-Schnittstelle", + "webAlt": "TrueNAS-CORE-Weboberfläche" + }, + "openmediavault": { + "id": "openmediavault", + "title": "OpenMediaVault", + "icon": "Server", + "officialName": "openmediavault.org", + "officialUrl": "https://www.openmediavault.org", + "description": "Debian-basiertes NAS mit modularen Plugins. Ressourcenschonend und gut für bescheidene Hardware geeignet, wenn ZFS nicht benötigt wird.", + "specs": [ + "Empfohlene Schnittstelle: SATA oder VirtIO", + "RAM: 2 GB (4 GB+ empfohlen)", + "Mindestens CPU Kerne: 1 (2+ empfohlen)", + "BIOS: ProxMenux standardmäßig auf SeaBIOS für OMV (bei Bedarf in Advanced überschreiben)", + "VirtIO Netzwerkadapter für besten Durchsatz" + ], + "shellImg": "/vm/openmediavault/openmediavault-shell.png", + "webImg": "/vm/openmediavault/openmediavault-web.png", + "shellAlt": "OpenMediaVault Shell-Schnittstelle", + "webAlt": "OpenMediaVault Web-Schnittstelle" + }, + "xigmanas": { + "id": "xigmanas", + "title": "XigmaNAS", + "icon": "Database", + "officialName": "xigmanas.com", + "officialUrl": "https://www.xigmanas.com", + "description": "FreeBSD-basierte NAS – die direkte Fortsetzung der ursprünglichen FreeNAS-Codebasis (0.7). Konzentriert auf ZFS und Einfachheit, mit einem leichteren Footprint als TrueNAS.", + "specs": [ + "Empfohlene Schnittstelle: SATA oder SCSI", + "RAM: 4 GB (8 GB+ empfohlen für ZFS)", + "Mindestens CPU Kerne: 2", + "UEFI (OVMF) Boot empfohlen", + "VirtIO Netzwerkadapter für besten Durchsatz" + ] + }, + "rockstor": { + "id": "rockstor", + "title": "Rockstor", + "icon": "HardDrive", + "officialName": "rockstor.com", + "officialUrl": "https://rockstor.com", + "description": "openSUSE Leap-basierte NAS rund um Btrfs. Snapshots, Subvolumes und das Rock-ons App Framework (Docker) für Medien, Filesharing und Backup.", + "specs": [ + "Empfohlene Schnittstelle: SATA oder VirtIO", + "RAM: 2 GB (4 GB+ empfohlen)", + "Mindestens CPU Kerne: 2", + "UEFI (OVMF) Boot empfohlen", + "VirtIO Netzwerkadapter für besten Durchsatz" + ], + "shellImg": "/vm/rockstor/rockstor-shell.png", + "webImg": "/vm/rockstor/rockstor-web.png", + "shellAlt": "Rockstor Shell-Schnittstelle", + "webAlt": "Rockstor Web-Schnittstelle" + }, + "zimaos": { + "id": "zimaos", + "title": "ZimaOS", + "icon": "MonitorIcon", + "officialName": "zimaspace.com", + "officialUrl": "https://www.zimaspace.com/zimaos", + "description": "Schlankes NAS-/Home-Server-System auf Buildroot-Basis, aus CasaOS hervorgegangen, für Medien, Docker-Apps und Hausautomation. Der eigene ProxMenux-Installer (vm/zimaos.sh) lädt ein vorgefertigtes Image und bietet CPU-, RAM- und Speicherkonfiguration.", + "specs": [ + "Empfohlene Schnittstelle: SATA oder VirtIO", + "Standard RAM: 4 GB (genug für Medien / selbst gehostete Apps)", + "Mindestens CPU Kerne: 2", + "UEFI-Start (OVMF)", + "VirtIO Netzwerkadapter für besten Durchsatz" + ] + }, + "fygoos": { + "id": "fygoos", + "title": "FygoOS", + "icon": "Server", + "officialName": "fygonas.com", + "officialUrl": "https://fygonas.com", + "description": "Internationale Ausgabe des Debian-basierten fnOS. ProxMenux lädt die neueste Installer-ISO von fygonas.com und bindet sie in die VM ein. Das System belegt ein Laufwerk; der übrige Platz kann App- und Dateivolumes aufnehmen. Nach der Installation zeigt die Konsole die Webadresse auf Port 5666 (Port 80 leitet dorthin um). Öffnen Sie sie, um das Gerät zu benennen und das Administratorkonto anzulegen.", + "specs": [ + "Systemdiskette: FygoOS schlägt eine 64 GB Systempartition vor, die in einer VM kleiner sein kann", + "Installation: Graphical Install oder Rescuing Install, falls der grafische Modus scheitert", + "UEFI (OVMF) booten ohne Secure Boot Keys, wie es der Installer benötigt", + "Netzwerk: Ethernet mit DHCP standardmäßig", + "VirtIO Netzwerkadapter für besten Durchsatz" + ] + } + }, + "related": { + "heading": "Verwandte Themen", + "itemsRich": [ + { + "href": "/docs/create-vm/system-nas", + "label": "Übersicht über System NAS", + "tail": "Schneller Vergleich aller NAS-Optionen." + }, + { + "href": "/docs/create-vm/system-nas/synology", + "label": "Synology-VM", + "tail": "- für den Synology DSM (Xpenology) Flow." + }, + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": "- empfohlen für NAS-Anwendungsfälle (echte Festplatten > virtuelle Festplatten)." + }, + { + "href": "/docs/help-info/zfs-commands", + "label": "ZFS Verwaltungsbefehle", + "tail": "— wenn Sie TrueNAS oder eine andere ZFS-unterstützte Distribution auswählen." + }, + { + "href": "/docs/create-vm", + "label": "Übersicht zur VM-Erstellung", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/create-vm/system-windows.json b/web/messages/de/docs/create-vm/system-windows.json new file mode 100644 index 00000000..6fed57f7 --- /dev/null +++ b/web/messages/de/docs/create-vm/system-windows.json @@ -0,0 +1,259 @@ +{ + "meta": { + "title": "VM erstellen: Windows-System | ProxMenux-Dokumentation", + "description": "Erstellen und konfigurieren Sie virtuelle Windows-Maschinen auf Proxmox VE mit ProxMenux. Enthält UUP Dump ISO- und lokale ISO-Pfade, Standard / erweiterte Konfiguration, den einheitlichen Speicherplan, optionalen GPU-Passthrough, TPM 2.0-Setup und VirtIO-Treiberinstallation.", + "ogTitle": "VM erstellen: Windows-System | ProxMenux-Dokumentation", + "ogDescription": "Erstellen Sie Windows-VMs auf Proxmox VE mit ProxMenux: UUP Dump oder lokale ISO, gemeinsamer Speicherplan, optionales GPU-Passthrough, TPM 2.0 und VirtIO-Treiber.", + "ogImageAlt": "ProxMenux Windows VM Menü" + }, + "header": { + "title": "VM erstellen: Windows-System", + "description": "Erstellen Sie eine Windows-VM auf Proxmox VE. ProxMenux führt durch ISO-Auswahl (UUP Dump oder lokal), CPU, RAM, Maschinentyp, gemeinsamen Speicherplan, optionales GPU-Passthrough, TPM 2.0 und die Installation der VirtIO-Treiber.", + "section": "Virtuelle Maschinen" + }, + "intro": { + "title": "Was dieses Skript macht", + "body": "Der Windows-Ablauf fragt nach der ISO-Quelle: UUP Dump oder eine lokale Kopie unter /var/lib/vz/template/iso. Danach startet der gemeinsame VM-Assistent mit Windows-Vorgaben: q35, OVMF und TPM 2.0. Die VirtIO-Treiber-ISO wird als zweites CD-ROM eingebunden; die VM ist damit bereit für den Windows-Installer." + }, + "image": { + "alt": "ProxMenux Windows Installationsoptionen", + "caption": "Windows-Installationsoptionen" + }, + "config": { + "heading": "Standard- oder erweiterte Konfiguration", + "intro": "Nachdem die ISO ausgewählt wurde, fragt ProxMenux, ob das Standardprofil VM verwendet oder der erweiterte Wizard geöffnet werden soll. Das Standardprofil ist auf moderne Windows Desktop- und Server-Editionen abgestimmt.", + "defaultHeading": "Standardkonfiguration", + "headerParam": "Parameter", + "headerValue": "Standardwert", + "defaultRows": [ + { + "param": "Maschinentyp", + "valueRich": "q35" + }, + { + "param": "BIOS", + "value": "OVMF (UEFI)" + }, + { + "param": "CPU-Typ", + "value": "Host" + }, + { + "param": "Kerne", + "value": "4" + }, + { + "param": "RAM", + "value": "8192 MB" + }, + { + "param": "Brücke", + "valueRich": "vmbr0" + }, + { + "param": "MAC-Adresse", + "value": "Autogeneriert" + }, + { + "param": "TPM", + "value": "Aktiviert (v2.0)" + }, + { + "param": "Nach Abschluss starten", + "value": "Nein" + } + ], + "advancedHeading": "Erweiterte Konfiguration", + "advancedIntro": "Der erweiterte Assistent zeigt jede Einstellung einzeln:", + "headerOptions": "Optionen", + "advancedRows": [ + { + "param": "Maschinentyp", + "optionsRich": "q35 oder i440fx" + }, + { + "param": "BIOS", + "options": "OVMF (UEFI) oder SeaBIOS (Legacy)" + }, + { + "param": "CPU-Typ", + "options": "Host, KVM64 und weitere aufgeführte CPU-Modelle" + }, + { + "param": "Kerne", + "options": "Anzahl CPU-Kerne" + }, + { + "param": "RAM", + "options": "Der VM zugewiesener Speicher" + }, + { + "param": "Brücke", + "options": "Netzbrücke" + }, + { + "param": "MAC-Adresse", + "options": "Eigene MAC-Adresse" + }, + { + "param": "VLAN", + "options": "VLAN-Tag (fakultativ)" + }, + { + "param": "MTU", + "options": "MTU-Größe" + }, + { + "param": "TPM", + "options": "TPM 2.0 wird für Windows automatisch hinzugefügt; der Assistent hat dafür keinen Schalter." + } + ], + "tpmWarnTitle": "TPM-Anforderungen hängen von der Windows-Ausgabe ab", + "tpmWarnBody": "Windows 11 benötigt normalerweise TPM 2.0. Windows Server 2022 benötigt es für bestimmte Sicherheitsfunktionen, nicht für jede Installation. ProxMenux fügt für Windows ein virtuelles TPM hinzu; der erweiterte Assistent bietet dafür keinen Schalter." + }, + "storagePlan": { + "heading": "Speicherplan", + "body": "Nach CPU / RAM öffnet ProxMenux das Storage Plan-Menü, in dem Sie virtuelle Festplatten, importierte Festplatten und PCI-Durchgangsgeräte im selben VM kombinieren können, indem Sie Elemente einzeln hinzufügen und beenden, wenn Sie fertig sind.", + "virtualDiskTitle": "a. Virtuelle Festplatte hinzufügen", + "virtualDiskItems": [ + "Listet die auf dem Host verfügbaren Proxmox-Speicher auf.", + "Sie wählen den Zielspeicher und die Größe in GB aus.", + "Das Laufwerk nutzt den gewählten Bus: SCSI (Standard), SATA, VirtIO oder IDE. SATA hat sechs Anschlüsse von 0 bis 5: sata0, sata1, …." + ], + "importDiskTitle": "b. Physisches Laufwerk hinzufügen", + "importDiskItems": [ + "Erkennt physische Festplatten, die sicher importiert werden können (System und geschützte Festplatten sind verborgen).", + "Sie wählen eine oder mehrere Festplatten über eine Checkliste aus.", + "Importierte Laufwerke nutzen den gewählten Bus und werden über qm set zugewiesen." + ], + "pciTitle": "c. Controller oder NVMe hinzufügen (PCI-Passthrough)", + "pciItems": [ + "Erkennt SATA-/SAS-HBAs und NVMe-Geräte und ermöglicht die Zuweisung eines ganzen Controllers an die VM. IOMMU muss aktiv sein; das Skript bietet die Aktivierung und bei Bedarf einen Neustart an.", + "Ein Bestätigungsschritt warnt vor einem kontrollerweiten Risiko (die gesamte Karte, einschließlich jeder Festplatte, verlässt den Host).", + "PCI-Geräte werden mit Befehlen wie qm set VMID --hostpci0 PCI_ADDRESS,pcie=1 zugewiesen. Ersetzen Sie VMID und PCI_ADDRESS durch VM-ID und PCI-Adresse und verwenden Sie für jedes Gerät einen freien hostpci-Index." + ], + "resetTitle": "Zurücksetzen und abschließen", + "resetBody": "Das Menü bietet r zum Zurücksetzen und Neubeginnen sowie d zum Abschließen und Fortfahren. Ein leerer Plan kann nicht abgeschlossen werden." + }, + "gpu": { + "heading": "Optionaler GPU-Durchgang", + "body": "Nach dem Speicherplan bietet ProxMenux bei einer geeigneten GPU den Passthrough-Assistenten an. Siehe GPU zu einer VM hinzufügen (Passthrough). Ein Host-Neustart kann erforderlich sein." + }, + "autoFeatures": { + "heading": "Automatische VM-Funktionen", + "efiTitle": "EFI-Laufwerk", + "efiBody": "Wenn OVMF (UEFI) ausgewählt ist, erstellt ProxMenux eine 4 MB EFI-Systemplatte auf dem Speicher, den Sie auswählen, und fügt sie an die VM an, so dass die UEFI-Firmware einen Platz zum Speichern ihrer Variablen hat.", + "tpmTitle": "TPM 2.0", + "tpmBody": "ProxMenux fügt Windows-VMs ein virtuelles TPM 2.0 hinzu. Es erfüllt die TPM-Anforderung von Windows 11 und ermöglicht TPM-abhängige Funktionen in Windows Server.", + "isoTitle": "ISO-Anschluss", + "isoBody": "Die Installations-ISO wird an ide2 eingebunden. Die VirtIO-Treiber-ISO wird bei Bedarf heruntergeladen und an ide3 eingebunden, bereit für Treiber laden.", + "guestTitle": "QEMU Gastagent", + "guestBody": "Der Gastagentenkanal ist in der VM-Konfiguration aktiviert. Der Agent selbst wird in Windows installiert, indem virtio-win-guest-tools.exe nach der Einrichtung ausgeführt wird." + }, + "installOptions": { + "heading": "Windows-Installationsoptionen", + "intro": "ProxMenux bietet zwei Methoden, um die Windows-Installationsmedien zu erhalten:", + "uupTitle": "UUP-Dump-ISO-Erstellung", + "uupLogoAlt": "UUP Dump Logo", + "uupBody": "Das UUP Dump ISO Creator-Skript ist ein ProxMenux-Dienstprogramm, das eine Windows ISO im laufenden Betrieb von Microsofts Windows Update-Servern erstellt. Es zieht saubere, offizielle Installationsdateien (einschließlich Insider Preview Builds).", + "uupItems": [ + "Zugriff auf die neuesten Windows Builds.", + "Unterstützt Insider Preview Versionen.", + "Saubere, offizielle Microsoft Installationsdateien.", + "Automatische ISO Erstellung und Montage im VM.", + "Unterstützt Home, Pro und Enterprise Editionen." + ], + "uupLearnMore": "Erfahren Sie mehr über UUP Dump ISO Creator", + "localTitle": "Installieren mit lokaler ISO", + "localBody": "Verwendet eine Windows-ISO, die Sie bereits in den lokalen Speicher des Hosts hochgeladen haben (/var/lib/vz/template/iso). Das Skript listet jede Nicht-VirtIO ISO auf, die es findet, und lässt Sie eine auswählen.", + "localImageAlt": "Lokales ISO-Auswahlmenü", + "localImageCaption": "Lokales ISO-Auswahlmenü" + }, + "endToEnd": { + "heading": "Was das Skript Ende-zu-Ende macht", + "items": [ + "Bezieht die Windows-ISO aus UUP Dump oder einer lokalen Kopie.", + "Wendet die Standard- oder erweiterte CPU / RAM / BIOS / Maschinentypkonfiguration an.", + "Öffnet den Speicherplan und verbindet die ausgewählten virtuellen Festplatten, importierten Festplatten und PCI-Geräte.", + "Erstellt bei UEFI-Installationen das EFI-Laufwerk und weist das TPM-2.0-Gerät zu.", + "Befestigt die Windows ISO auf ide2.", + "Lädt die VirtIO-Treiber-ISO herunter und bindet sie an ide3 ein.", + "Legt die Bootreihenfolge fest: zuerst Laufwerk, dann ISO.", + "Aktiviert den QEMU Guest Agent Kanal.", + "Optional läuft der GPU Passthrough-Assistent.", + "Generiert eine stilisierte HTML-Beschreibung, die dem VM beigefügt ist.", + "Startet die VM, wenn Sie dies gewählt haben." + ] + }, + "virtio": { + "heading": "VirtIO Treiber während der Installation", + "body": "Der VM-Ersteller nutzt standardmäßig SCSI-Laufwerke mit VirtIO-SCSI-Controller und einen VirtIO-Netzwerkadapter. Eine normale Windows-ISO kann deren Treiber von der an ide3 eingebundenen VirtIO-ISO benötigen. SATA-Laufwerke funktionieren meist mit Windows-eigenen Treibern; SATA ändert den VirtIO-Netzwerkadapter nicht.", + "warnTitle": "VirtIO-Netzwerktreiber laden, wenn Windows kein Netzwerk hat", + "warnBody": "Die VM nutzt einen VirtIO-Netzwerkadapter (virtio). Enthält Windows dessen Treiber nicht, laden Sie ihn während der Installation für den Netzwerkzugriff. Sonst können Aktivierung und Windows Update beim ersten Start fehlen.", + "stepLabel": "Schritt", + "steps": [ + { + "title": "Zugriff auf den Bildschirm \"Wo möchten Sie Windows installieren?\"", + "body": "Wenn während der Windows-Einrichtung keine Festplatten angezeigt werden, ist der Speichertreiber für Ihre ausgewählte Schnittstelle (SCSI oder VirtIO) noch nicht geladen. Laden Sie es manuell von der montierten VirtIO ISO.", + "img": "/vm/windows/virtio-step-1.png", + "caption": "Windows-Setup ohne Festplatten angezeigt" + }, + { + "title": "Klicken Sie auf „Treiber laden“", + "bodyRich": "Klicken Sie auf Treiber laden, durchsuchen Sie die eingebundene VirtIO-ISO und wählen Sie den Speichertreiber für die gewählte Laufwerksschnittstelle.", + "img": "/vm/windows/virtio-step-2.png", + "caption": "Klicken Sie auf Load Driver, um den VirtIO ISO zu durchsuchen" + }, + { + "title": "Durchsuchen des richtigen Treiberordners", + "bodyRich": "Wählen Sie auf der VirtIO-ISO vioscsi für den VirtIO-SCSI-Controller oder viostor für VirtIO-Blockgeräte. Wählen Sie danach den Unterordner für Windows-Version und Architektur.", + "img": "/vm/windows/virtio-step-3.png", + "caption": "Durchsuchen des entsprechenden Treiberordners" + }, + { + "title": "Wählen Sie den Treiber", + "bodyRich": "Windows listet die verfügbaren Treiber auf. Wählen Sie diejenige, die zu Ihrem Setup passt (normalerweise Red Hat VirtIO SCSI-Controller) und klicken Sie auf Next.", + "img": "/vm/windows/virtio-step-4.png", + "caption": "Wählen Sie den VirtIO-Treiber aus, der zu Ihrer Festplattenschnittstelle passt" + }, + { + "title": "(Optional) Installieren Sie den Netzwerktreiber", + "bodyRich": "Wenn Sie auch VirtIO als Netzwerkschnittstelle ausgewählt haben, laden Sie den Netzwerktreiber aus dem NetKVM-Ordner derselben ISO und wählen Sie den Unterordner für Ihre Windows-Version aus. Ohne diesen Schritt installiert Windows ohne Internetzugang.", + "img": "/vm/windows/virtio-step-5.png", + "caption": "Laden Sie den VirtIO-Netzwerktreiber, um das Internet während der Installation zu aktivieren" + } + ], + "tipTitle": "Nach der Installation: virtio-win-guest-tools.exe ausführen", + "tipBody": "Öffnen Sie nach der Windows-Installation die VirtIO-ISO im Datei-Explorer und starten Sie virtio-win-guest-tools.exe. Es installiert die übrigen Treiber für Netzwerk, Anzeige, Eingabe und Ballooning sowie den QEMU Guest Agent in einem Durchgang." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/utils/UUp-Dump-ISO-Creator", + "label": "UUP-Dump-ISO-Erstellung", + "tail": " — aktuelle Windows-Installations-ISOs auf dem Proxmox-Host erstellen." + }, + { + "href": "/docs/disk-manager/import-disk-image-vm", + "label": "Disk-Image in VM importieren", + "tail": " — ein vorhandenes Windows-Disk-Image (.vmdk, .qcow2, .img oder .raw) in eine bestehende VM importieren; .vhdx vorher in ein unterstütztes Format konvertieren." + }, + { + "href": "/docs/hardware/gpu-vm-passthrough", + "label": "GPU zu VM hinzufügen (Passthrough)", + "tail": "- für Windows VMs mit hardwarebeschleunigtem GPU." + }, + { + "href": "/docs/help-info/vm-ct-commands", + "label": "VM und CT Verwaltungsbefehle", + "tail": "— qm CLI Referenz." + }, + { + "href": "/docs/create-vm", + "label": "Übersicht zur VM-Erstellung", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/add-controller-nvme-vm.json b/web/messages/de/docs/disk-manager/add-controller-nvme-vm.json new file mode 100644 index 00000000..5a9deee2 --- /dev/null +++ b/web/messages/de/docs/disk-manager/add-controller-nvme-vm.json @@ -0,0 +1,128 @@ +{ + "meta": { + "title": "Hinzufügen von Controller oder NVMe zu VM | ProxMenux Dokumentation", + "description": "PCI-Passthrough eines Speichercontrollers (SATA-/SAS-HBA) oder NVMe-Geräts an eine Proxmox-VM mit ProxMenux. Das Werkzeug aktiviert IOMMU, erkennt Konflikte und richtet die Zuweisung mit qm set --hostpci ein.", + "ogTitle": "Hinzufügen von Controller oder NVMe zu VM | ProxMenux Dokumentation", + "ogDescription": "Weisen Sie einer Proxmox-VM einen vollständigen SATA-/SAS-HBA oder ein NVMe-Gerät zu. ProxMenux übernimmt IOMMU, Konflikterkennung und die hostpci-Zuweisung." + }, + "header": { + "title": "Controller oder NVMe zu VM hinzufügen", + "description": "Weisen Sie einer Proxmox-VM einen vollständigen SATA-/SAS-Host-Bus-Adapter (HBA) oder ein NVMe-Gerät per PCI-Passthrough zu. ProxMenux prüft IOMMU und aktiviert es mit Ihrer Zustimmung, ermittelt geeignete Geräte, schließt Konflikte aus und trägt die Zuweisung mit qm set --hostpci ein.", + "section": "Datenträgerverwaltung · VM" + }, + "intro": { + "title": "Was dies tut (und nicht tut)", + "body": "Dies ist Passthrough auf Geräteebene: Die PCIe-Karte wird der Kontrolle des Host-Kernels entzogen und an vfio-pci gebunden. Die VM sieht den Controller selbst statt einzeln durchgereichter Laufwerke und lädt ihren eigenen Treiber für dieses Modell. Alle am Controller angeschlossenen Laufwerke folgen automatisch, mit nativen SMART-Daten, vollständigen Firmware-Funktionen und ohne Proxmox-Zwischenschicht." + }, + "howRuns": { + "heading": "Wie das Skript läuft", + "body": "Der Fluss hat zwei Phasen mit klarer Trennung zwischen \"Sammeln von Informationen, Überprüfen von IOMMU und Auflisten von Kandidaten\" und \"Tatsächliches Anbringen des PCI-Geräts\". Bis zur endgültigen Bestätigung wird nichts an die VM-Konfiguration geschrieben." + }, + "iommu": { + "heading": "IOMMU-Gruppen - warum die gesamte Karte auf einmal geht", + "body": "Die Isolationsgrenze für PCI-Passthrough ist die IOMMU-Gruppe, die kleinste von der IOMMU der CPU isolierbare Einheit. Geräte derselben Gruppe lassen sich nicht voneinander isolieren. Teilt ein SATA-Controller die Gruppe mit einem USB-Controller, muss für SATA-Passthrough möglicherweise auch USB vom Host gelöst werden, oder Passthrough ist nicht möglich. Dabei werden nicht automatisch alle Geräte der Gruppe der VM zugewiesen.", + "outro": "ProxMenux zeigt die IOMMU-Gruppe neben jedem Gerät und warnt Sie, wenn eine Gruppe mehr als nur den gewünschten Controller trägt. Sie können weiterhin fortfahren, wenn die zusätzlichen Geräte in der Gruppe akzeptabel sind (z. B. alle Festplattencontroller) oder zurückgehen, wenn dies nicht der Fall ist." + }, + "prereqs": { + "heading": "Voraussetzungen", + "items": [ + "CPU und Mainboard unterstützen VT-d (Intel) oder AMD-Vi, und die Funktion ist in der Firmware (BIOS/UEFI) aktiviert. ProxMenux kann die Kernelkonfiguration aktivieren, aber keine Firmware-Einstellung umschalten.", + "Die Ziel-VM ist vor dem Anschließen von PCI-Geräten ausgeschaltet.", + "Der Controller/NVMe ist nicht derjenige, der das Proxmox Root-Dateisystem hält. Das Skript verbirgt alles, was vom Host verwendet wird.", + "Das Gastbetriebssystem hat Treiber für das Controllermodell. Linux unterstützt nahezu jeden HBA ab Werk; unter Windows muss der Herstellertreiber vor dem Start mit angeschlossenem Controller installiert sein." + ], + "warnTitle": "Live Migration ist nicht möglich", + "warnBody": "Ein VM mit PCI-Durchgang ist an den Knoten gebunden, der die Karte physisch hält. Live-Migration über Knoten wird fehlschlagen. Planen Sie Backups / Replikation entsprechend." + }, + "steps": { + "heading": "Schritt für Schritt", + "stepLabel": "Schritt", + "list": [ + { + "title": "Ziel-VM auswählen", + "bodyRich": "ProxMenux liest qm list und zeigt alle VMs an. Wählen Sie die VM, die den Controller erhalten soll.", + "img": "/disk/nvme-passthrough/vm-selection.png", + "alt": "VM Auswahldialog", + "caption": "VM Auswahldialog" + }, + { + "title": "IOMMU-Prüfung", + "bodyRich": "Wenn IOMMU auf der Kernel-Cmdline nicht aktiviert ist, bietet ProxMenux an, sie zu aktivieren. Es bearbeitet /etc/kernel/cmdline (für systemd-Boot) oder /etc/default/grub (für GRUB) und fügt den richtigen Parameter für Ihren CPU hinzu:", + "items": [ + "intel_iommu=on für Intel CPUs.", + "amd_iommu=on für AMD CPUs." + ], + "outro": "Nach dieser Änderung ist ein Neustart erforderlich. Das Skript bietet einen Neustart jetzt oder später an; Passthrough kann erst fortgesetzt werden, wenn die neue Kernel-Kommandozeile aktiv ist.", + "img": "/disk/nvme-passthrough/iommu-warning.png", + "alt": "Dialog „IOMMU erforderlich“", + "caption": "IOMMU Erforderlich — Angebot zum Aktivieren und Neustarten" + }, + { + "title": "Geräteaufzählung", + "body": "ProxMenux listet jeden Speichercontroller (SATA / SAS HBA) und jedes NVMe-Gerät auf dem PCI-Bus auf. Für jedes Gerät zeigt es:", + "items": [ + "PCI-Adresse (00:17.0, 01:00.0, ...).", + "Hersteller- und Gerätebeschreibung.", + "IOMMU-Gruppe - und eine ⚠ Warnung, wenn die Gruppe mit anderen, nicht verwandten Geräten geteilt wird.", + "Die aktuell am Controller angeschlossenen Laufwerke, damit Sie wissen, welche zur VM wechseln." + ], + "img": "/disk/nvme-passthrough/device-list.png", + "alt": "Controller / NVMe Geräteaufzählung", + "caption": "Controller / NVMe Geräteaufzählung mit IOMMU Gruppeninfo" + }, + { + "title": "Konflikterkennung", + "bodyRich": "Das Skript sperrt eindeutig unsichere Geräte: bereits einer anderen VM über hostpci zugewiesene Geräte, vom Host verwendete Geräte wie den Root-Laufwerkscontroller sowie Geräte in IOMMU-Gruppen, deren andere Mitglieder nicht sicher erkannt werden können. Geeignete Geräte stehen zur Auswahl.", + "img": "/disk/nvme-passthrough/conflict-warning.png", + "alt": "Konflikterkennungswarnung", + "caption": "Konflikterkennung: Gerät mit Angabe des Grundes gesperrt" + }, + { + "title": "Zuweisen und abschließen", + "bodyRich": "Für jedes gewählte Gerät führt ProxMenux qm set <VMID> --hostpciN <pci-addr>,pcie=1 aus und verwendet den nächsten freien hostpci-Slot. Wurde IOMMU in dieser Sitzung aktiviert, wird vor dem Start der VM an den erforderlichen Neustart erinnert.", + "img": "/disk/nvme-passthrough/assignment.png", + "alt": "Zusammenfassung der Zuweisung", + "caption": "Zusammenfassung der Zuweisung: belegte hostpciN-Slots und zugewiesene PCI-Adressen" + } + ] + }, + "manual": { + "heading": "Manuelles Äquivalent" + }, + "troubleshoot": { + "heading": "Fehlerbehebung", + "noGroupsTitle": "IOMMU aktiviert, aber keine Gruppen unter /sys/kernel/iommu_groups/", + "noGroupsBody": "Ein Neustart ist erforderlich, damit die Kernel-Cmdline wirksam wird. Wenn die Gruppen nach dem Neustart immer noch fehlen, ist VT-d / AMD-Vi wahrscheinlich in der Firmware deaktiviert - überprüfen Sie das BIOS / UEFI-Setup.", + "busyTitle": "VM startet nicht: \"hostpciN: device busy\"", + "busyBody": "Ein anderer Prozess hält das PCI-Gerät noch belegt. Häufige Ursachen sind eine abgestürzte VM, die es nicht freigegeben hat, oder der Host-Kernel, der es beim letzten Start wieder übernommen hat. Prüfen Sie mit lspci -nnk -s <addr> den gebundenen Treiber; es sollte vfio-pci sein. Ein vollständiger Neustart des Hosts behebt dies meist.", + "noDisksTitle": "Gast sieht den Controller, aber nicht die Festplatten", + "noDisksBody": "Im Gast fehlt der Treiber für das Controllermodell. Unter Linux tritt dies selten auf, unter Windows hingegen schon. Installieren Sie den Herstellertreiber, etwa von LSI/Broadcom für ältere SAS-HBAs, solange die Karte nicht zugewiesen ist, und starten Sie dann mit zugewiesener Karte neu.", + "sharedTitle": "Die IOMMU-Gruppe enthält ein nicht zugehöriges Gerät", + "sharedBody": "Möglichkeiten: (a) die Karte in einen anderen PCIe-Slot setzen, um ihre Gruppe zu ändern, (b) ACS override im Kernel aktivieren, um Gruppen aufzuteilen (Sicherheitskompromiss, vor der Anwendung recherchieren), oder (c) eine andere Karte in einer besser isolierten Gruppe wählen." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": "Alternatives Modell: Befestigen Sie eine physische Festplatte über Proxmox ohne vollständigen PCIe-Durchgang." + }, + { + "href": "/docs/hardware/gpu-vm-passthrough", + "label": "GPU zu VM hinzufügen (Passthrough)", + "tail": "- gleiche IOMMU / VFIO-Konzepte, die auf einen GPU angewendet wurden." + }, + { + "href": "/docs/help-info/gpu-commands", + "label": "GPU Durchgangsbefehle", + "tail": "IOMMU-Verifizierungsbefehle gelten auch für den Controller / NVMe-Durchgang." + }, + { + "href": "/docs/disk-manager", + "label": "Übersicht Disk Manager", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/format-disk.json b/web/messages/de/docs/disk-manager/format-disk.json new file mode 100644 index 00000000..24c14381 --- /dev/null +++ b/web/messages/de/docs/disk-manager/format-disk.json @@ -0,0 +1,142 @@ +{ + "meta": { + "title": "Physischen Datenträger formatieren oder löschen | ProxMenux-Dokumentation", + "description": "Physische Datenträger auf Proxmox VE mit ProxMenux löschen und formatieren. Vier Betriebsarten, strenge Sicherheitsfilter gegen Systemlaufwerke, eingehängte oder verwendete Laufwerke und doppelte Bestätigung mit Eingabe des vollständigen Pfads.", + "ogTitle": "Physischen Datenträger formatieren oder löschen | ProxMenux-Dokumentation", + "ogDescription": "Physische Datenträger auf Proxmox löschen oder formatieren. Sicherheitsfilter, vier Betriebsarten und doppelte Bestätigung." + }, + "header": { + "title": "Physischen Datenträger formatieren / löschen", + "description": "Wischen oder formatieren Sie sicher eine physische Festplatte auf dem Proxmox-Host. ProxMenux zeigt nur Festplatten an, die völlig frei sind (keine Systemnutzung, keine Gastreferenz, keine Mounts) und erzwingt eine doppelte Bestätigung - ja / nein plus Eingabe des vollständigen Festplattenpfads -, bevor Daten zerstört werden.", + "section": "Disk Manager · Dienstprogramme" + }, + "danger": { + "title": "Zerstörendes Werkzeug", + "body": "Jede Operation hier schreibt auf die Festplatte. Es gibt kein Rückgängigmachen. ProxMenux wendet strenge Sicherheitsfilter und Bestätigungen an, aber die Verantwortung für die Auswahl der richtigen Festplatte liegt bei Ihnen. Lesen Sie den Festplattenpfad zweimal laut aus, bevor Sie ihn eingeben." + }, + "howRuns": { + "heading": "Wie das Skript läuft", + "body": "Der Ablauf hat zwei Phasen mit einer dreifachen Sicherheitsprüfung dazwischen. In Phase 1 werden Laufwerksauswahl, Betriebsart und Dateisystemdetails erfasst. Geschrieben wird erst, wenn Phase 2 unmittelbar vor der Ausführung eine abschließende erneute Prüfung bestanden hat." + }, + "visibility": { + "heading": "Sichtbarkeitsregeln", + "intro": "Bevor das Menü überhaupt erscheint, filtert ProxMenux die Festplattenliste. Es werden nur Kandidaten gezeigt, die nachweislich sicher sind:", + "items": [ + "Angezeigt werden Laufwerke, die nicht vom Host genutzt werden (Root-Pool, Swap, eingehängte Dateisysteme, aktives ZFS/LVM/RAID) und nicht in einer VM-/LXC-Konfiguration referenziert sind, unabhängig vom Betriebszustand des Gasts.", + "Ausgeblendet: Host- und Systemlaufwerke.", + "Ausgeblendet: in VM-/LXC-Konfigurationen referenzierte Laufwerke." + ], + "safetyTitle": "Sicherheit bei Bestätigung und Ausführung", + "safetyItems": [ + "Bei Laufwerken mit veralteten oder aktiven Metadaten erscheinen detaillierte Warnungen vor der Bestätigung.", + "Von einer laufenden VM genutzte Laufwerke werden bei der Bestätigung vollständig gesperrt.", + "Laufwerke mit eingehängten Partitionen werden bei der Ausführung vollständig gesperrt. Unmittelbar vor dem Schreiben erfolgt eine neue Prüfung.", + "Zwei Bestätigungen sind immer erforderlich: ein Ja/Nein-Dialog und die exakte Eingabe des vollständigen Laufwerkspfads." + ] + }, + "modes": { + "heading": "Betriebsarten", + "intro": "Nachdem Sie eine Festplatte ausgewählt haben, wählen Sie einen von vier Modi. Sie unterscheiden sich darin, was sie zerstören und was sie zurücklassen.", + "headerMode": "Modus", + "headerPart": "Partitionstabelle", + "headerData": "Daten", + "headerUseCase": "Anwendungsfall", + "rows": [ + { + "mode": "1. Alles löschen", + "part": "Zerstört", + "data": "Signaturen gelöscht", + "useCase": "Entfernt die Partitionstabelle sowie Dateisystem- und RAID-Metadaten, überschreibt die ersten und letzten 16 MiB mit Nullen und führt bei Laufwerken mit Discard-Unterstützung ein TRIM über den gesamten Datenträger aus. Die übrigen Daten werden nicht überschrieben und können auf HDDs noch wiederherstellbar sein; dies ist kein sicheres Löschen." + }, + { + "mode": "2. FS-Etiketten entfernen", + "part": "Erhalten", + "data": "Erhalten", + "useCase": "Entfernt alte ZFS-/LVM-/RAID-Signaturen, damit das Laufwerk nicht mehr automatisch eingebunden wird. Nutzdaten werden nicht flächig überschrieben, können durch das Entfernen der Signaturen aber unzugänglich werden." + }, + { + "mode": "3. Daten mit Nullen überschreiben", + "part": "Erhalten", + "data": "Zerstört (mit Nullen überschrieben)", + "useCase": "Bestehende Partitionen mit Nullen überschreiben und die Partitionstabelle erhalten. Nicht zugewiesener Speicher und SSD-Reservebereiche sind nicht erfasst; dies garantiert keine vollständige Gerätesanitisierung." + }, + { + "mode": "4. Vollformat", + "part": "Neue GPT", + "data": "Zerstört", + "useCase": "Einsatzbereites Laufwerk mit einer neuen Partition und einem neuen Dateisystem." + } + ], + "fullFormatOutro": "Vollständiges Formatieren führt zu zwei weiteren Dialogen:", + "fullFormatItems": [ + "Dateisystem: ext4, xfs, exfat (portabel) oder btrfs. Fehlen Werkzeuge wie mkfs.btrfs, wird mit einer klaren Meldung abgebrochen.", + "Label — optionales Dateisystem-Etikett zur Identifizierung." + ] + }, + "steps": { + "heading": "Schritt für Schritt", + "stepLabel": "Schritt", + "list": [ + { + "title": "Freies Laufwerk wählen", + "body": "ProxMenux listet jede Festplatte auf, die die oben genannten Sichtbarkeitsfilter passiert. Festplatten mit ⚠-Markern tragen veraltete Metadaten und werden mit den Details angezeigt, damit Sie wissen, was Sie überschreiben möchten." + }, + { + "title": "Wählen Sie einen Betriebsmodus", + "bodyRich": "Wählen Sie gemäß obiger Tabelle Alles löschen, FS-Etiketten entfernen, Daten mit Nullen überschreiben oder Vollständiges Formatieren." + }, + { + "title": "(Nur Vollformat) Dateisystem und Label auswählen", + "body": "ext4 / xfs / exfat / btrfs, plus ein optionales Etikett." + }, + { + "title": "Zweimal bestätigen", + "bodyRich": "ProxMenux zeigt eine Zusammenfassung der geplanten Aktion. Sie bestätigen im Ja/Nein-Dialog und geben den vollständigen Laufwerkspfad exakt wie angezeigt ein, etwa /dev/sdc. Jede andere Eingabe bricht ab." + }, + { + "title": "Revalidierung und Ausführung", + "body": "Kurz vor dem Schreiben überprüft das Skript den Festplattenzustand erneut. Wurde zwischenzeitlich eine Partition gemountet oder ein VM gestartet, wird die Ausführung abgebrochen. Ansonsten läuft die gewählte Operation und eine Zusammenfassung wird am Ende gedruckt." + } + ] + }, + "manual": { + "heading": "Manuelle Äquivalente", + "body": "Wenn Sie es vorziehen, die entsprechenden Befehle von Hand auszuführen:" + }, + "troubleshoot": { + "notListedTitle": "Disk nicht im Menü aufgeführt", + "notListedBody": "Das Laufwerk wurde aus Sicherheitsgründen ausgeblendet. Häufige Gründe: Es ist eingehängt, auch unter /mnt/tmp, aktives Mitglied von ZFS/LVM/RAID, in einer VM-/LXC-Konfiguration referenziert oder das Proxmox-Root-Laufwerk. Prüfen Sie auf dem Host lsblk -f und cat /proc/mdstat, um den Grund zu ermitteln.", + "busyTitle": "\"Disk may be busy\" / Unmount fehlgeschlagen", + "busyBody": "Etwas hält immer noch eine offene Datei auf einer Partition dieser Festplatte - meistens ein Container oder eine Shell, deren cwd sich in einem Mountpoint befindet. Identifizieren Sie es mit lsof | grep /dev/sdX oder fuser -vm /dev/sdX1, stoppen Sie es und führen Sie das Tool erneut aus." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/smart-disk-test", + "label": "SMART-Festplattenstatus und -test", + "tail": "Überprüfen Sie eine Festplatte vor / nach dem Wischen, um zu bestätigen, dass sie gesund ist." + }, + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": "Übergeben Sie eine frisch formatierte Festplatte an eine VM." + }, + { + "href": "/docs/storage-share/host-local-disk", + "label": "Lokale Festplatte als Proxmox-Speicher hinzufügen", + "tail": "Registrieren Sie eine gelöschte Festplatte als Proxmox-Speicherpool." + }, + { + "href": "/docs/help-info/storage-commands", + "label": "Speicher- und Festplattenbefehle", + "tail": " — Referenz für lsblk, blkid und parted." + }, + { + "href": "/docs/disk-manager", + "label": "Übersicht Disk Manager", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/import-disk-image-vm.json b/web/messages/de/docs/disk-manager/import-disk-image-vm.json new file mode 100644 index 00000000..a1d2cb83 --- /dev/null +++ b/web/messages/de/docs/disk-manager/import-disk-image-vm.json @@ -0,0 +1,110 @@ +{ + "meta": { + "title": "Datenträgerbild in VM importieren | ProxMenux Dokumentation", + "description": "Importieren Sie eine Disk-Image-Datei (.img / .qcow2 / .vmdk / .raw) mit ProxMenux in ein vorhandenes Proxmox VM. Per-Image-Schnittstelle, SSD-Emulation und bootfähiges Flag, mit qm importdisk unter der Haube.", + "ogTitle": "Datenträgerbild in VM importieren | ProxMenux Dokumentation", + "ogDescription": "Importieren Sie .img / .qcow2 / .vmdk / .raw Disk-Images in einen vorhandenen VM mit ProxMenux. Verwendet qm importdisk unter der Haube." + }, + "header": { + "title": "Disk-Image in VM importieren", + "description": "Importieren Sie eine Disk-Image-Datei (.img, .qcow2, .vmdk oder .raw) in eine vorhandene Proxmox VM. ProxMenux sammelt jede Entscheidung im Voraus - Ziel VM, Proxmox-Speicher, Quellverzeichnis, Schnittstelle, SSD-Emulation, Boot-Order - und führt dann qm importdisk für jedes ausgewählte Image aus.", + "section": "Datenträgerverwaltung · VM" + }, + "intro": { + "title": "Was das macht", + "body": "Anders als Datenträger in VM importieren, das ein physisches Laufwerk direkt zuweist, verwendet dieser Ablauf eine Datei auf dem Host, etwa einen Export eines anderen Hypervisors, ein heruntergeladenes Cloud-Image oder ein aus einer Sicherung extrahiertes Image. Sie wird in ein Proxmox-VM-Laufwerk auf dem gewählten Speicher umgewandelt. Die Originaldatei bleibt erhalten: ProxMenux kopiert sie, statt sie zu verschieben." + }, + "howRuns": { + "heading": "Wie das Skript läuft", + "body": "Der Ablauf hat zwei Phasen. Phase 1 erfasst über Dialoge vorab alle Entscheidungen: VM, Speicher, Quellverzeichnis, Images und Optionen je Image. Phase 2 führt qm importdisk für jedes ausgewählte Image aus. Vor Beginn von Phase 2 wird weder etwas kopiert noch die VM-Konfiguration verändert." + }, + "prereqs": { + "heading": "Voraussetzungen", + "items": [ + "Mindestens eine VM ist auf dem Host definiert. Die Ziel-VM sollte ausgeschaltet sein.", + "Mindestens eine Disk-Image-Datei in einem unterstützten Format liegt im Quellverzeichnis. Unterstützt werden .img, .qcow2, .vmdk und .raw.", + "Genug freien Speicherplatz auf dem Ziel-Proxmox-Speicher, um jede importierte Festplatte zu halten.", + "ProxMenux sucht standardmäßig nach Images in /var/lib/vz/template/iso. Sie können es auf jedes andere Verzeichnis auf dem Host verweisen, wenn es gefragt wird." + ] + }, + "steps": { + "heading": "Schritt für Schritt", + "stepLabel": "Schritt", + "list": [ + { + "title": "Ziel-VM auswählen", + "bodyRich": "ProxMenux liest qm list und zeigt jeden VM an. Wählen Sie diejenige aus, die die importierten Festplatten erhält." + }, + { + "title": "Wählen Sie den Proxmox-Speicher", + "bodyRich": "Die Liste enthält jeden Speicher, für den der Inhaltstyp images aktiviert ist. Wenn es nur einen Kandidaten gibt, wird er automatisch ausgewählt und der Dialog wird übersprungen." + }, + { + "title": "Wählen Sie das Quellverzeichnis", + "bodyRich": "Wählen Sie das Standardverzeichnis (/var/lib/vz/template/iso) oder geben Sie einen benutzerdefinierten Pfad ein - in der Regel, wo Sie das Image heruntergeladen / hochgeladen haben. Das Skript lehnt Pfade ab, die nicht existieren." + }, + { + "title": "Wählen Sie ein oder mehrere Images", + "bodyRich": "Das Verzeichnis wird nach .img / .qcow2 / .vmdk / .raw Dateien gescannt. Sie können mehrere gleichzeitig auswählen; jede wird im nächsten Schritt die Per-Bild-Optionen durchlaufen." + }, + { + "title": "Per-Image Optionen", + "intro": "Für jedes ausgewählte Image fragt ProxMenux:", + "items": [ + "Interface — scsi (Standard), virtio, sata oder ide. Passt dazu, wie der Gast die Festplatte sehen wird.", + "SSD-Emulation: nur für Nicht-VirtIO-Schnittstellen verfügbar. Fügt ssd=1 hinzu, damit der Gast das Laufwerk als SSD erkennt. Discard/TRIM muss separat konfiguriert werden.", + "Bootfähig: Bei Ja nimmt ProxMenux das Laufwerk als primäres Startgerät in die Bootreihenfolge der VM auf." + ] + }, + { + "title": "Importieren und zuweisen", + "bodyRich": "ProxMenux führt qm importdisk für jedes Image aus (Konvertierung des Formats im laufenden Betrieb, wo dies erforderlich ist), fügt die resultierende Festplatte mit den von Ihnen gewählten Optionen an den nächsten freien Slot (scsiN, sataN, ...) an und aktualisiert - wenn Sie eine Festplatte als bootfähig markiert haben - die Boot-Order. Fortschritt zeigt sich im Terminal." + } + ] + }, + "manual": { + "heading": "Manuelles Äquivalent", + "body": "Ein einzelnes Image lässt sich mit drei qm-Befehlen importieren:", + "warnTitle": "Konvertierung des Disk-Image-Formats", + "warnBody": "Wenn der Zielspeicher das Quellformat nativ nicht halten kann (z. B. LVM-Speicher .qcow2), konvertiert qm importdisk das Image transparent. Dies kann mehrere Minuten für Multi-GB-Bilder dauern; planen Sie entsprechend und unterbrechen Sie das Skript nicht." + }, + "troubleshoot": { + "heading": "Fehlerbehebung", + "noImagesTitle": "\"Keine kompatiblen Disk Images gefunden\"", + "noImagesBody": "Das Skript fand keine Datei mit .img, .qcow2, .vmdk oder .raw. Prüfen Sie die Erweiterung einschließlich Groß-/Kleinschreibung, ob die Datei direkt im gewählten Verzeichnis liegt (Unterverzeichnisse werden nicht durchsucht) und ob root sie lesen darf.", + "slowTitle": "Import ist langsam", + "slowBody": "Konvertierungen zwischen VMDK, QCOW2 und raw können durch CPU und E/A begrenzt sein. Die Importgeschwindigkeit hängt von Quelllesezugriffen, Zielschreibzugriffen und nötiger Dekomprimierung ab. Bevorzugen Sie nach Möglichkeit raw- oder qcow2-Quellen.", + "uefiTitle": "Importiertes Laufwerk startet in der UEFI-Shell", + "uefiBody": "Das Image wurde für Legacy (SeaBIOS) gebaut, aber der VM verwendet OVMF (oder umgekehrt). Wechseln Sie entweder den VM BIOS in Proxmox, um das Image abzugleichen, oder bauen Sie das Image vor dem Importieren im richtigen Modus neu auf." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": " — derselbe Ablauf für physische Laufwerke (Passthrough)." + }, + { + "href": "/docs/utils/import-vm", + "label": "VM von OVA / OVF importieren", + "tail": "- wenn Sie ein vollständiges VM-Paket haben, nicht nur ein Festplattenimage." + }, + { + "href": "/docs/utils/UUp-Dump-ISO-Creator", + "label": "UUP-Dump-ISO-Erstellung", + "tail": "- Generieren Sie eine Windows ISO, wenn Sie eine für eine neue Installation benötigen." + }, + { + "href": "/docs/help-info/storage-commands", + "label": "Speicher- und Festplattenbefehle", + "tail": " — Referenz für qm importdisk und qemu-img convert." + }, + { + "href": "/docs/disk-manager", + "label": "Übersicht Disk Manager", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/import-disk-lxc.json b/web/messages/de/docs/disk-manager/import-disk-lxc.json new file mode 100644 index 00000000..7d8235f2 --- /dev/null +++ b/web/messages/de/docs/disk-manager/import-disk-lxc.json @@ -0,0 +1,115 @@ +{ + "meta": { + "title": "Datenträger zu LXC importieren | ProxMenux Dokumentation", + "description": "Befestigen Sie eine physische Festplatte an einen vorhandenen LXC-Container auf Proxmox VE mit ProxMenux. Behandelt Dateisystemerkennung / -formatierung, Einhängepunkt, unprivilegierte → privilegierte Konvertierung und persistente Gerätepfade.", + "ogTitle": "Datenträger zu LXC importieren | ProxMenux Dokumentation", + "ogDescription": "Befestigen Sie eine physische Festplatte an einen vorhandenen LXC-Container. Behandelt Dateisystem, Mount Point und unprivilegierte → privilegierte Konvertierung." + }, + "header": { + "title": "Datenträger in LXC importieren", + "description": "Fügen Sie eine physische Festplatte an einen vorhandenen LXC-Container auf dem Proxmox-Host an. ProxMenux erkennt freie Festplatten, formatiert sie optional mit einem unterstützten Dateisystem und verkabelt die Partition mit persistenten Gerätepfaden an einen Einhängepunkt im Container.", + "section": "Datenträgerverwaltung · LXC" + }, + "intro": { + "title": "VM-Passthrough und LXC-Passthrough", + "body": "LXC-Passthrough stellt hier kein rohes Blockgerät bereit. Der Container erhält einen Einhängepunkt, also ein Verzeichnis auf der gewählten Host-Partition. Anders als bei einer VM nutzt der Gast den Host-Kernel; das Dateisystem (ext4/xfs/btrfs) wird daher vom Proxmox-Host gelesen, verwaltet und dem Container zugänglich gemacht." + }, + "howRuns": { + "heading": "Wie das Skript läuft", + "body": "Der Fluss hat zwei Phasen mit klarer Trennung zwischen \"Sammeln von Informationen und Entscheidungen\" und \"tatsächliches Ändern der Containerkonfiguration\". Bis zur endgültigen Bestätigung wird nichts in den CT eingebaut.", + "summary": "ProxMenux blendet das Root-Laufwerk, eingehängte Laufwerke und bereits in VM-/LXC-Konfigurationen referenzierte Laufwerke aus. Aktive ZFS-/LVM-/RAID-Mitglieder werden ausgeblendet; alte Signaturen werden mit ⚠ markiert, damit Sie sie vor dem Formatieren löschen können." + }, + "prereqs": { + "heading": "Voraussetzungen", + "items": [ + "Mindestens ein LXC-Container, der auf dem Host definiert ist.", + "Der Container ist privilegiert, oder Sie nehmen das Konvertierungsangebot des Skripts an.", + "Mindestens eine physische Festplatte, die vom Host oder einem anderen Gast nicht verwendet wird." + ], + "warnTitle": "Privilegierter Container erforderlich", + "warnBody": "Dieses Skript benötigt für die direkte Laufwerkszuweisung einen privilegierten Container. Bei unprivileged: 1 bietet es an, /etc/pve/lxc/<CTID>.conf direkt zu ändern. Das ändert das Sicherheitsmodell, ordnet vorhandene Dateieigentümer aber nicht neu zu. Sichern Sie den Container und prüfen Sie die Eigentümer vor der Zustimmung, oder erstellen Sie ihn privilegiert neu." + }, + "steps": { + "heading": "Schritt für Schritt", + "stepLabel": "Schritt", + "list": [ + { + "title": "Wählen Sie den Zielcontainer", + "img": "/disk/select-container.png", + "caption": "Auswahlmenü für Container", + "bodyRich": "ProxMenux listet jeden LXC-Container auf dem Host auf (pct list). Wählen Sie diejenige, die die Festplatte erhalten wird. Wenn der Container nicht privilegiert ist, wird Ihnen angeboten, ihn jetzt umzuwandeln." + }, + { + "title": "Wählen Sie die physische Festplatte", + "img": "/disk/disk-selection-ct.png", + "caption": "Freie Festplatten auf dem Host erkannt (mit ⚠ Labels für veraltete Metadaten)", + "bodyRich": "Nur sichere Kandidaten werden angezeigt. Anders als bei VMs weist dieses Skript ein Laufwerk pro Durchlauf zu. Führen Sie es für jedes weitere Laufwerk erneut aus." + }, + { + "title": "Formatieren oder wiederverwenden", + "intro": "Das Skript überprüft die Festplatte:", + "items": [ + "Wenn es bereits ein unterstütztes Dateisystem (ext4 / xfs / btrfs) trägt, können Sie es so wiederverwenden, wie es ist (bestehende Dateien bleiben erhalten).", + "Wenn es kein unterstütztes Dateisystem hat, wird Ihnen angeboten, es zu formatieren. Wählen Sie ext4 / xfs / btrfs." + ] + }, + { + "title": "Wählen Sie den Mount Point", + "bodyRich": "Geben Sie den Pfad ein, unter dem der Container das Laufwerk sehen soll, etwa /mnt/data oder /mnt/disk_passthrough. Dies ist der Pfad innerhalb des Containers; der Host sieht die Partition bereits unter /dev/disk/by-id/…." + }, + { + "title": "Zuweisen und prüfen", + "img": "/disk/assignment-ct.png", + "caption": "Zuordnung zu CT", + "bodyRich": "ProxMenux fügt der Containerkonfiguration einen neuen mpN:-Eintrag hinzu, überspringt belegte Indizes und liest die Konfiguration zur Bestätigung erneut. Bei laufendem Container wird die Einhängung sofort angewendet, andernfalls beim nächsten Start.", + "extraImg": "/disk/result-point.png", + "extraAlt": "Mount Point erfolgreich erstellt", + "extraCaption": "Einhängepunkt erstellt und geprüft" + } + ] + }, + "manual": { + "heading": "Manuelles Äquivalent", + "body": "Das Skript wickelt pct set ab. Ein direkter Aufruf für eine einzelne Festplatte sieht so aus:" + }, + "important": { + "heading": "Wichtige Überlegungen", + "items": [ + "Ein Laufwerk pro Durchlauf. Führen Sie das Skript für jedes weitere Laufwerk erneut aus.", + "Hängen Sie nicht dieselbe Partition an mehrere Container an, die sie gleichzeitig bereitstellen könnten – gleichzeitige Schreibvorgänge beschädigen das Dateisystem.", + "Alte Metadaten (RAID / ZFS / LVM-Signaturen) müssen manuell gelöscht werden, bevor Sie das Skript formatieren lassen, andernfalls weigert sich mkfs zu überschreiben. Verwenden Sie dafür das Format / Wipe Physical Disk Tool." + ] + }, + "troubleshoot": { + "heading": "Fehlerbehebung", + "unprivTitle": "\"Kann nicht mit einem unprivilegierten Container fortfahren\"", + "unprivBody": "Sie haben die Konvertierung abgelehnt; das Skript wurde daher ohne Änderungen beendet. Um fortzufahren, führen Sie es erneut aus und stimmen zu: Das Skript stoppt den Container, sichert seine Konfiguration in /etc/pve/lxc/<CTID>.conf.bak und ersetzt unprivileged: 1 durch unprivileged: 0. Diese Änderung allein wandelt die UID-/GID-Eigentümer vorhandener Dateien nicht um; sichern Sie den Container daher vorher und passen Sie die Eigentümer an. Alternativ stellen Sie ihn privilegiert wieder her oder erstellen ihn neu.", + "permsTitle": "Berechtigungen im Container sind falsch", + "permsBody": "Beim Einhängen in einen privilegierten Container bleiben vorhandene Dateieigentümer erhalten; ein neues Dateisystem gehört häufig root:root. Läuft der Dienst unter einem anderen Benutzer, etwa www-data für Nextcloud, prüfen Sie dessen UID/GID und passen Sie die nötigen Dateien mit chown/chmod an. Dies verändert auch den zugrunde liegenden Datenträger." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/import-disk-vm", + "label": "Datenträger in VM importieren", + "tail": " — entsprechender Ablauf für VMs." + }, + { + "href": "/docs/storage-share/lxc-mount-points", + "label": "LXC-Einhängepunkte (Host ↔ CT)", + "tail": "- Hostverzeichnisse mit einem CT teilen, anstatt eine ganze Festplatte anzuhängen." + }, + { + "href": "/docs/help-info/vm-ct-commands", + "label": "VM und CT Verwaltungsbefehle", + "tail": "- pct config / push / pull Referenz." + }, + { + "href": "/docs/disk-manager", + "label": "Übersicht Disk Manager", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/import-disk-vm.json b/web/messages/de/docs/disk-manager/import-disk-vm.json new file mode 100644 index 00000000..4ae57b22 --- /dev/null +++ b/web/messages/de/docs/disk-manager/import-disk-vm.json @@ -0,0 +1,113 @@ +{ + "meta": { + "title": "Datenträger zu VM importieren | ProxMenux Dokumentation", + "description": "Befestigen Sie eine physische Festplatte an eine vorhandene Proxmox VM mit ProxMenux. Sichere Festplattenerkennung (ausgenommen System, montiert, in Gebrauch, ZFS/RAID/LVM), persistente /dev/disk/by-id-Pfade und SATA / SCSI / VirtIO / IDE-Schnittstellen.", + "ogTitle": "Datenträger zu VM importieren | ProxMenux Dokumentation", + "ogDescription": "Befestigen Sie eine physische Festplatte an eine vorhandene Proxmox VM. Verwendet persistente /dev/disk/by-id-Pfade und unterstützt SATA / SCSI / VirtIO / IDE." + }, + "header": { + "title": "Datenträger in VM importieren", + "description": "Weisen Sie einer vorhandenen VM ein physisches Laufwerk des Proxmox-Hosts zu. ProxMenux erkennt freie Laufwerke, lässt Sie Ziel-VM und Bustyp (SATA/SCSI/VirtIO/IDE) wählen und verwendet persistente /dev/disk/by-id-Pfade. So bleibt die Zuordnung bei Neustarts und geänderter Gerätereihenfolge bestehen.", + "section": "Datenträgerverwaltung · VM" + }, + "intro": { + "title": "Was das macht", + "body": "Ein physisches Laufwerk wird als Backend einer virtuellen Laufwerksschnittstelle zugewiesen. Der Gast erhält nicht automatisch native SMART-Daten oder Funktionen des physischen Controllers; dafür ist Controller-PCI-Passthrough geeignet. Das Laufwerk wird, falls vorhanden, über seinen Pfad /dev/disk/by-id/… zugewiesen, sodass eine geänderte Gerätereihenfolge beim Start die Zuordnung nicht unterbricht; andernfalls wird /dev/sdX verwendet." + }, + "howRuns": { + "heading": "Wie das Skript läuft", + "body": "Der Fluss hat zwei Phasen mit klarer Trennung zwischen \"Sammeln von Informationen und Entscheidungen\" und \"tatsächliches Anbringen von Festplatten am VM\". Bis zur endgültigen Bestätigung bleibt die VM-Konfiguration unberührt.", + "summary": "ProxMenux blendet unsichere Kandidaten aus: Root-Laufwerk, auf dem Host eingehängte Laufwerke, aktive ZFS-/LVM-/RAID-Mitglieder und bereits in VM-/LXC-Konfigurationen referenzierte Laufwerke. Laufwerke mit alten, nicht mehr verwendeten ZFS-/RAID-/LVM-Metadaten erscheinen mit ⚠, werden aber nicht gesperrt." + }, + "prereqs": { + "heading": "Voraussetzungen", + "items": [ + "Mindestens ein VM, der auf dem Host definiert ist (das Skript wird direkt an qm list gesendet).", + "Die Ziel-VM ist ausgeschaltet. Läuft sie, bricht das Skript mit einer Meldung ab; das Hinzufügen im laufenden Betrieb ist nicht Teil dieses Ablaufs.", + "Mindestens eine physische Festplatte auf dem Host, die nicht vom Host oder einem anderen Gast verwendet wird." + ] + }, + "steps": { + "heading": "Schritt für Schritt", + "stepLabel": "Schritt", + "list": [ + { + "title": "Ziel-VM auswählen", + "bodyRich": "ProxMenux listet alle VMs auf dem Host über qm list auf. Wählen Sie die VM für das Laufwerk. Läuft sie, bricht das Skript ab und fordert Sie auf, sie zuerst herunterzufahren." + }, + { + "title": "Wählen Sie die physische Festplatte(n) zum Anbringen", + "img": "/disk/disk-selection.png", + "caption": "Freie Festplatten auf dem Host erkannt (mit ⚠ Labels für veraltete Metadaten)", + "body": "Das Skript scannt jede Host-Disk und zeigt nur diejenigen an, die sicher angehängt werden können. Mit der Checkliste können Sie eine oder mehrere auf einmal auswählen. Disks mit alten ZFS / LVM / RAID-Signaturen werden immer noch angezeigt, gekennzeichnet mit ⚠, so dass Sie wissen, dass sie ein Wischen benötigen, bevor der Gast sie formatieren kann." + }, + { + "title": "Wählen Sie die Bus-Schnittstelle", + "img": "/disk/disk-assigment.png", + "caption": "Auswahl der Busschnittstellen", + "intro": "Wählen Sie aus, wie der Gast die Festplatte sehen soll:", + "items": [ + "SCSI – moderner Standard für Linux und Windows mit installierten VirtIO-SCSI-Treibern.", + "SATA — breite Kompatibilität, funktioniert out of the box auf praktisch jedem Gast.", + "VirtIO: effizientes paravirtualisiertes Blockgerät; benötigt Gasttreiber.", + "IDE: für ältere Gäste ohne einen der oben genannten Treiber." + ] + }, + { + "title": "Zuweisen und prüfen", + "bodyRich": "ProxMenux verbindet jede ausgewählte Festplatte mit dem besten /dev/disk/by-id/-Pfad, überspringt Slot-Indizes, die bereits in der VM-Konfiguration verwendet werden (scsi0, sata0, ...) und liest die Konfiguration erneut, um die Zuordnung zu bestätigen. Am Ende wird eine Zusammenfassung gedruckt." + } + ] + }, + "manual": { + "heading": "Manuelles Äquivalent", + "body": "Das Skript ist ein Wrapper um qm set. Der genaue Befehl für eine einzelne Festplatte sieht so aus:", + "migrationTitle": "Live Migration ist nicht möglich", + "migrationBody": "Eine VM mit einem direkt zugewiesenen physischen Laufwerk ist an den Host gebunden, der dieses bereitstellt. Ein anderer Knoten hat normalerweise weder denselben Pfad /dev/disk/by-id/… noch dessen Daten. Sichern und übertragen Sie diese separat oder verwenden Sie verwaltete virtuelle Laufwerke auf gemeinsamem oder unterstützt repliziertem Speicher. Gehen Sie nicht davon aus, dass Proxmox-Speicherreplikation dieses Gerät umfasst.", + "shareTitle": "Teilen Sie keine physische Festplatte zwischen VMs", + "shareBody": "Wenn Sie dieselbe physische Festplatte gleichzeitig an zwei VMs anbringen, werden die Daten beim nächsten Schreiben beschädigt. ProxMenux erkennt diese Situation und blockiert sie, aber wenn Sie das Skript umgehen (direktes qm set), stellen Sie sicher, dass nur ein Gast jemals die Festplatte mountet." + }, + "troubleshoot": { + "heading": "Fehlerbehebung", + "noDisksTitle": "\"Für diesen VM sind keine Festplatten verfügbar\"", + "noDisksIntro": "Das Skript fand Festplatten, aber jeder Kandidat wurde herausgefiltert. Häufige Gründe:", + "noDisksItems": [ + "Jede verbleibende Festplatte wird bereits in der Konfiguration des Ziels VM referenziert.", + "Jede verbleibende Festplatte wird auf dem Host montiert oder ist Teil eines aktiven ZFS / LVM / RAID.", + "Der Host hat nur die Root Disk installiert." + ], + "noDisksOutro": "Führen Sie lsblk -f auf dem Host aus, um den Status jeder Festplatte zu überprüfen.", + "noVisibleTitle": "VM sieht die Festplatte nach dem Booten nicht", + "noVisibleBody": "Bei SCSI oder VirtIO benötigt der Gast den passenden Treiber. Linux bringt ihn im Kernel mit; unter Windows installieren Sie ihn von der VirtIO-ISO (siehe VM erstellen: Windows). SATA auf der Hardware-Registerkarte der VM ist ein schneller Test, ob das Laufwerk zugewiesen ist." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/import-disk-image-vm", + "label": "Disk-Image in VM importieren", + "tail": " — derselbe Ablauf für Disk-Image-Dateien (qcow2 / vmdk / raw / img)." + }, + { + "href": "/docs/disk-manager/import-disk-lxc", + "label": "Datenträger in LXC importieren", + "tail": " — entsprechender Ablauf für LXC-Container." + }, + { + "href": "/docs/disk-manager/add-controller-nvme-vm", + "label": "Controller oder NVMe zu VM hinzufügen", + "tail": "- wenn Sie einen vollständigen PCIe / NVMe-Durchgang anstelle eines Festplattenanschlusses benötigen." + }, + { + "href": "/docs/help-info/storage-commands", + "label": "Speicher- und Festplattenbefehle", + "tail": "qm importdisk und zugehörige Shell-Referenz." + }, + { + "href": "/docs/disk-manager", + "label": "Übersicht Disk Manager", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/index.json b/web/messages/de/docs/disk-manager/index.json new file mode 100644 index 00000000..d75fab7d --- /dev/null +++ b/web/messages/de/docs/disk-manager/index.json @@ -0,0 +1,135 @@ +{ + "meta": { + "title": "Proxmox-Datenträgerverwaltung: VM/LXC-Zuweisung, SMART-Tests, Löschen | ProxMenux", + "description": "Verwalten Sie physische Laufwerke auf einem Proxmox-VE-Host mit ProxMenux: Laufwerke VMs oder LXC zuweisen (qm set scsi/sata/virtio, pct mp), img-/qcow2-/vmdk-/raw-Images importieren, ganze HBAs oder NVMe-Controller durchreichen, Laufwerke mit Sicherheitsprüfungen löschen und SMART-Selbsttests ausführen.", + "ogTitle": "Proxmox-Datenträgerverwaltung: VM/LXC-Zuweisung, SMART-Tests, Löschen", + "ogDescription": "Laufwerke VMs/LXC zuweisen, Images importieren, Controller per PCI durchreichen, Laufwerke sicher bedienen und SMART-Tests auf Proxmox VE ausführen.", + "twitterTitle": "Proxmox-Datenträgerverwaltung | ProxMenux", + "twitterDescription": "Laufwerke VMs/LXC zuweisen, Images importieren, PCI-Passthrough, Löschen mit Sicherheitsprüfungen und SMART-Tests." + }, + "header": { + "title": "Festplattenmanager", + "description": "Das Menü Datenträgerverwaltung bündelt ProxMenux-Werkzeuge für physische Laufwerke auf einem Proxmox-Host: Zuweisung an VMs und LXC, Import von Disk-Images, Passthrough ganzer Controller oder NVMe-Geräte, Löschen mit Sicherheitsprüfungen und SMART-Tests.", + "section": "Festplattenmanager" + }, + "intro": { + "title": "Wofür dieses Menü ist", + "body": "Diese Werkzeuge arbeiten mit für den Proxmox-Host sichtbaren physischen Laufwerken und dort gespeicherten Image-Dateien. Es gibt drei Gruppen: VM (Laufwerke zuweisen oder Images importieren), LXC (Laufwerke an Container anbinden) und Dienstprogramme (Formatierung und SMART-Status). Zuweisung und Löschen blenden Systemlaufwerke und belegte Laufwerke aus oder sperren sie; SMART-Prüfungen können sie ohne Löschen untersuchen." + }, + "opening": { + "heading": "Öffnen des Menüs", + "body": "Wählen Sie im Hauptmenü von ProxMenux Disk Manager. Sie werden sehen:", + "imageAlt": "Disk Manager Menü mit VM, LXC und Utilities Blöcken" + }, + "groups": { + "heading": "Drei Werkzeuggruppen", + "intro": "Das Disk Manager-Menü ist in drei Gruppen unterteilt: VM, LXC und Utilities. Klicken Sie auf eine Karte, um direkt zu den Werkzeugen in dieser Gruppe zu springen.", + "vmTitle": "VM", + "vmBody": "Tools, die auf vorhandenen virtuellen Maschinen arbeiten. Der VM muss ausgeschaltet werden; ProxMenux validiert dies, bevor er die Konfiguration berührt.", + "vmItems": [ + "Datenträger in VM importieren", + "Disk-Image in VM importieren", + "Controller oder NVMe zu VM hinzufügen" + ], + "lxcTitle": "LXC", + "lxcBody": "Werkzeuge für bestehende LXC-Container. Dieses Skript benötigt einen privilegierten Container und bietet bei Bedarf die Änderung der Einstellung an. Lesen Sie zuerst die Hinweise zur Konvertierung und zu Dateieigentümern auf der Werkzeugseite.", + "lxcItems": [ + "Datenträger in LXC importieren" + ], + "utilitiesTitle": "Dienstprogramme", + "utilitiesBody": "Hostseitige Tools, die auf den Festplatten selbst arbeiten, unabhängig von VM oder Containern.", + "utilitiesItems": [ + "Physischen Datenträger formatieren / löschen", + "SMART-Festplattenstatus und -test" + ] + }, + "vm": { + "heading": "VM", + "intro": "Tools, die auf vorhandenen virtuellen Maschinen arbeiten. Der VM muss ausgeschaltet werden; ProxMenux validiert dies, bevor er die Konfiguration berührt.", + "options": [ + { + "icon": "HardDrive", + "href": "/docs/disk-manager/import-disk-vm", + "title": "Datenträger in VM importieren", + "description": "Befestigen Sie eine nicht zugewiesene physische Festplatte an eine vorhandene VM. Unterstützt SATA / SCSI / VirtIO / IDE, verwendet persistente /dev/disk/by-id-Pfade." + }, + { + "icon": "FileDown", + "href": "/docs/disk-manager/import-disk-image-vm", + "title": "Disk-Image in VM importieren", + "description": "Importieren Sie Disk-Image-Dateien (.img / .qcow2 / .vmdk / .raw) in eine vorhandene VM. Zweiphasiger Ablauf mit qm importdisk im Hintergrund." + }, + { + "icon": "Cpu", + "href": "/docs/disk-manager/add-controller-nvme-vm", + "title": "Controller oder NVMe zu VM hinzufügen", + "description": "PCI-Durchgang eines ganzen SATA / SAS HBA- oder NVMe-Geräts zu einem VM. Behandelt IOMMU Enablement und Konflikterkennung." + } + ] + }, + "lxc": { + "heading": "LXC", + "intro": "Werkzeuge für bestehende LXC-Container. Dieses Skript benötigt einen privilegierten Container und bietet bei Bedarf die Änderung der Einstellung an. Lesen Sie zuerst die Hinweise zur Konvertierung und zu Dateieigentümern auf der Werkzeugseite.", + "options": [ + { + "icon": "Boxes", + "href": "/docs/disk-manager/import-disk-lxc", + "title": "Datenträger in LXC importieren", + "description": "Befestigen Sie eine nicht zugewiesene physische Festplatte an einem vorhandenen LXC-Container als Montagepunkt. Behandelt Dateisystemerkennung / Format und privilegierte Konvertierung." + } + ] + }, + "utilities": { + "heading": "Dienstprogramme", + "intro": "Hostseitige Tools, die auf den Festplatten selbst arbeiten, unabhängig von VM oder Containern.", + "options": [ + { + "icon": "Eraser", + "href": "/docs/disk-manager/format-disk", + "title": "Physische Festplatte formatieren/löschen (sicher)", + "description": "Laufwerke mit strengen Sicherheitskontrollen löschen: Nur vollständig freie Laufwerke werden angezeigt, und eine doppelte Bestätigung ist immer erforderlich." + }, + { + "icon": "Activity", + "href": "/docs/disk-manager/smart-disk-test", + "title": "SMART-Festplattenstatus und -test", + "description": "Führen Sie SMART Gesundheitschecks und kurze / lange Selbsttests auf SATA-, SAS- und NVMe-Laufwerken durch. Exportiert JSON für den Monitor." + } + ] + }, + "safety": { + "title": "Sicherheitsregeln für Laufwerkszuweisung und Löschen", + "intro": "Zuweisungs- und Löschvorgänge filtern Geräte aus, die den Host gefährden könnten:", + "items": [ + "Das Systemlaufwerk sowie Root-Pool, Swap und aktive ZFS-/LVM-/RAID-Mitglieder bleiben ausgeblendet.", + "Auf dem Host eingehängte Laufwerke werden ausgeblendet oder bei der Ausführung gesperrt.", + "Datenträger , auf die mit einem laufenden VM oder LXC verwiesen wird, können nicht neu zugewiesen oder gelöscht werden.", + "Operationen, die den Host-Status ändern können (IOMMU enable, Wipe) erfordern eine explizite Bestätigung." + ] + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/storage-share", + "label": "Speicher- und Freigabemanager", + "tail": "- registrieren Sie den Speicher in Proxmox selbst (NFS / Samba / iSCSI / local) und teilen Sie Ordner zwischen Host und CTs." + }, + { + "href": "/docs/help-info/storage-commands", + "label": "Speicher- und Festplattenbefehle", + "tail": "— Blockgeräte, LVM, pvesm, SMART Referenz." + }, + { + "href": "/docs/help-info/zfs-commands", + "label": "ZFS Verwaltungsbefehle", + "tail": "— für ZFS-Pools und Datensätze." + }, + { + "href": "/docs/hardware/gpu-vm-passthrough", + "label": "GPU zu VM hinzufügen (Passthrough)", + "tail": "- die gleichen IOMMU / VFIO-Konzepte für GPUs." + } + ] + } +} diff --git a/web/messages/de/docs/disk-manager/smart-disk-test.json b/web/messages/de/docs/disk-manager/smart-disk-test.json new file mode 100644 index 00000000..93c20841 --- /dev/null +++ b/web/messages/de/docs/disk-manager/smart-disk-test.json @@ -0,0 +1,146 @@ +{ + "meta": { + "title": "SMART-Laufwerksstatus und Tests | ProxMenux-Dokumentation", + "description": "Führen Sie SMART-Gesundheitsprüfungen und Festplattentests auf Proxmox VE mit ProxMenux aus. Unterstützt SATA / SAS (smartmontools) und NVMe (nvme-cli). Kurze und lange Selbsttests, JSON Export für ProxMenux Monitor Integration.", + "ogTitle": "SMART-Laufwerksstatus und Tests | ProxMenux-Dokumentation", + "ogDescription": "SMART Gesundheitschecks und Selbsttests für SATA / SAS und NVMe Laufwerke auf Proxmox. JSON Export für den Monitor." + }, + "header": { + "title": "SMART-Festplattenstatus und -test", + "description": "Lesen Sie SMART-Daten und lösen Sie Selbsttests auf den physischen Festplatten aus, die an den Proxmox-Host angeschlossen sind. ProxMenux installiert automatisch Smartmontools für SATA / SAS und nvme-cli für NVMe, führt die Aktion innerhalb der Laufwerk-Firmware aus (lange Tests überleben Terminalschluss) und exportiert JSON-Ergebnisse für den ProxMenux Monitor.", + "section": "Disk Manager · Dienstprogramme" + }, + "intro": { + "title": "Wofür das ist", + "body": "SMART (Self-Monitoring, Analysis and Reporting Technology) ermöglicht es dem Laufwerk selbst, seinen Zustand zu melden und Selbsttests durchzuführen. Dieses Tool stellt diese Daten frei, ohne dass Sie sich die richtige smartctl/nvme-Aufrufung merken müssen, und behält die Ausgabe als JSON, so dass der Monitor Trends im Laufe der Zeit zeichnen kann." + }, + "howRuns": { + "heading": "Wie das Skript läuft", + "body": "Alle Aktionen hier sind zerstörungsfrei - SMART liest die Laufwerk-Firmware-Zähler, und Selbsttests werden innerhalb des Laufwerks selbst in die Warteschlange gestellt. Es ist keine Phasentrennung erforderlich. Der Ablauf ist: Abhängigkeitsprüfung → Festplattenauswahl → Aktionsauswahl → Werkzeugaufruf (smartctl oder nvme-cli abhängig vom Festplattenbus) → gedruckte Ausgabe plus JSON Export für den Monitor." + }, + "deps": { + "heading": "Abhängigkeiten", + "body": "Das Tool installiert automatisch, was es beim ersten Durchlauf benötigt. SATA / SAS-Laufwerke verlassen sich auf smartmontools; NVMe-Laufwerke verlassen sich auf nvme-cli. Beide:" + }, + "actions": { + "heading": "Verfügbare Maßnahmen", + "headerAction": "Aktion", + "headerWhat": "Was gelesen oder ausgeführt wird", + "headerDur": "Dauer", + "rows": [ + { + "action": "Schneller Gesundheitszustand", + "whatRich": "Insgesamt PASSED/FAILED + Schlüsselattribute.
smartctl -H + -A (SATA/SAS) oder nvme smart-log (NVMe).", + "dur": "Sofort" + }, + { + "action": "Vollständiger Bericht", + "whatRich": "Vollständige SMART-Daten, scrollbar. smartctl -x für SATA/SAS oder nvme smart-log + id-ctrl für NVMe.", + "dur": "Sofort" + }, + { + "action": "Kurztest", + "what": "Grundlegende Oberfläche und elektrische Überprüfung, in der Warteschlange innerhalb der Firmware des Laufwerks.", + "dur": "~2 Minuten" + }, + { + "action": "Lange Prüfung", + "what": "Vollständiger Scan der gesamten Oberfläche. Läuft auf der Laufwerk-Hardware - bleibt bestehen, auch wenn Sie das Terminal schließen.", + "dur": "Stunden (abhängig von der Laufwerksgröße)" + }, + { + "action": "Testfortschritt prüfen", + "what": "Status des aktiven oder letzten Selbsttests.", + "dur": "Sofort" + } + ], + "tipTitle": "Lange Tests laufen nach dem Schließen des Terminals weiter", + "tipBody": "Ein langer Test läuft im Laufwerk und benötigt keinen weiterlaufenden ProxMenux-Prozess. Schließen Sie das Terminal und verwenden Sie später Testfortschritt prüfen. Ein Host-Neustart, Controller-Reset oder Stromausfall kann den Test und die Ergebniserfassung unterbrechen; eine automatische Fortsetzung ist nicht garantiert." + }, + "json": { + "heading": "JSON Export für den Monitor", + "intro": "Status-, Kurztest- und Fortschrittsaktionen schreiben eine JSON-Datei mit Zeitstempel nach /usr/local/share/proxmenux/smart/<disk>/; Langtestergebnisse werden im Hintergrund erfasst. Der vollständige Bericht wird nur angezeigt. Die Struktur ist:", + "outro": "ProxMenux Monitor liest diese Dateien zur Darstellung von Zustandstrends je Laufwerk. Beim Erreichen der Aufbewahrungsgrenze werden alte Dateien automatisch entfernt." + }, + "steps": { + "heading": "Schritt für Schritt", + "stepLabel": "Schritt", + "list": [ + { + "title": "Laufwerk auswählen", + "body": "ProxMenux listet jede physische Festplatte auf dem Host mit ihrem Modell und ihrer Größe auf. Im Gegensatz zum Format-Tool wird diese Liste nicht durch die Sicherheit gefiltert - das Lesen von SMART ist eine zerstörungsfreie Operation.", + "img": "/disk/smart/disk-selection.png", + "alt": "SMART Plattenauswahlmenü", + "caption": "SMART Plattenauswahlmenü" + }, + { + "title": "Wählen Sie eine Aktion", + "body": "Wählen Sie schnellen Status, vollständigen Bericht, Kurztest, Langtest oder Testfortschritt. Das Menü bleibt nach jeder Aktion offen, sodass Sie mehrere Abfragen desselben Laufwerks ausführen können.", + "img": "/disk/smart/action-menu.png", + "alt": "SMART Aktionsmenü", + "caption": "SMART Aktionsmenü (5 Aktionen + Abbruch)" + }, + { + "title": "(Nur langer Test) Hintergrundausführung bestätigen", + "body": "ProxMenux weist darauf hin, dass der Test nach dem Schließen des Terminals weiterläuft, und zeigt den Speicherort der JSON-Ergebnisse. Bestätigen Sie, um den Test zu starten.", + "img": "/disk/smart/long-test-warning.png", + "alt": "Dialog über lange Testbestätigung", + "caption": "Lange Testbestätigung – läuft im Hintergrund, Ergebnis wird in JSON gespeichert" + }, + { + "title": "Prüfergebnisse", + "bodyRich": "Status-, Test- und Fortschrittsausgaben erscheinen im Terminal; Vollständiger Bericht öffnet sich in einem scrollbaren Dialog. JSON wird für die oben genannten Aktionen exportiert, nicht für den vollständigen Bericht. Verwenden Sie bei Langtests später Testfortschritt prüfen für dasselbe Laufwerk.", + "img": "/disk/smart/quick-status.png", + "alt": "Schnelle Ausgabe des Gesundheitszustands", + "caption": "Schnelle Ausgabe des Gesundheitszustands (SATA — smartctl -H + -A)" + } + ] + }, + "manual": { + "heading": "Manuelle Äquivalente", + "nvmeWarnTitle": "NVMe Selbsttests sind Laufwerk-Firmware abhängig", + "nvmeWarnBody": "Nicht jedes NVMe-Laufwerk unterstützt den kurzen / langen Selbsttestbefehl. Wenn ein Laufwerk den Test ablehnt, sind das SMART-Protokoll und die ID-Controllerdaten (nvme smart-log + id-ctrl) immer noch das zuverlässigste Gesundheitssignal." + }, + "troubleshoot": { + "heading": "Fehlerbehebung", + "noSmartTitle": "\"Konnte SMART-Daten aus /dev/sdX nicht lesen\"", + "noSmartBody": "Die Festplatte befindet sich wahrscheinlich hinter einem RAID / SAS-Controller, der SMART nicht durchläuft. Versuchen Sie mit Megaraid-basierten Karten smartctl -d megaraid,N /dev/sdX. Für HBAs im IT-Modus funktioniert der direkte Aufruf.", + "longTitle": "Langer Test nie abgeschlossen", + "longBody": "Der Test läuft in der Laufwerksfirmware; hohe E/A-Last kann ihn verzögern. Nutzen Sie eine ruhige Zeit, etwa nachts. Bei SATA zeigt smartctl -c den verbleibenden Prozentsatz, bei SAS smartctl -l selftest; das Selbsttestprotokoll zeigt Abschlussstatus und gegebenenfalls fehlerhafte LBAs. Lesefehler müssen untersucht werden; langsamer Fortschritt allein beweist keinen defekten Sektor." + }, + "related": { + "heading": "Verwandte Themen", + "items": [ + { + "href": "/docs/disk-manager/format-disk", + "label": "Physischen Datenträger formatieren / löschen", + "tail": "Wiederverwendung einer Festplatte, nachdem bestätigt wurde, dass sie gesund ist." + }, + { + "href": "/docs/help-info/storage-commands", + "label": "Speicher- und Laufwerksbefehle → SMART-Laufwerksstatus", + "tail": "Referenz smartctl/nvme." + }, + { + "href": "/docs/help-info/zfs-commands", + "label": "ZFS Verwaltungsbefehle", + "tail": " — zpool scrub prüft ZFS-Datenprüfsummen und ergänzt SMART-Hardwaretests." + }, + { + "href": "/docs/monitor/dashboard/storage", + "label": "ProxMenux Monitor — Speicherregister", + "tail": " — Laufwerksdetailansicht mit den hier exportierten JSON-Daten: vollständige SMART-Tabelle, Verlauf und PDF-Bericht." + }, + { + "href": "/docs/monitor/dashboard/hardware", + "label": "ProxMenux Monitor — Register Hardware", + "tail": "Speicherzusammenfassung mit Modell, Kapazität und ausgehandelter Verbindungsgeschwindigkeit (aktuell vs. maximal auf NVMe)." + }, + { + "href": "/docs/disk-manager", + "label": "Übersicht Disk Manager", + "tail": "." + } + ] + } +} diff --git a/web/messages/de/docs/external-repositories.json b/web/messages/de/docs/external-repositories.json new file mode 100644 index 00000000..25801ecf --- /dev/null +++ b/web/messages/de/docs/external-repositories.json @@ -0,0 +1,58 @@ +{ + "meta": { + "title": "Externe Repositorys | ProxMenux-Dokumentation", + "description": "ProxMenux bindet ausgewählte externe Skript-Repositorys wie community-scripts und ZimaOS als Alternativen in einigen Menüs ein. Jedes Skript ist mit seiner Quelle gekennzeichnet. Fehlerberichte gehören an den ursprünglichen Maintainer.", + "ogTitle": "Externe Repositorys | ProxMenux-Dokumentation", + "ogDescription": "Externe Skript-Repositorys, die als Alternativen in ProxMenux-Menüs eingebunden sind." + }, + "header": { + "title": "Externe Repositorys", + "description": "ProxMenux bindet ausgewählte externe Skript-Repositorys als Alternativen in einige Menübereiche ein. Die Skripte stammen aus vertrauenswürdigen Drittquellen und werden im Menü klar mit ihrer Herkunft gekennzeichnet.", + "section": "Externe Repositorys" + }, + "practice": { + "title": "Was das in der Praxis bedeutet", + "body": "Verwendet eine Menüoption ein externes Skript statt eigenen ProxMenux-Codes, nennt das Menü ausdrücklich dessen Herkunft und Repository. ProxMenux startet diese Skripte, verändert sie aber nicht. Für Fehler in ihrem Verhalten ist der Maintainer des jeweiligen Repositorys zuständig." + }, + "integrated": { + "heading": "Derzeit eingebunden", + "usedInLabel": "Verwendung in:", + "items": [ + { + "name": "community-scripts / ProxmoxVE", + "url": "https://community-scripts.github.io/ProxmoxVE/", + "description": "Führt die Arbeit von ttecks Helper Scripts fort. Umfangreicher Katalog von Skripten zur Bereitstellung von LXCs wie Home Assistant, Plex oder Frigate, VMs, Erweiterungen und PVE-Anpassungen in einem Durchgang.", + "usedIn": "Helper-Scripts-Starter und Netzwerk (Korrektur des NIC-Offloadings für Intel e1000e)" + } + ] + }, + "attribution": { + "heading": "Urheberangaben und Anerkennung", + "items": [ + "Die ursprünglichen Autoren werden stets im Menü und auf der Dokumentationsseite zum jeweiligen Skript genannt.", + "Ein Link zum Quell-Repository erscheint sowohl vor dem Skriptstart als auch auf der zugehörigen Dokumentationsseite.", + "Unterstützen Sie die Entwickler dieser externen Projekte mit einem ⭐, durch Sponsoring oder mit aussagekräftigen Fehlerberichten." + ] + }, + "report": { + "title": "Wo Sie Probleme mit externen Skripten melden können", + "body": "Funktioniert ein externes Skript nicht wie erwartet, melden Sie das direkt im jeweiligen Repository, nicht bei ProxMenux. ProxMenux lädt das Skript herunter und führt es unverändert aus. Der ursprüngliche Maintainer kann den Fehler einordnen und beheben. ProxMenux-Issues sind für Probleme mit dem Starter oder der Einbindung des Skripts gedacht." + }, + "suggest": { + "heading": "Vorschläge für neue externe Repositorys", + "intro": "Haben Sie ein Skript oder Repository gefunden, das gut in ein ProxMenux-Menü passt? Schlagen Sie es vor über:", + "discussionTitle": "💬 Diskussion eröffnen", + "discussionBody": "Ideal für Ideen: Holen Sie Rückmeldungen aus der Community ein, bevor daraus eine Funktionsanfrage wird.", + "issueTitle": "🐛 Issue eröffnen", + "issueBody": "Für konkrete Funktionsanfragen mit klar abgegrenztem Umfang." + }, + "candidate": { + "title": "Was macht einen guten Integrationskandidaten aus", + "items": [ + "Quelloffen und aktiv betreut.", + "Idempotent: kann sicher mehrfach ausgeführt werden.", + "Verändert die zentrale Proxmox-Konfiguration nicht unbemerkt.", + "Löst ein Problem, das ProxMenux noch nicht abdeckt, und vermeidet so doppelte Funktionen." + ] + } +} diff --git a/web/messages/de/docs/glossary.json b/web/messages/de/docs/glossary.json new file mode 100644 index 00000000..6d3ca65d --- /dev/null +++ b/web/messages/de/docs/glossary.json @@ -0,0 +1,677 @@ +{ + "meta": { + "title": "Glossar | ProxMenux-Dokumentation", + "description": "Definitionen der Begriffe aus der ProxMenux-Dokumentation zu Proxmox, Speicher, Netzwerken und Virtualisierung.", + "ogTitle": "Glossar | ProxMenux-Dokumentation", + "ogDescription": "Schnellreferenz für die technischen Begriffe, die in der ProxMenux-Dokumentation verwendet werden." + }, + "header": { + "title": "Glossar", + "description": "Kurze Definitionen der Begriffe aus ProxMenux und zur Funktionsweise von Proxmox.", + "section": "Glossar" + }, + "callout": { + "title": "Wie man diese Seite benutzt", + "bodyRich": "Jeder Eintrag enthält eine Kategorie und eine kurze Definition. Siehe auch führt zur passenden Dokumentation. Mit der A–Z-Navigation springen Sie direkt zu einem Buchstaben." + }, + "jumpHeading": "Zu einem Buchstaben springen", + "seeAlsoLabel": "Siehe auch:", + "aliasesLabel": "auch:", + "missingCallout": { + "title": "Ein Begriff fehlt?", + "leadRich": "Fehlt ein Begriff, erstellen Sie ein Issue oder einen PR auf GitHub. Das Glossar steht in web/messages/en/docs/glossary.json, ergänzt durch die jeweiligen Sprachkataloge." + }, + "entries": [ + { + "term": "ACS", + "aliases": [ + "Access Control Services", + "ACS-Override" + ], + "category": "Virtualisierung", + "definitionRich": "PCIe-Zugriffskontrollen zur Isolierung des Geräteverkehrs. IOMMU-Gruppen hängen von Hardware und Topologie ab. Der Kernel-Patch ACS override kann die von Linux gemeldeten Gruppen aufteilen, schafft aber keine Hardware-Isolierung; die Zuweisung an getrennte VMs birgt ein Sicherheitsrisiko.", + "seeAlso": [ + { + "label": "Controller oder NVMe zu VM hinzufügen", + "href": "/docs/disk-manager/add-controller-nvme-vm" + } + ] + }, + { + "term": "ARC", + "aliases": [ + "Adaptive Replacement Cache" + ], + "category": "Speicher", + "definitionRich": "ZFS-Lesecache im RAM des Hosts für kürzlich und häufig verwendete Datenblöcke. Das Standardlimit hängt von Proxmox-Version und Installationseinstellungen ab. ProxMenux bietet bei der Nachinstallation eine Option zur Begrenzung.", + "seeAlso": [ + { + "label": "Nachinstallation: Speicher", + "href": "/docs/post-install/storage" + }, + { + "label": "ZFS-Befehle", + "href": "/docs/help-info/zfs-commands" + } + ] + }, + { + "term": "Bind-Mount", + "category": "Linux", + "definitionRich": "Linux-Funktion, die einen Teil des Dateisystems unter einem weiteren Pfad zugänglich macht. ProxMenux nutzt Bind-Mounts, um Host-Verzeichnisse über pct set -mpN in LXC-Container einzubinden.", + "seeAlso": [ + { + "label": "LXC-Einhängepunkte", + "href": "/docs/storage-share/lxc-mount-points" + } + ] + }, + { + "term": "Netzwerk-Bond", + "aliases": [ + "Bündelung von Netzwerkschnittstellen", + "Link-Aggregation" + ], + "category": "Netzwerk", + "definitionRich": "Bündelt mehrere physische Netzwerkschnittstellen zu einer logischen Schnittstelle für Redundanz (active-backup), höheren Gesamtdurchsatz (LACP / 802.3ad) oder beides. Konfiguration in /etc/network/interfaces." + }, + { + "term": "Brücke", + "aliases": [ + "vmbrX", + "vmbr0", + "Linux Bridge" + ], + "category": "Netzwerk", + "definitionRich": "Virtueller Layer-2-Switch auf dem Host. vmbr0 verbindet Gäste üblicherweise mit dem physischen Netzwerk. Weitere Bridges (vmbr1, vmbr2...) können Gastnetze je nach physischen Verbindungen und Konfiguration trennen.", + "seeAlso": [ + { + "label": "Brückenanalyse und geführte Reparatur", + "href": "/docs/network/bridge-analysis" + } + ] + }, + { + "term": "Ceph", + "category": "Speicher", + "definitionRich": "Verteilter Speicher für hyperkonvergente Proxmox-Cluster. OSDs (Object Storage Daemons) bilden die Grundlage für Objekt-, Block- und Dateispeicher. Die erforderliche Ceph-Version hängt von Proxmox-Version und Upgrade-Pfad ab; beachten Sie die passende Upgrade-Anleitung.", + "seeAlso": [ + { + "label": "Aktualisieren Sie PVE 8 auf PVE 9", + "href": "/docs/utils/upgrade-pve8-pve9" + } + ] + }, + { + "term": "CT", + "aliases": [ + "Container", + "LXC-Container" + ], + "category": "Proxmox", + "definitionRich": "Proxmox-Kurzbezeichnung für einen LXC-Container. Seine numerische CTID verweist auf /etc/pve/lxc/<CTID>.conf. Siehe auch LXC.", + "seeAlso": [ + { + "label": "VMs und Container verwalten", + "href": "/docs/help-info/vm-ct-commands" + } + ] + }, + { + "term": "deb822", + "category": "Linux", + "definitionRich": "Modernes Format für APT-Paketquellen: .sources-Dateien mit strukturierten Blöcken und den Feldern Types, URIs, Suites, Components und Signed-By. Ersetzt das einzeilige .list-Format; Standard ab PVE 9 / Debian Trixie.", + "seeAlso": [ + { + "label": "Aktualisieren Sie PVE 8 auf PVE 9", + "href": "/docs/utils/upgrade-pve8-pve9" + } + ] + }, + { + "term": "dGPU", + "aliases": [ + "Dedizierte GPU" + ], + "category": "Virtualisierung", + "definitionRich": "Separate Grafikkarte, meist über PCIe angebunden, im Gegensatz zu einer iGPU. Sie kann per VFIO einer VM zugewiesen oder über Gerätedateien und den Host-Treiber von mehreren LXC-Containern genutzt werden, sofern Hardware und Anwendungen dies unterstützen.", + "seeAlso": [ + { + "label": "GPU an VM durchreichen", + "href": "/docs/hardware/gpu-vm-passthrough" + } + ] + }, + { + "term": "Dialog", + "category": "Linux", + "definitionRich": "Auf ncurses basierendes Werkzeug für Menüs, Eingabeaufforderungen und Fortschrittsbalken im Terminal. Die Menüs von ProxMenux verwenden dialog." + }, + { + "term": "DKMS", + "aliases": [ + "Unterstützung von dynamischen Kernmodulen" + ], + "category": "Linux", + "definitionRich": "Framework zum Neubauen registrierter externer Kernelmodule wie NVIDIA oder ZFS bei Kerneländerungen. Erfolgreiche Builds benötigen kompatiblen Modulquellcode und passende Kernel-Header.", + "seeAlso": [ + { + "label": "Installieren Coral TPU (Host)", + "href": "/docs/hardware/install-coral-tpu-host" + } + ] + }, + { + "term": "Fail2Ban", + "category": "Linux", + "definitionRich": "Dienst, der Protokolle auf wiederholte Anmeldefehler überwacht und die betreffenden IP-Adressen über die Firewall sperrt. ProxMenux liefert vorkonfigurierte Jails für SSH, die Proxmox-Oberfläche und Monitor.", + "seeAlso": [ + { + "label": "Fail2Ban", + "href": "/docs/security/fail2ban" + } + ] + }, + { + "term": "Fastfetch", + "category": "Linux", + "definitionRich": "Systeminformationswerkzeug mit ASCII-Grafik beim Anmelden an der Shell. Ersetzt neofetch und kann bei der ProxMenux-Nachinstallation optional aktiviert werden.", + "seeAlso": [ + { + "label": "Nach der Installation: Optionales", + "href": "/docs/post-install/optional" + } + ] + }, + { + "term": "GRUB", + "aliases": [ + "GRand Unified Bootloader" + ], + "category": "Linux", + "definitionRich": "Bootloader für GRUB-basierte Linux-Installationen. Konfiguration in /etc/default/grub und /boot/grub/; Änderungen mit update-grub übernehmen. Proxmox kann auch systemd-boot verwenden; prüfen Sie vor Änderungen an Kernelparametern den aktiven Bootloader.", + "seeAlso": [ + { + "label": "GPU Durchgangsbefehle", + "href": "/docs/help-info/gpu-commands" + } + ] + }, + { + "term": "iGPU", + "aliases": [ + "Integrierte GPU" + ], + "category": "Virtualisierung", + "definitionRich": "Im CPU-Paket integrierter Grafikprozessor, etwa Intel UHD / Iris oder Radeon in Ryzen-APUs. Wird häufig über /dev/dri von mehreren LXC-Containern genutzt, statt exklusiv an eine VM durchgereicht zu werden.", + "seeAlso": [ + { + "label": "GPU zu LXC hinzufügen", + "href": "/docs/hardware/igpu-acceleration-lxc" + } + ] + }, + { + "term": "ifupdown / ifupdown2", + "category": "Netzwerk", + "definitionRich": "Klassische Debian-Netzwerkkonfiguration über /etc/network/interfaces. Proxmox verwendet standardmäßig die neu entwickelte Variante ifupdown2, die laufende Konfigurationen mit ifreload -a neu laden kann." + }, + { + "term": "initramfs", + "category": "Linux", + "definitionRich": "Anfängliches RAM-Dateisystem für den Start, bevor das eigentliche Root-Dateisystem eingehängt ist. Enthält früh benötigte Treiber. Nach Änderungen an VFIO, IOMMU oder Speichertreibern mit update-initramfs -u -k all neu erstellen." + }, + { + "term": "IOMMU", + "aliases": [ + "Eingangs-/Ausgangsspeicherverwaltungseinheit", + "VT-d", + "AMD-Vi" + ], + "category": "Virtualisierung", + "definitionRich": "Hardware-Funktion (Intel VT-d / AMD-Vi) für PCIe-Passthrough an VMs. Im Firmware-Setup aktivieren und die Kernel-Unterstützung prüfen. Je nach Plattform und Kernelvorgaben können Parameter wie intel_iommu=on oder amd_iommu=on erforderlich sein.", + "seeAlso": [ + { + "label": "Post-Install: Virtualisierung", + "href": "/docs/post-install/virtualization" + }, + { + "label": "GPU an VM durchreichen", + "href": "/docs/hardware/gpu-vm-passthrough" + } + ] + }, + { + "term": "IOMMU-Gruppe", + "category": "Virtualisierung", + "definitionRich": "Gerätegruppe als kleinste IOMMU-Isolationseinheit. Geräte innerhalb einer Gruppe sind nicht unbedingt gegeneinander isoliert; prüfen Sie vor VFIO-Zuweisungen alle Mitglieder. Auflisten mit find /sys/kernel/iommu_groups/ -type l | sort -V." + }, + { + "term": "iSCSI", + "aliases": [ + "Internet SCSI" + ], + "category": "Speicher", + "definitionRich": "Protokoll für Blockspeicher über IP: Ein entferntes Laufwerk erscheint als lokales SCSI-Gerät. In Proxmox mit pvesm add iscsi einbinden.", + "seeAlso": [ + { + "label": "Hinzufügen von iSCSI-Ziel als Proxmox-Speicher", + "href": "/docs/storage-share/host-iscsi" + } + ] + }, + { + "term": "KVM", + "aliases": [ + "Kernel-basierte virtuelle Maschine" + ], + "category": "Virtualisierung", + "definitionRich": "Linux-Kernelmodul, das den Host zum Hypervisor macht. Zusammen mit QEMU für die Geräteemulation bildet es die Grundlage der Proxmox-VMs." + }, + { + "term": "LVM", + "aliases": [ + "Logical Volume Manager" + ], + "category": "Speicher", + "definitionRich": "Linux-Volumeverwaltung mit Volume Groups (VG) aus physischem Speicher und darin liegenden Logical Volumes (LV). LVM-basierte Proxmox-Installationen nutzen häufig LVM-thin für Gastlaufwerke; andere Speicherarten sind möglich. Werkzeuge: pvs, vgs und lvs.", + "seeAlso": [ + { + "label": "Befehle für Speicher und Datenträger", + "href": "/docs/help-info/storage-commands" + } + ] + }, + { + "term": "LXC", + "aliases": [ + "Linux Containers" + ], + "category": "Linux", + "definitionRich": "Virtualisierung auf Betriebssystemebene: Isolierte Benutzerumgebungen teilen den Host-Kernel. Üblicherweise weniger Overhead als VMs, jedoch mit anderer Isolationsgrenze. Proxmox verwaltet LXC-Container über pct.", + "seeAlso": [ + { + "label": "VMs und Container verwalten", + "href": "/docs/help-info/vm-ct-commands" + } + ] + }, + { + "term": "Lynis", + "category": "Linux", + "definitionRich": "Open-Source-Sicherheitsprüfer von CISOfy. Analysiert den Host und liefert einen Härtungswert sowie konkrete Verbesserungsvorschläge. ProxMenux installiert die aktuelle Version aus dem ursprünglichen GitHub-Repository statt des älteren Debian-Pakets.", + "seeAlso": [ + { + "label": "Lynis", + "href": "/docs/security/lynis" + } + ] + }, + { + "term": "MAC-Adresse", + "aliases": [ + "Hardware-Adresse" + ], + "category": "Netzwerk", + "definitionRich": "Meist 48-Bit-Kennung einer Ethernet-Schnittstelle, durch Hardware oder Software vergeben. ProxMenux ordnet unter Persistente Netzwerknamen MAC-Adressen feste Namen (eno1, enp3s0) zu, damit diese nach einer PCI-Neunummerierung gleich bleiben.", + "seeAlso": [ + { + "label": "Persistente Schnittstellennamen", + "href": "/docs/network/persistent-names" + } + ] + }, + { + "term": "MOTD", + "aliases": [ + "Botschaft des Tages" + ], + "category": "Linux", + "definitionRich": "Text nach der SSH-Anmeldung, gespeichert in /etc/motd. ProxMenux kann bei der Nachinstallation ein Banner hinzufügen; der Deinstaller stellt die vorherige Datei wieder her, falls eine Sicherung existiert." + }, + { + "term": "NFS", + "aliases": [ + "Netzwerkdateisystem" + ], + "category": "Speicher", + "definitionRich": "Aus Unix stammendes Protokoll zur Dateifreigabe im Netzwerk. In Proxmox mit pvesm add nfs als Speicher einbinden; ProxMenux bietet außerdem eigene Abläufe für NFS-Clients und -Server in LXC.", + "seeAlso": [ + { + "label": "Speicher- und Freigabemanager", + "href": "/docs/storage-share" + } + ] + }, + { + "term": "nftables / iptables", + "category": "Netzwerk", + "definitionRich": "Linux-Firewallwerkzeuge. nftables ist das moderne Framework; iptables unterstützt klassische oder nft-kompatible Backends. Das klassische pve-firewall verwendet iptables, das alternative proxmox-firewall hingegen nftables.", + "seeAlso": [ + { + "label": "Netzbefehle", + "href": "/docs/help-info/network-commands" + } + ] + }, + { + "term": "nouveau", + "category": "Linux", + "definitionRich": "Offener NVIDIA-Grafiktreiber im Linux-Kernel. Beim Installieren des proprietären NVIDIA-Treibers verhindern Sie, dass nouveau dieselbe GPU belegt, üblicherweise durch Blacklisting und Neubauen des initramfs.", + "seeAlso": [ + { + "label": "NVIDIA Installationsleitfaden", + "href": "/guides/nvidia" + } + ] + }, + { + "term": "No-Subscription-Repository", + "category": "Proxmox", + "definitionRich": "Proxmox-Paketquelle ohne Enterprise-Abonnement. Pakete werden weniger umfassend validiert als im Enterprise-Repository, das Proxmox für den Produktivbetrieb empfiehlt. ProxMenux bietet die Repository-Konfiguration bei der Nachinstallation.", + "seeAlso": [ + { + "label": "Post-Install: Grundeinstellungen", + "href": "/docs/post-install/basic-settings" + } + ] + }, + { + "term": "OVA / OVF", + "aliases": [ + "Open Virtualization Format" + ], + "category": "Virtualisierung", + "definitionRich": "DMTF-Formate zum Verpacken portabler VMs. OVA bündelt die Dateien in einem TAR-Archiv; OVF beschreibt die VM in XML mit zugehörigen Laufwerksdateien, häufig VMDK. Importmöglichkeiten hängen vom Zielhypervisor und der Konfiguration ab.", + "seeAlso": [ + { + "label": "VM nach OVA / OVF exportieren", + "href": "/docs/utils/export-vm" + }, + { + "label": "VM von OVA / OVF importieren", + "href": "/docs/utils/import-vm" + } + ] + }, + { + "term": "OVMF", + "aliases": [ + "UEFI Firmware", + "Open Virtual Machine Firmware" + ], + "category": "Virtualisierung", + "definitionRich": "UEFI-Firmware für VMs, auswählbar mit qm set <vmid> -bios ovmf. Für UEFI-Gäste, etwa Windows 11 und Secure-Boot-Konfigurationen. efidisk0 speichert EFI-Variablen dauerhaft; viele andere Gäste können weiterhin klassisches BIOS nutzen.", + "seeAlso": [ + { + "label": "GPU an VM durchreichen", + "href": "/docs/hardware/gpu-vm-passthrough" + } + ] + }, + { + "term": "PBS", + "aliases": [ + "Proxmox Backup Server" + ], + "category": "Proxmox", + "definitionRich": "Eigenständiges Proxmox-Sicherungsprodukt mit inkrementeller Übertragung, Deduplizierung und optionaler clientseitiger Verschlüsselung für VMs und CTs. Als vzdump-Ziel nutzbar; ein separater Backupserver ist Sicherungen auf dem geschützten Host vorzuziehen." + }, + { + "term": "pct", + "category": "Proxmox", + "definitionRich": "Kommandozeilenwerkzeug für LXC-Container in Proxmox: pct list / start / stop / shutdown / config / enter / exec / push / pull / destroy.", + "seeAlso": [ + { + "label": "VMs und Container verwalten", + "href": "/docs/help-info/vm-ct-commands" + } + ] + }, + { + "term": "pmxcfs", + "aliases": [ + "Proxmox Cluster Filesystem" + ], + "category": "Proxmox", + "definitionRich": "FUSE-basiertes virtuelles Dateisystem unter /etc/pve/. Repliziert Konfigurationsänderungen zwischen den Clusterknoten; Änderungen in /etc/pve/ werden automatisch verteilt." + }, + { + "term": "pveam", + "aliases": [ + "Proxmox-VE-Appliance-Verwaltung" + ], + "category": "Proxmox", + "definitionRich": "Werkzeug für den lokalen CT-Vorlagenkatalog. pveam update aktualisiert den Index; pveam available listet Vorlagen auf und pveam download <storage> <template> lädt eine herunter." + }, + { + "term": "pveproxy", + "category": "Proxmox", + "definitionRich": "HTTPS-Dienst für Proxmox-Weboberfläche und Webterminal auf Port 8006. Wird bei Hauptversions-Upgrades neu gestartet; deshalb verweigert ProxMenux das Upgrade PVE 8 → 9 im Webterminal." + }, + { + "term": "pvesm", + "aliases": [ + "Proxmox-Speicherverwaltung" + ], + "category": "Proxmox", + "definitionRich": "CLI-Tool zur Verwaltung von Proxmox-Speicher. pvesm status zeigt alle konfigurierten Speicher an; pvesm add <type> registriert neue (NFS, Samba, iSCSI, dir, ZFS, LVM, ...).", + "seeAlso": [ + { + "label": "Befehle für Speicher und Datenträger", + "href": "/docs/help-info/storage-commands" + } + ] + }, + { + "term": "Privilegierter Container", + "category": "Linux", + "definitionRich": "LXC-Container, dessen root-Benutzer auf Host-UID 0 abgebildet wird; Namespaces und weitere Einschränkungen gelten weiterhin. Weniger isoliert als unprivilegierte Container. GPU-, USB- und Samba-Zugriff benötigen diesen Modus nicht grundsätzlich; Berechtigungen und UID-Zuordnungen sind entscheidend." + }, + { + "term": "ProxMenux Monitor", + "category": "ProxMenux", + "definitionRich": "Optionales Web-Dashboard für ProxMenux. Zeigt aktuelle Host-Statistiken zu CPU, RAM, Datenträgern und Netzwerk sowie eine VM-/LXC-Übersicht; Anmeldung mit 2FA über TCP-Port 8008.", + "seeAlso": [ + { + "label": "ProxMenux Monitor", + "href": "/docs/settings/proxmenux-monitor" + } + ] + }, + { + "term": "qcow2", + "aliases": [ + "QEMU Copy-On-Write v2" + ], + "category": "Speicher", + "definitionRich": "VM-Laufwerksformat, häufig auf Verzeichnisspeichern genutzt. Unterstützt bedarfsgerechte Speicherbelegung, Snapshots und Copy-on-Write. Konvertieren mit qemu-img convert -O qcow2 ...." + }, + { + "term": "qm", + "aliases": [ + "QEMU-Verwaltung" + ], + "category": "Proxmox", + "definitionRich": "Kommandozeilenwerkzeug für VMs in Proxmox: qm list / start / stop / shutdown / config / set / destroy / importdisk / migrate. Entspricht pct für Container.", + "seeAlso": [ + { + "label": "VMs und Container verwalten", + "href": "/docs/help-info/vm-ct-commands" + } + ] + }, + { + "term": "Q35", + "category": "Virtualisierung", + "definitionRich": "QEMU-Maschinentyp mit emuliertem Intel-Q35-Chipsatz und PCIe-Unterstützung. Gegenüber i440FX für PCIe-Passthrough und moderne Gäste empfohlen. Einstellen mit qm set <vmid> -machine q35." + }, + { + "term": "QEMU", + "category": "Virtualisierung", + "definitionRich": "Freier Maschinenemulator und Virtualisierer. Proxmox kombiniert ihn mit KVM, um VMs mit nahezu nativer Leistung auszuführen." + }, + { + "term": "rclone", + "category": "Linux", + "definitionRich": "Kommandozeilenwerkzeug für zahlreiche Cloudspeicher wie Google Drive, Dropbox, OneDrive, Mega und S3. Die Cloud-Backup-Anleitung nutzt es, um entfernten Speicher als Verzeichnis für vzdump-Archive einzuhängen.", + "seeAlso": [ + { + "label": "Sicherungen in persönlichem Cloud-Speicher", + "href": "/guides/backup_cloud" + } + ] + }, + { + "term": "Samba / SMB / CIFS", + "category": "Speicher", + "definitionRich": "Microsoft-Protokollfamilie für Dateifreigaben. SMB ist das Protokoll, Samba die freie Linux-Implementierung und CIFS ein älterer SMB-Dialekt. In Proxmox mit pvesm add cifs einbinden.", + "seeAlso": [ + { + "label": "Speicher- und Freigabemanager", + "href": "/docs/storage-share" + } + ] + }, + { + "term": "SeaBIOS", + "category": "Virtualisierung", + "definitionRich": "Freie Implementierung eines klassischen BIOS für QEMU. Standardfirmware neuer VMs, sofern nicht OVMF gewählt wird. Geeignet für ältere Systeme und Linux-Installationen ohne UEFI-Anforderung." + }, + { + "term": "smartctl / SMART", + "aliases": [ + "Self-Monitoring, Analysis and Reporting Technology" + ], + "category": "Speicher", + "definitionRich": "In Laufwerke integrierte Diagnose für Zustand, Fehlerzähler und Selbsttests. Das Kommandozeilenwerkzeug ist smartctl aus smartmontools; nvme-cli ergänzt NVMe-spezifische Befehle.", + "seeAlso": [ + { + "label": "SMART-Festplattenstatus und -test", + "href": "/docs/disk-manager/smart-disk-test" + }, + { + "label": "Speicherbefehle → SMART Abschnitt", + "href": "/docs/help-info/storage-commands" + } + ] + }, + { + "term": "systemd / systemctl / journalctl", + "category": "Linux", + "definitionRich": "Init-System von Debian / Proxmox. systemctl verwaltet Dienste: starten, stoppen, aktivieren und Status anzeigen. journalctl liest das binäre Journal von systemd-journald." + }, + { + "term": "sysctl", + "category": "Linux", + "definitionRich": "Werkzeug zum Lesen und Ändern von Kernelparametern. Dauerhafte Einstellungen stehen üblicherweise in /etc/sysctl.d/*.conf; die Unterstützung für /etc/sysctl.conf hängt vom Ladedienst ab. ProxMenux bietet bei der Nachinstallation ein sysctl-Profil.", + "seeAlso": [ + { + "label": "Post-Install: Netzwerk", + "href": "/docs/post-install/network" + } + ] + }, + { + "term": "trixie", + "category": "Linux", + "definitionRich": "Codename für Debian 13, die OS-Basis für Proxmox VE 9. Vorgänger: bookworm (Debian 12 / PVE 8).", + "seeAlso": [ + { + "label": "Aktualisieren Sie PVE 8 auf PVE 9", + "href": "/docs/utils/upgrade-pve8-pve9" + } + ] + }, + { + "term": "udev", + "category": "Linux", + "definitionRich": "Linux-Geräteverwaltung. Reagiert auf Hardware-Ereignisse und erstellt oder entfernt Einträge unter /dev. Regeln in /etc/udev/rules.d/ können Schnittstellennamen an MAC-Adressen binden, NVIDIA-Gerätedateien beim Laden des Treibers erstellen und weitere Aufgaben übernehmen." + }, + { + "term": "UID / GID idmap", + "aliases": [ + "UID-Schaltung" + ], + "category": "Linux", + "definitionRich": "Bei der Standardzuordnung unprivilegierter LXC-Container sind Host-UIDs und -GIDs um +100000 verschoben: Container-UID 0 entspricht Host-UID 100000, UID 1000 entspricht 101000. Eigene Zuordnungen können abweichen; Dateieigentümer auf Bind-Mounts müssen dazu passen.", + "seeAlso": [ + { + "label": "LXC-Einhängepunkte", + "href": "/docs/storage-share/lxc-mount-points" + } + ] + }, + { + "term": "Unprivilegierter Container", + "category": "Linux", + "definitionRich": "LXC-Container, dessen root auf eine unprivilegierte Host-UID abgebildet wird, normalerweise 100000. Das begrenzt Host-Rechte bei gemeinsamem Kernel. Hardwarezugriff bleibt mit passenden Geräteberechtigungen und Zuordnungen möglich; privilegierter Betrieb ist nicht grundsätzlich nötig." + }, + { + "term": "VFIO", + "aliases": [ + "Virtuelle Funktion I/O" + ], + "category": "Virtualisierung", + "definitionRich": "Linux-Kernel-Framework für sichere Gerätetreiber im Userspace, genutzt beim direkten Durchreichen von PCIe-Geräten an VMs. Beteiligt sind vfio, vfio_iommu_type1 und vfio_pci; Konfiguration in /etc/modprobe.d/vfio.conf.", + "seeAlso": [ + { + "label": "Post-Install: Virtualisierung", + "href": "/docs/post-install/virtualization" + } + ] + }, + { + "term": "VirtIO", + "category": "Virtualisierung", + "definitionRich": "Paravirtualisierte Treiber für Netzwerk, Speicher, Memory-Ballooning und GPU. Vermeiden den Aufwand emulierter älterer Hardware wie e1000 oder IDE. Der Gast benötigt passende Treiber: in Linux enthalten, für Windows als separate ISO verfügbar." + }, + { + "term": "VLAN", + "aliases": [ + "Virtuelles LAN" + ], + "category": "Netzwerk", + "definitionRich": "802.1Q-Tags zur Aufteilung eines physischen Netzwerks in mehrere logische Netze. Pro Bridge oder Netzwerkschnittstelle in /etc/network/interfaces konfigurierbar." + }, + { + "term": "VM", + "aliases": [ + "Virtuelle Maschine" + ], + "category": "Proxmox", + "definitionRich": "Von Proxmox mit QEMU/KVM verwaltete virtuelle Maschine mit eigenem Gast-Kernel. Ihre numerische VMID entspricht /etc/pve/qemu-server/<VMID>.conf. Üblicherweise mehr Overhead als LXC, mit getrennter Kernel-Isolationsgrenze.", + "seeAlso": [ + { + "label": "VMs und Container verwalten", + "href": "/docs/help-info/vm-ct-commands" + } + ] + }, + { + "term": "vzdump", + "category": "Proxmox", + "definitionRich": "Proxmox-Sicherungswerkzeug für VMs und CTs. Dateiarchive sind nicht inkrementell; PBS-Ziele unterstützen inkrementelle Übertragungen. Aufträge legen Modus (snapshot / suspend / stop), Kompression (etwa zstd, gzip oder lzo), Aufbewahrung und Benachrichtigungen fest.", + "seeAlso": [ + { + "label": "Befehle für Sicherung und Wiederherstellung", + "href": "/docs/help-info/backup-commands" + } + ] + }, + { + "term": "ZFS", + "aliases": [ + "Zettabyte File System" + ], + "category": "Speicher", + "definitionRich": "Dateisystem und Volumeverwaltung mit Snapshots, Send/Receive-Replikation, Prüfsummen und Kompression. Beschädigte Daten können mit einer gültigen redundanten Kopie repariert werden. Der Lesecache ARC nutzt RAM und lässt sich begrenzen.", + "seeAlso": [ + { + "label": "Befehle zur ZFS-Verwaltung", + "href": "/docs/help-info/zfs-commands" + } + ] + } + ] +} diff --git a/web/messages/de/docs/help-info/backup-commands.json b/web/messages/de/docs/help-info/backup-commands.json new file mode 100644 index 00000000..a9005c12 --- /dev/null +++ b/web/messages/de/docs/help-info/backup-commands.json @@ -0,0 +1,235 @@ +{ + "meta": { + "title": "Proxmox-Sicherungsbefehle — vzdump, qmrestore, pct restore | ProxMenux", + "description": "Referenz der Sicherungs- und Wiederherstellungsbefehle von Proxmox VE: vzdump für VMs und LXC-Container, qmrestore für VMs, pct restore für Container, geplante Sicherungen in /etc/pve/jobs.cfg, Hook-Skripte und auszuschließende Pfade.", + "ogTitle": "Proxmox-Sicherungsbefehle — vzdump, qmrestore, pct restore", + "ogDescription": "Referenz der Befehle vzdump, qmrestore und pct restore zum Sichern und Wiederherstellen von Proxmox VMs und LXC Containern.", + "twitterTitle": "Proxmox Backup Befehle | ProxMenux", + "twitterDescription": "vzdump, qmrestore und pct restore Befehle zum Sichern und Wiederherstellen von Proxmox VMs und Containern." + }, + "header": { + "title": "Sicherung und Wiederherstellung", + "description": "Kuratierte Referenz der Befehle vzdump, qmrestore und pct restore für Proxmox Backup-Workflows. Enthält Planung, erweiterte Optionen wie Hook-Skripte / Ausschlusspfade / I / O-Priorität und Backup-Dateiverwaltung.", + "section": "Hilfe und Informationen" + }, + "intro": { + "title": "Drei Backup-Modi", + "body": "Bei VMs ist snapshot eine Live-Sicherung ohne Speicher-Snapshot-Anforderung. suspend pausiert die VM davor und verbessert die Konsistenz nicht unbedingt. stop fährt sie zunächst herunter; eine zuvor laufende VM kann nach Beginn der Sicherung weiterlaufen. Bei LXC ist es anders: snapshot benötigt Snapshot-fähigen Speicher, suspend nutzt zwei rsync-Durchläufe, und stop hält den Container während der gesamten Sicherung an." + }, + "commandGroups": [ + { + "title": "VM-Sicherung", + "commands": [ + { + "command": "vzdump ", + "description": "Erstellen Sie ein Backup eines bestimmten VM/CT" + }, + { + "command": "vzdump --storage ", + "description": "VM auf einen bestimmten Speicher sichern" + }, + { + "command": "vzdump --mode snapshot", + "description": "Snapshot-Backup erstellen (für VMs)" + }, + { + "command": "vzdump --mode suspend", + "description": "VM vor der Live-Sicherung pausieren, veralteter suspend-Modus" + }, + { + "command": "vzdump --mode stop", + "description": "VM vor dem Start der Sicherung herunterfahren" + }, + { + "command": "vzdump --all", + "description": "Backup aller VMs und Container" + }, + { + "command": "vzdump --all --exclude ,", + "description": "Backup aller außer spezifizierten VMs" + } + ] + }, + { + "title": "Backup-Optionen", + "commands": [ + { + "command": "vzdump --compress zstd", + "description": "Verwenden Sie zstd Kompression für Backup" + }, + { + "command": "vzdump --compress gzip --pigz ", + "description": "Verwenden Sie pigz mit mehreren Threads" + }, + { + "command": "vzdump --storage --notes-template ", + "description": "Notizen zum Backup hinzufügen" + }, + { + "command": "vzdump --mailto ", + "description": "Senden Sie Benachrichtigung E-Mail" + }, + { + "command": "vzdump --prune-backups keep-last=", + "description": "Halten Sie nur n Backups pro VM" + }, + { + "command": "vzdump --stdexcludes 0", + "description": "Keine temporären Dateien ausschließen" + }, + { + "command": "vzdump --quiet 1", + "description": "Ausgabemeldungen unterdrücken" + } + ] + }, + { + "title": "Backups wiederherstellen", + "commands": [ + { + "command": "qmrestore ", + "description": "VM aus dem Backup wiederherstellen" + }, + { + "command": "qmrestore --storage ", + "description": "Wiederherstellen zu einem bestimmten Speicher" + }, + { + "command": "qmrestore --unique", + "description": "VM mit neu erzeugten zufälligen MAC-Adressen wiederherstellen" + }, + { + "command": "pct restore ", + "description": "Container aus dem Backup wiederherstellen" + }, + { + "command": "pct restore --storage ", + "description": "Container auf einen bestimmten Speicher wiederherstellen" + }, + { + "command": "pct restore --rootfs :", + "description": "Mit Root-Disk auf dem gewählten Speicher und in der gewählten Größe wiederherstellen" + }, + { + "command": "pct restore --unprivileged 1", + "description": "Wiederherstellen als unprivilegierte CT" + } + ] + }, + { + "title": "Sicherungsverwaltung", + "commands": [ + { + "command": "ls -la /var/lib/vz/dump/", + "description": "Backups am Standardstandort auflisten" + }, + { + "command": "find /var/lib/vz/dump/ -name \"*.vma*\"", + "description": "VM Backups finden" + }, + { + "command": "find /var/lib/vz/dump/ -name \"*.tar*\"", + "description": "Container-Backups finden" + }, + { + "command": "pvesm list --content backup", + "description": "Backups im spezifischen Speicher auflisten" + }, + { + "command": "rm /var/lib/vz/dump/", + "description": "Löschen einer Backup-Datei" + }, + { + "command": "cat /etc/vzdump.conf", + "description": "Backup Konfiguration anzeigen" + } + ] + }, + { + "title": "Geplante Backups", + "commands": [ + { + "command": "cat /etc/pve/jobs.cfg", + "description": "Backup-Zeitplan anzeigen" + }, + { + "command": "nano /etc/vzdump.conf", + "description": "Backup-Konfiguration bearbeiten" + }, + { + "command": "systemctl list-timers", + "description": "systemd-Timer auflisten" + }, + { + "command": "systemctl status cron", + "description": "Status des cron-Dienstes prüfen" + }, + { + "command": "pvenode task list --typefilter vzdump", + "description": "Abgeschlossene Sicherungsaufgaben auflisten" + }, + { + "command": "pvenode task log ", + "description": "Protokoll einer Sicherungsaufgabe anhand der UPID anzeigen" + } + ] + }, + { + "title": "Fortgeschrittene Operationen", + "commands": [ + { + "command": "qmrestore --force", + "description": "Wiederherstellung erzwingen und vorhandene VM überschreiben" + }, + { + "command": "vzdump --dumpdir ", + "description": "Benutzerdefiniertes Backup-Verzeichnis angeben" + }, + { + "command": "vzdump --script