mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 10:47:36 +00:00
refine post-install and hardware GPU docs, Monitor UX and CLI styling
- rewrite the 15 post-install pages and the 3 hardware GPU pages so they reflect the current scripts (reversibility, tracked-tool counts, kernel parameters, per-tool commands, Alpine LXC propagation flow) - migrate the legacy step-badge helper on post-install/optional and create-vm/synology to the canonical pill component, with the stepLabel key added in each locale - fix rich-text i18n calls missing helpers across network, automated, optional, security, customization and the post-install landing pages, and escape the `<iface>` placeholder in automated so intl no longer parses it as a tag - remove the mouse-follow blue overlay from the docs landing layout - reposition the App-tab Edit button and stack the Search and Register controls vertically on mobile - move the Bulk update Configure/Edit control into the section header so it behaves the same on desktop and mobile - show a spinner during the final autoremove/autoclean pass of update-pve-safe so the cleanup step reads as active instead of silent - restyle the shell spinner and msg_info in a distinctive purple and drop the unused msg_lang duplicate - add a web-docs i18n build script and its CI workflow, plus tests for the pushover notification channel
This commit is contained in:
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import ast
|
import ast
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -160,7 +162,10 @@ def translate_googletrans(text: str, dest_lang: str, context: str) -> str:
|
|||||||
|
|
||||||
translator = Translator()
|
translator = Translator()
|
||||||
full_text = f"{context} {text}".strip()
|
full_text = f"{context} {text}".strip()
|
||||||
return translator.translate(full_text, dest=dest_lang).text
|
result = translator.translate(full_text, dest=dest_lang)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
result = asyncio.run(result)
|
||||||
|
return result.text
|
||||||
|
|
||||||
|
|
||||||
def translate_google_web(text: str, dest_lang: str, context: str, timeout: int) -> str:
|
def translate_google_web(text: str, dest_lang: str, context: str, timeout: int) -> str:
|
||||||
|
|||||||
@@ -0,0 +1,853 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build missing translations for the ProxMenux documentation catalog."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from build_translation_cache import ( # noqa: E402
|
||||||
|
clean_translation,
|
||||||
|
translate_appimage,
|
||||||
|
translate_google_web,
|
||||||
|
translate_googletrans,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_LANGUAGES = ("es", "de", "fr", "it", "pt", "sk", "sv")
|
||||||
|
DEFAULT_CONTEXT = (
|
||||||
|
"Context: ProxMenux technical documentation for Proxmox VE users. "
|
||||||
|
"Preserve product names, commands, paths, variables and placeholders. Translate:"
|
||||||
|
)
|
||||||
|
|
||||||
|
TECHNICAL_TERMS = (
|
||||||
|
"Proxmox VE Helper-Scripts",
|
||||||
|
"Proxmox Backup Server",
|
||||||
|
"Proxmox Mail Gateway",
|
||||||
|
"Proxmox VE",
|
||||||
|
"ProxMenux Monitor",
|
||||||
|
"ProxMenux Scripts",
|
||||||
|
"Docker Compose",
|
||||||
|
"Docker Engine",
|
||||||
|
"Google Coral",
|
||||||
|
"Edge TPU",
|
||||||
|
"Let's Encrypt",
|
||||||
|
"Cloudflare",
|
||||||
|
"Pushover",
|
||||||
|
"Telegram",
|
||||||
|
"Discord",
|
||||||
|
"Microsoft Teams",
|
||||||
|
"GitHub",
|
||||||
|
"Gotify",
|
||||||
|
"Apprise",
|
||||||
|
"Frigate",
|
||||||
|
"Vaultwarden",
|
||||||
|
"Portainer",
|
||||||
|
"ProxMenux",
|
||||||
|
"AppImage",
|
||||||
|
"systemctl",
|
||||||
|
"journalctl",
|
||||||
|
"smartctl",
|
||||||
|
"pveproxy",
|
||||||
|
"apt-get",
|
||||||
|
"gasket-dkms",
|
||||||
|
"libedgetpu",
|
||||||
|
"QEMU",
|
||||||
|
"LXC",
|
||||||
|
"ZFS",
|
||||||
|
"Ceph",
|
||||||
|
"Docker",
|
||||||
|
"OpenAI",
|
||||||
|
"WebSocket",
|
||||||
|
"OAuth",
|
||||||
|
"DKMS",
|
||||||
|
"SSH",
|
||||||
|
"API",
|
||||||
|
)
|
||||||
|
|
||||||
|
PROTECTED_PATTERNS = (
|
||||||
|
# Keep rich-text tags visible to Google Translate. It preserves their
|
||||||
|
# structure while translating the enclosed prose, whereas replacing
|
||||||
|
# opening/closing tags with adjacent sentinels can make the provider drop
|
||||||
|
# one side of the pair. The contract is validated after translation.
|
||||||
|
re.compile(r"`[^`]+`"),
|
||||||
|
re.compile(r"https?://[^\s<>]+"),
|
||||||
|
re.compile(r"\{[A-Za-z_][A-Za-z0-9_.-]*\}"),
|
||||||
|
re.compile(r"\$\{?[A-Za-z_][A-Za-z0-9_]*\}?"),
|
||||||
|
re.compile(r"(?<![\w-])--[A-Za-z0-9][A-Za-z0-9_-]*"),
|
||||||
|
re.compile(r"(?<![A-Za-z0-9<])/(?:[A-Za-z0-9._~:@%+=-]+/)*[A-Za-z0-9._~:@%+=-]+"),
|
||||||
|
re.compile(r"\b[A-Za-z0-9_.-]+\.(?:json|ya?ml|toml|conf|service|socket|sh|py|tsx?|jsx?|md)\b"),
|
||||||
|
)
|
||||||
|
LITERAL_TAG_RE = re.compile(
|
||||||
|
r"<(code|kbd|pre)\b[^>]*>.*?</\1>",
|
||||||
|
re.IGNORECASE | re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
TERM_RE = re.compile(
|
||||||
|
"|".join(
|
||||||
|
rf"(?<![A-Za-z0-9_]){re.escape(term)}(?![A-Za-z0-9_])"
|
||||||
|
for term in sorted(TECHNICAL_TERMS, key=len, reverse=True)
|
||||||
|
),
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
TAG_RE = re.compile(r"</?([A-Za-z][A-Za-z0-9]*)>")
|
||||||
|
PLACEHOLDER_RE = re.compile(r"\{[A-Za-z_][A-Za-z0-9_.-]*\}")
|
||||||
|
NON_TRANSLATABLE_KEYS = {
|
||||||
|
"command",
|
||||||
|
"code",
|
||||||
|
"href",
|
||||||
|
"icon",
|
||||||
|
"id",
|
||||||
|
"path",
|
||||||
|
"route",
|
||||||
|
"slug",
|
||||||
|
"src",
|
||||||
|
"url",
|
||||||
|
}
|
||||||
|
SOURCE_STATE_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Leaf:
|
||||||
|
path: tuple[str | int, ...]
|
||||||
|
source: str
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(path: Path) -> Any:
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: Path, value: Any) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
temporary.write_text(
|
||||||
|
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
temporary.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_leaves(node: Any, path: tuple[str | int, ...] = ()) -> list[Leaf]:
|
||||||
|
leaves: list[Leaf] = []
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for key, value in node.items():
|
||||||
|
leaves.extend(iter_leaves(value, path + (key,)))
|
||||||
|
elif isinstance(node, list):
|
||||||
|
for index, value in enumerate(node):
|
||||||
|
leaves.extend(iter_leaves(value, path + (index,)))
|
||||||
|
elif isinstance(node, str):
|
||||||
|
leaves.append(Leaf(path, node))
|
||||||
|
return leaves
|
||||||
|
|
||||||
|
|
||||||
|
def get_at_path(node: Any, path: tuple[str | int, ...]) -> Any:
|
||||||
|
current = node
|
||||||
|
try:
|
||||||
|
for part in path:
|
||||||
|
if isinstance(part, int):
|
||||||
|
if not isinstance(current, list):
|
||||||
|
return None
|
||||||
|
current = current[part]
|
||||||
|
else:
|
||||||
|
if not isinstance(current, dict):
|
||||||
|
return None
|
||||||
|
current = current[part]
|
||||||
|
except (IndexError, KeyError, TypeError):
|
||||||
|
return None
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def path_key(path: tuple[str | int, ...]) -> str:
|
||||||
|
return ".".join(str(part) for part in path)
|
||||||
|
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
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):
|
||||||
|
return True
|
||||||
|
if re.fullmatch(r"[A-Z0-9_.:/+-]{2,}", source):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def leaf_token(path: tuple[str | int, ...]) -> str:
|
||||||
|
return json.dumps(path, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def source_fingerprints(source: Any) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
leaf_token(leaf.path): hashlib.sha256(leaf.source.encode("utf-8")).hexdigest()[:20]
|
||||||
|
for leaf in iter_leaves(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def needs_translation(
|
||||||
|
source: str,
|
||||||
|
target: Any,
|
||||||
|
path: tuple[str | int, ...],
|
||||||
|
refresh: bool,
|
||||||
|
forced_tokens: set[str] | None = None,
|
||||||
|
) -> bool:
|
||||||
|
if should_copy(source, path):
|
||||||
|
return False
|
||||||
|
if refresh:
|
||||||
|
return True
|
||||||
|
if forced_tokens and leaf_token(path) in forced_tokens:
|
||||||
|
return True
|
||||||
|
return not isinstance(target, str) or not target.strip() or target == source
|
||||||
|
|
||||||
|
|
||||||
|
def protect_rich_tags(text: str) -> tuple[str, dict[str, str]]:
|
||||||
|
"""Give translatable rich-text tags opaque names during translation.
|
||||||
|
|
||||||
|
Google can remove semantic tags such as ``strong`` or ``em`` after
|
||||||
|
translating their contents. Unknown tag names are retained, so rename
|
||||||
|
non-literal tags temporarily and restore them before contract validation.
|
||||||
|
Literal ``code``, ``kbd`` and ``pre`` elements stay untouched because the
|
||||||
|
provider preserves both their markup and their contents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
names: dict[str, str] = {}
|
||||||
|
reverse: dict[str, str] = {}
|
||||||
|
|
||||||
|
def replace(match: re.Match[str]) -> str:
|
||||||
|
original = match.group(1)
|
||||||
|
if original.lower() in {"code", "kbd", "pre"}:
|
||||||
|
return match.group(0)
|
||||||
|
key = original.lower()
|
||||||
|
internal = names.get(key)
|
||||||
|
if internal is None:
|
||||||
|
internal = f"pmxrich{len(names):04d}"
|
||||||
|
names[key] = internal
|
||||||
|
reverse[internal] = original
|
||||||
|
slash = "/" if match.group(0).startswith("</") else ""
|
||||||
|
return f"<{slash}{internal}>"
|
||||||
|
|
||||||
|
return TAG_RE.sub(replace, text), reverse
|
||||||
|
|
||||||
|
|
||||||
|
def restore_rich_tags(text: str, mapping: dict[str, str]) -> str:
|
||||||
|
for internal, original in mapping.items():
|
||||||
|
text = re.sub(
|
||||||
|
rf"<(/?){re.escape(internal)}>",
|
||||||
|
lambda match: f"<{match.group(1)}{original}>",
|
||||||
|
text,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def protect_text(text: str) -> tuple[str, dict[str, str]]:
|
||||||
|
# Google already keeps both the markup and the contents of literal rich-
|
||||||
|
# text blocks. Replacing a filename or path inside one of these blocks can
|
||||||
|
# leave the sentinel as its only child, which the provider may discard.
|
||||||
|
literal_ranges = [(match.start(), match.end()) for match in LITERAL_TAG_RE.finditer(text)]
|
||||||
|
|
||||||
|
def overlaps_literal(start: int, end: int) -> bool:
|
||||||
|
return any(start < literal_end and end > literal_start for literal_start, literal_end in literal_ranges)
|
||||||
|
|
||||||
|
candidates: list[tuple[int, int]] = []
|
||||||
|
for pattern in PROTECTED_PATTERNS:
|
||||||
|
candidates.extend(
|
||||||
|
(match.start(), match.end())
|
||||||
|
for match in pattern.finditer(text)
|
||||||
|
if not overlaps_literal(match.start(), match.end())
|
||||||
|
)
|
||||||
|
candidates.extend(
|
||||||
|
(match.start(), match.end())
|
||||||
|
for match in TERM_RE.finditer(text)
|
||||||
|
if not overlaps_literal(match.start(), match.end())
|
||||||
|
)
|
||||||
|
candidates.sort(key=lambda item: (item[0], -(item[1] - item[0])))
|
||||||
|
|
||||||
|
selected: list[tuple[int, int]] = []
|
||||||
|
cursor = -1
|
||||||
|
for start, end in candidates:
|
||||||
|
if start >= cursor:
|
||||||
|
selected.append((start, end))
|
||||||
|
cursor = end
|
||||||
|
|
||||||
|
mapping: dict[str, str] = {}
|
||||||
|
chunks: list[str] = []
|
||||||
|
cursor = 0
|
||||||
|
for index, (start, end) in enumerate(selected):
|
||||||
|
# Google Translate can drop underscore-delimited sentinels when they
|
||||||
|
# sit directly beside inline markup (for example an opening <em>
|
||||||
|
# token followed by translated prose). Triple brackets remain opaque
|
||||||
|
# in that position and preserve the complete rich-text contract.
|
||||||
|
token = f"[[[PMXDOC{index:04d}]]]"
|
||||||
|
chunks.append(text[cursor:start])
|
||||||
|
chunks.append(token)
|
||||||
|
mapping[token] = text[start:end]
|
||||||
|
cursor = end
|
||||||
|
chunks.append(text[cursor:])
|
||||||
|
return "".join(chunks), mapping
|
||||||
|
|
||||||
|
|
||||||
|
def restore_text(text: str, mapping: dict[str, str]) -> str:
|
||||||
|
missing = [token for token in mapping if token not in text]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"translation provider changed protected token {missing[0]}")
|
||||||
|
for token, original in mapping.items():
|
||||||
|
text = text.replace(token, original)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def validate_contract(source: str, target: str) -> None:
|
||||||
|
if sorted(TAG_RE.findall(source)) != sorted(TAG_RE.findall(target)):
|
||||||
|
raise ValueError("rich-text tag contract changed")
|
||||||
|
if sorted(PLACEHOLDER_RE.findall(source)) != sorted(PLACEHOLDER_RE.findall(target)):
|
||||||
|
raise ValueError("placeholder contract changed")
|
||||||
|
|
||||||
|
|
||||||
|
def provider_function(args: argparse.Namespace) -> Callable[[str, str], str]:
|
||||||
|
def translate(text: str, language: str) -> str:
|
||||||
|
if args.provider == "googletrans":
|
||||||
|
raw = translate_googletrans(text, language, args.context)
|
||||||
|
elif args.provider == "google-web":
|
||||||
|
raw = translate_google_web(text, language, args.context, args.timeout)
|
||||||
|
else:
|
||||||
|
raw = translate_appimage(
|
||||||
|
text,
|
||||||
|
language,
|
||||||
|
args.context,
|
||||||
|
args.timeout,
|
||||||
|
args.appimage_path,
|
||||||
|
)
|
||||||
|
return clean_translation(raw).strip()
|
||||||
|
|
||||||
|
return translate
|
||||||
|
|
||||||
|
|
||||||
|
def translate_with_retry(
|
||||||
|
source: str,
|
||||||
|
language: str,
|
||||||
|
translate: Callable[[str, str], str],
|
||||||
|
retries: int,
|
||||||
|
delay: float,
|
||||||
|
) -> str:
|
||||||
|
rich_text, rich_mapping = protect_rich_tags(source)
|
||||||
|
protected, mapping = protect_text(rich_text)
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for attempt in range(retries + 1):
|
||||||
|
try:
|
||||||
|
translated = translate(protected, language)
|
||||||
|
if not translated:
|
||||||
|
raise ValueError("translation provider returned an empty value")
|
||||||
|
translated = restore_text(translated, mapping)
|
||||||
|
translated = restore_rich_tags(translated, rich_mapping)
|
||||||
|
validate_contract(source, translated)
|
||||||
|
return translated
|
||||||
|
except Exception as exc: # network/provider errors are retried together
|
||||||
|
last_error = exc
|
||||||
|
if attempt < retries:
|
||||||
|
time.sleep(delay * (attempt + 1))
|
||||||
|
raise RuntimeError(str(last_error)) from last_error
|
||||||
|
|
||||||
|
|
||||||
|
def merge_tree(
|
||||||
|
source: Any,
|
||||||
|
target: Any,
|
||||||
|
translations: dict[tuple[str | int, ...], str],
|
||||||
|
path: tuple[str | int, ...] = (),
|
||||||
|
) -> Any:
|
||||||
|
if isinstance(source, dict):
|
||||||
|
target_dict = target if isinstance(target, dict) else {}
|
||||||
|
return {
|
||||||
|
key: merge_tree(value, target_dict.get(key), translations, path + (key,))
|
||||||
|
for key, value in source.items()
|
||||||
|
}
|
||||||
|
if isinstance(source, list):
|
||||||
|
target_list = target if isinstance(target, list) else []
|
||||||
|
return [
|
||||||
|
merge_tree(
|
||||||
|
value,
|
||||||
|
target_list[index] if index < len(target_list) else None,
|
||||||
|
translations,
|
||||||
|
path + (index,),
|
||||||
|
)
|
||||||
|
for index, value in enumerate(source)
|
||||||
|
]
|
||||||
|
if isinstance(source, str):
|
||||||
|
if path in translations:
|
||||||
|
return translations[path]
|
||||||
|
if should_copy(source, path):
|
||||||
|
return source
|
||||||
|
if isinstance(target, str) and target.strip():
|
||||||
|
return target
|
||||||
|
return source
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
def source_files(source_root: Path, section: str) -> list[Path]:
|
||||||
|
scope = (source_root / section).resolve()
|
||||||
|
root = source_root.resolve()
|
||||||
|
if scope != root and root not in scope.parents:
|
||||||
|
raise ValueError("section must stay inside the English messages directory")
|
||||||
|
if scope.is_file():
|
||||||
|
if scope.suffix != ".json":
|
||||||
|
raise ValueError("section file must be JSON")
|
||||||
|
return [scope]
|
||||||
|
if not scope.is_dir():
|
||||||
|
raise ValueError(f"section does not exist: {scope}")
|
||||||
|
return sorted(
|
||||||
|
path
|
||||||
|
for path in scope.rglob("*.json")
|
||||||
|
if not any(part.startswith(".") for part in path.relative_to(root).parts)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_memory(source_root: Path, messages_root: Path, language: str) -> dict[str, str]:
|
||||||
|
memory: dict[str, str] = {}
|
||||||
|
conflicts: set[str] = set()
|
||||||
|
for source_path in sorted(source_root.rglob("*.json")):
|
||||||
|
if any(part.startswith(".") for part in source_path.relative_to(source_root).parts):
|
||||||
|
continue
|
||||||
|
target_path = messages_root / language / source_path.relative_to(source_root)
|
||||||
|
source = read_json(source_path)
|
||||||
|
target = read_json(target_path)
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
for leaf in iter_leaves(source):
|
||||||
|
translated = get_at_path(target, leaf.path)
|
||||||
|
if not isinstance(translated, str) or not translated.strip() or translated == leaf.source:
|
||||||
|
continue
|
||||||
|
previous = memory.get(leaf.source)
|
||||||
|
if previous is not None and previous != translated:
|
||||||
|
conflicts.add(leaf.source)
|
||||||
|
else:
|
||||||
|
memory[leaf.source] = translated
|
||||||
|
for source in conflicts:
|
||||||
|
memory.pop(source, None)
|
||||||
|
return memory
|
||||||
|
|
||||||
|
|
||||||
|
def pending_leaves(
|
||||||
|
source: Any,
|
||||||
|
target: Any,
|
||||||
|
refresh: bool,
|
||||||
|
forced_tokens: set[str] | None = None,
|
||||||
|
) -> list[Leaf]:
|
||||||
|
return [
|
||||||
|
leaf
|
||||||
|
for leaf in iter_leaves(source)
|
||||||
|
if needs_translation(
|
||||||
|
leaf.source,
|
||||||
|
get_at_path(target, leaf.path),
|
||||||
|
leaf.path,
|
||||||
|
refresh,
|
||||||
|
forced_tokens,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def schema_matches(source: Any, target: Any) -> bool:
|
||||||
|
if type(source) is not type(target):
|
||||||
|
return False
|
||||||
|
if isinstance(source, dict):
|
||||||
|
return list(source) == list(target) and all(
|
||||||
|
schema_matches(source[key], target[key]) for key in source
|
||||||
|
)
|
||||||
|
if isinstance(source, list):
|
||||||
|
return len(source) == len(target) and all(
|
||||||
|
schema_matches(left, right) for left, right in zip(source, target)
|
||||||
|
)
|
||||||
|
if isinstance(source, str):
|
||||||
|
# Localized strings intentionally differ from the English source.
|
||||||
|
return True
|
||||||
|
# Booleans, numbers and null values are structural data and must stay in
|
||||||
|
# sync with the English catalog instead of retaining an obsolete value.
|
||||||
|
return source == target
|
||||||
|
|
||||||
|
|
||||||
|
def empty_source_state() -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"version": SOURCE_STATE_VERSION,
|
||||||
|
"initialized": False,
|
||||||
|
"source": {},
|
||||||
|
"pending": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_source_state(path: Path) -> dict[str, Any]:
|
||||||
|
if not path.exists():
|
||||||
|
return empty_source_state()
|
||||||
|
try:
|
||||||
|
value = read_json(path)
|
||||||
|
except ValueError:
|
||||||
|
return empty_source_state()
|
||||||
|
if not isinstance(value, dict) or value.get("version") != SOURCE_STATE_VERSION:
|
||||||
|
return empty_source_state()
|
||||||
|
if not isinstance(value.get("source"), dict) or not isinstance(value.get("pending"), dict):
|
||||||
|
return empty_source_state()
|
||||||
|
value.setdefault("initialized", True)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def state_pending_tokens(state: dict[str, Any], language: str, relative: str) -> set[str]:
|
||||||
|
language_state = state.setdefault("pending", {}).setdefault(language, {})
|
||||||
|
values = language_state.get(relative, [])
|
||||||
|
if not isinstance(values, list):
|
||||||
|
return set()
|
||||||
|
return {str(value) for value in values}
|
||||||
|
|
||||||
|
|
||||||
|
def set_state_pending_tokens(
|
||||||
|
state: dict[str, Any], language: str, relative: str, tokens: set[str]
|
||||||
|
) -> None:
|
||||||
|
language_state = state.setdefault("pending", {}).setdefault(language, {})
|
||||||
|
if tokens:
|
||||||
|
language_state[relative] = sorted(tokens)
|
||||||
|
else:
|
||||||
|
language_state.pop(relative, None)
|
||||||
|
if not language_state:
|
||||||
|
state["pending"].pop(language, None)
|
||||||
|
|
||||||
|
|
||||||
|
def translate_file(
|
||||||
|
source: Any,
|
||||||
|
target: Any,
|
||||||
|
leaves: list[Leaf],
|
||||||
|
language: str,
|
||||||
|
memory: dict[str, str],
|
||||||
|
translate: Callable[[str, str], str],
|
||||||
|
args: argparse.Namespace,
|
||||||
|
) -> tuple[Any | None, list[str], int]:
|
||||||
|
resolved: dict[tuple[str | int, ...], str] = {}
|
||||||
|
failures: list[str] = []
|
||||||
|
jobs: dict[str, list[tuple[str | int, ...]]] = {}
|
||||||
|
|
||||||
|
for leaf in leaves:
|
||||||
|
if not args.refresh and leaf.source in memory:
|
||||||
|
resolved[leaf.path] = memory[leaf.source]
|
||||||
|
continue
|
||||||
|
jobs.setdefault(leaf.source, []).append(leaf.path)
|
||||||
|
|
||||||
|
if jobs:
|
||||||
|
with ThreadPoolExecutor(max_workers=args.workers) as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(
|
||||||
|
translate_with_retry,
|
||||||
|
text,
|
||||||
|
language,
|
||||||
|
translate,
|
||||||
|
args.retries,
|
||||||
|
args.retry_delay,
|
||||||
|
): text
|
||||||
|
for text in jobs
|
||||||
|
}
|
||||||
|
for future in as_completed(futures):
|
||||||
|
text = futures[future]
|
||||||
|
try:
|
||||||
|
translated = future.result()
|
||||||
|
memory[text] = translated
|
||||||
|
for path in jobs[text]:
|
||||||
|
resolved[path] = translated
|
||||||
|
except Exception as exc:
|
||||||
|
failures.append(f"{text[:90]}: {exc}")
|
||||||
|
if args.sleep:
|
||||||
|
time.sleep(args.sleep)
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
return None, failures, len(jobs)
|
||||||
|
return merge_tree(source, target, resolved), [], len(jobs)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--source-dir", type=Path, default=Path("web/messages/en"))
|
||||||
|
parser.add_argument("--messages-dir", type=Path, default=Path("web/messages"))
|
||||||
|
parser.add_argument("--languages", default=",".join(DEFAULT_LANGUAGES))
|
||||||
|
parser.add_argument(
|
||||||
|
"--section",
|
||||||
|
default=".",
|
||||||
|
help="Relative file or directory below the English messages directory.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--provider",
|
||||||
|
choices=("google-web", "googletrans", "appimage"),
|
||||||
|
default="googletrans",
|
||||||
|
)
|
||||||
|
parser.add_argument("--appimage-path", type=Path, default=Path("ProxMenux-Monitor.AppImage"))
|
||||||
|
parser.add_argument(
|
||||||
|
"--source-state",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Source fingerprint state used to detect changed English strings. "
|
||||||
|
"Defaults to <messages-dir>/.docs-i18n-source-state.json."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument("--context", default=DEFAULT_CONTEXT)
|
||||||
|
parser.add_argument("--timeout", type=int, default=30)
|
||||||
|
parser.add_argument("--workers", type=int, default=4)
|
||||||
|
parser.add_argument("--retries", type=int, default=3)
|
||||||
|
parser.add_argument("--retry-delay", type=float, default=2.0)
|
||||||
|
parser.add_argument("--sleep", type=float, default=0.0)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-files",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Maximum pending files per locale; zero processes all files.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--dry-run", action="store_true")
|
||||||
|
parser.add_argument("--check", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--refresh",
|
||||||
|
action="store_true",
|
||||||
|
help="Overwrite existing translations in the selected scope.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--strict", action="store_true")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = build_parser().parse_args()
|
||||||
|
if args.workers < 1 or args.retries < 0 or args.max_files < 0:
|
||||||
|
print("workers must be positive; retries and max-files cannot be negative", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
source_root = args.source_dir.resolve()
|
||||||
|
messages_root = args.messages_dir.resolve()
|
||||||
|
source_state_path = (
|
||||||
|
args.source_state.resolve()
|
||||||
|
if args.source_state is not None
|
||||||
|
else messages_root / ".docs-i18n-source-state.json"
|
||||||
|
)
|
||||||
|
languages = [item.strip() for item in args.languages.split(",") if item.strip()]
|
||||||
|
try:
|
||||||
|
files = source_files(source_root, args.section)
|
||||||
|
except ValueError as exc:
|
||||||
|
print(exc, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if not languages:
|
||||||
|
print("No target languages selected.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if args.refresh:
|
||||||
|
print("WARNING: --refresh overwrites existing translations in the selected scope.")
|
||||||
|
|
||||||
|
translate = provider_function(args)
|
||||||
|
state = load_source_state(source_state_path)
|
||||||
|
state_was_initialized = bool(state.get("initialized"))
|
||||||
|
previous_source_state = state.setdefault("source", {})
|
||||||
|
|
||||||
|
file_info: dict[
|
||||||
|
str, tuple[Path, Any, dict[str, str], set[str], set[str]]
|
||||||
|
] = {}
|
||||||
|
selected_relatives: set[str] = set()
|
||||||
|
for source_path in files:
|
||||||
|
relative = source_path.relative_to(source_root).as_posix()
|
||||||
|
selected_relatives.add(relative)
|
||||||
|
source = read_json(source_path)
|
||||||
|
current_fingerprints = source_fingerprints(source)
|
||||||
|
translatable_tokens = {
|
||||||
|
leaf_token(leaf.path)
|
||||||
|
for leaf in iter_leaves(source)
|
||||||
|
if not should_copy(leaf.source, leaf.path)
|
||||||
|
}
|
||||||
|
previous_fingerprints = previous_source_state.get(relative, {})
|
||||||
|
if not isinstance(previous_fingerprints, dict):
|
||||||
|
previous_fingerprints = {}
|
||||||
|
if state_was_initialized:
|
||||||
|
changed_tokens = {
|
||||||
|
token
|
||||||
|
for token, fingerprint in current_fingerprints.items()
|
||||||
|
if token in translatable_tokens
|
||||||
|
# Existing English leaves whose text changed must be sent to
|
||||||
|
# the provider even when the target still contains the old,
|
||||||
|
# non-empty translation. New leaves are handled by the usual
|
||||||
|
# missing-target detection, preserving a translation supplied
|
||||||
|
# manually in the same commit.
|
||||||
|
and token in previous_fingerprints
|
||||||
|
and previous_fingerprints[token] != fingerprint
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# The first run establishes the baseline without replacing
|
||||||
|
# existing human/Codex translations. Missing target values are
|
||||||
|
# still discovered separately for every locale below.
|
||||||
|
changed_tokens = set()
|
||||||
|
file_info[relative] = (
|
||||||
|
source_path,
|
||||||
|
source,
|
||||||
|
current_fingerprints,
|
||||||
|
changed_tokens,
|
||||||
|
translatable_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Materialize every locale's pending queue before advancing the shared
|
||||||
|
# English baseline. If the runner stops halfway through, unprocessed
|
||||||
|
# locales retain the exact changed leaf tokens for the next run.
|
||||||
|
for language in languages:
|
||||||
|
for relative, (
|
||||||
|
_,
|
||||||
|
source,
|
||||||
|
_,
|
||||||
|
changed_tokens,
|
||||||
|
translatable_tokens,
|
||||||
|
) in file_info.items():
|
||||||
|
target_path = messages_root / language / relative
|
||||||
|
target = read_json(target_path)
|
||||||
|
pending_tokens = (
|
||||||
|
state_pending_tokens(state, language, relative)
|
||||||
|
& translatable_tokens
|
||||||
|
)
|
||||||
|
pending_tokens.update(changed_tokens)
|
||||||
|
pending_tokens.update(
|
||||||
|
leaf_token(leaf.path)
|
||||||
|
for leaf in pending_leaves(source, target, args.refresh)
|
||||||
|
)
|
||||||
|
set_state_pending_tokens(state, language, relative, pending_tokens)
|
||||||
|
|
||||||
|
for relative, (_, _, current_fingerprints, _, _) in file_info.items():
|
||||||
|
previous_source_state[relative] = current_fingerprints
|
||||||
|
state["initialized"] = True
|
||||||
|
|
||||||
|
# A complete run also mirrors deletion of an English catalog. Partial
|
||||||
|
# --section runs deliberately leave unrelated paths untouched.
|
||||||
|
if args.section in (".", ""):
|
||||||
|
removed_files = set(previous_source_state) - selected_relatives
|
||||||
|
for relative in sorted(removed_files):
|
||||||
|
for language in languages:
|
||||||
|
target_path = messages_root / language / relative
|
||||||
|
if target_path.exists() and not (args.check or args.dry_run):
|
||||||
|
target_path.unlink()
|
||||||
|
print(f"[{language}] removed obsolete catalog {target_path}")
|
||||||
|
set_state_pending_tokens(state, language, relative, set())
|
||||||
|
previous_source_state.pop(relative, None)
|
||||||
|
|
||||||
|
if not (args.check or args.dry_run):
|
||||||
|
write_json(source_state_path, state)
|
||||||
|
|
||||||
|
total_failures = 0
|
||||||
|
total_written = 0
|
||||||
|
print(f"English files: {len(files)} | locales: {', '.join(languages)}")
|
||||||
|
|
||||||
|
for language in languages:
|
||||||
|
memory = collect_memory(source_root, messages_root, language)
|
||||||
|
# A target value paired with a newly changed English source is the old
|
||||||
|
# translation, not valid translation memory for the new sentence.
|
||||||
|
# Remove every queued source text before provider reuse; successful
|
||||||
|
# translations repopulate memory normally for later files.
|
||||||
|
queued_source_texts: set[str] = set()
|
||||||
|
for relative_key, (_, source, _, _, _) in file_info.items():
|
||||||
|
queued_tokens = state_pending_tokens(state, language, relative_key)
|
||||||
|
queued_source_texts.update(
|
||||||
|
leaf.source
|
||||||
|
for leaf in iter_leaves(source)
|
||||||
|
if leaf_token(leaf.path) in queued_tokens
|
||||||
|
)
|
||||||
|
for source_text in queued_source_texts:
|
||||||
|
memory.pop(source_text, None)
|
||||||
|
pending: list[
|
||||||
|
tuple[Path, Path, Any, Any, list[Leaf], set[str], bool]
|
||||||
|
] = []
|
||||||
|
total_strings = 0
|
||||||
|
missing_strings = 0
|
||||||
|
|
||||||
|
for source_path in files:
|
||||||
|
relative = source_path.relative_to(source_root)
|
||||||
|
relative_key = relative.as_posix()
|
||||||
|
target_path = messages_root / language / relative
|
||||||
|
source = file_info[relative_key][1]
|
||||||
|
target = read_json(target_path)
|
||||||
|
queued_tokens = state_pending_tokens(state, language, relative_key)
|
||||||
|
leaves = pending_leaves(source, target, args.refresh, queued_tokens)
|
||||||
|
total_strings += len(iter_leaves(source))
|
||||||
|
missing_strings += len(leaves)
|
||||||
|
schema_changed = not schema_matches(source, target)
|
||||||
|
if leaves or schema_changed:
|
||||||
|
pending.append(
|
||||||
|
(
|
||||||
|
source_path,
|
||||||
|
target_path,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
leaves,
|
||||||
|
queued_tokens,
|
||||||
|
schema_changed,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[{language}] {missing_strings}/{total_strings} strings pending "
|
||||||
|
f"across {len(pending)} files; reusable translations: {len(memory)}"
|
||||||
|
)
|
||||||
|
if args.check or args.dry_run:
|
||||||
|
continue
|
||||||
|
if args.max_files:
|
||||||
|
pending = pending[: args.max_files]
|
||||||
|
|
||||||
|
for index, (
|
||||||
|
source_path,
|
||||||
|
target_path,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
leaves,
|
||||||
|
queued_tokens,
|
||||||
|
schema_changed,
|
||||||
|
) in enumerate(pending, 1):
|
||||||
|
relative = source_path.relative_to(source_root)
|
||||||
|
relative_key = relative.as_posix()
|
||||||
|
suffix = " + schema sync" if schema_changed else ""
|
||||||
|
print(
|
||||||
|
f"[{language} {index}/{len(pending)}] {relative} "
|
||||||
|
f"({len(leaves)} strings{suffix})",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
built, failures, calls = translate_file(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
leaves,
|
||||||
|
language,
|
||||||
|
memory,
|
||||||
|
translate,
|
||||||
|
args,
|
||||||
|
)
|
||||||
|
if failures:
|
||||||
|
total_failures += len(failures)
|
||||||
|
set_state_pending_tokens(state, language, relative_key, queued_tokens)
|
||||||
|
write_json(source_state_path, state)
|
||||||
|
print(
|
||||||
|
f" skipped atomically after {len(failures)} failures "
|
||||||
|
f"({calls} calls)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
for failure in failures[:5]:
|
||||||
|
print(f" - {failure}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
write_json(target_path, built)
|
||||||
|
set_state_pending_tokens(state, language, relative_key, set())
|
||||||
|
write_json(source_state_path, state)
|
||||||
|
total_written += 1
|
||||||
|
print(f" wrote {target_path} ({calls} provider calls)", flush=True)
|
||||||
|
|
||||||
|
if args.check or args.dry_run:
|
||||||
|
return 0
|
||||||
|
print(f"Completed: {total_written} files written; {total_failures} failed strings.")
|
||||||
|
return 1 if args.strict and total_failures else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT = Path(__file__).parents[1] / "build_web_docs_i18n.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("build_web_docs_i18n", SCRIPT)
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
sys.modules[SPEC.name] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class WebDocsI18nTests(unittest.TestCase):
|
||||||
|
def run_generator(self, root, provider):
|
||||||
|
source_root = root / "messages" / "en"
|
||||||
|
messages_root = root / "messages"
|
||||||
|
state_path = messages_root / ".docs-i18n-source-state.json"
|
||||||
|
argv = [
|
||||||
|
str(SCRIPT),
|
||||||
|
"--source-dir",
|
||||||
|
str(source_root),
|
||||||
|
"--messages-dir",
|
||||||
|
str(messages_root),
|
||||||
|
"--languages",
|
||||||
|
"de",
|
||||||
|
"--source-state",
|
||||||
|
str(state_path),
|
||||||
|
"--workers",
|
||||||
|
"1",
|
||||||
|
]
|
||||||
|
with mock.patch.object(sys, "argv", argv), mock.patch.object(
|
||||||
|
MODULE, "provider_function", return_value=provider
|
||||||
|
):
|
||||||
|
return MODULE.main()
|
||||||
|
|
||||||
|
def test_protected_contract_round_trip(self):
|
||||||
|
source = (
|
||||||
|
"Run <code>systemctl restart pveproxy</code> in Proxmox VE, "
|
||||||
|
"then open <link>Settings</link> at {host}."
|
||||||
|
)
|
||||||
|
protected, mapping = MODULE.protect_text(source)
|
||||||
|
self.assertIn("<code>systemctl restart pveproxy</code>", protected)
|
||||||
|
self.assertNotIn("Proxmox VE", protected)
|
||||||
|
self.assertIn("<code>", protected)
|
||||||
|
self.assertIn("</code>", protected)
|
||||||
|
self.assertIn("<link>", protected)
|
||||||
|
self.assertIn("</link>", protected)
|
||||||
|
self.assertNotIn("<code>", mapping.values())
|
||||||
|
self.assertNotIn("</code>", mapping.values())
|
||||||
|
self.assertNotIn("systemctl", mapping.values())
|
||||||
|
self.assertEqual(MODULE.restore_text(protected, mapping), source)
|
||||||
|
|
||||||
|
def test_human_translation_is_not_pending(self):
|
||||||
|
source = {"title": "Updates", "url": "https://example.com"}
|
||||||
|
target = {"title": "Actualizaciones", "url": "https://example.com"}
|
||||||
|
self.assertEqual(MODULE.pending_leaves(source, target, refresh=False), [])
|
||||||
|
|
||||||
|
def test_changed_source_text_is_pending_even_with_existing_translation(self):
|
||||||
|
source = {"title": "Updated installation guidance"}
|
||||||
|
target = {"title": "Vorherige Installationsanleitung"}
|
||||||
|
token = MODULE.leaf_token(("title",))
|
||||||
|
leaves = MODULE.pending_leaves(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
refresh=False,
|
||||||
|
forced_tokens={token},
|
||||||
|
)
|
||||||
|
self.assertEqual([leaf.path for leaf in leaves], [("title",)])
|
||||||
|
|
||||||
|
def test_source_fingerprints_change_per_leaf(self):
|
||||||
|
before = MODULE.source_fingerprints({"title": "One", "body": "Same"})
|
||||||
|
after = MODULE.source_fingerprints({"title": "Two", "body": "Same"})
|
||||||
|
self.assertNotEqual(
|
||||||
|
before[MODULE.leaf_token(("title",))],
|
||||||
|
after[MODULE.leaf_token(("title",))],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
before[MODULE.leaf_token(("body",))],
|
||||||
|
after[MODULE.leaf_token(("body",))],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_schema_mismatch_detects_removed_keys(self):
|
||||||
|
self.assertFalse(
|
||||||
|
MODULE.schema_matches(
|
||||||
|
{"title": "Title"},
|
||||||
|
{"title": "Título", "removed": "Old"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_schema_mismatch_detects_changed_structural_values(self):
|
||||||
|
self.assertFalse(
|
||||||
|
MODULE.schema_matches(
|
||||||
|
{"enabled": True, "retries": 2},
|
||||||
|
{"enabled": False, "retries": 2},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_schema_allows_localized_string_values(self):
|
||||||
|
self.assertTrue(
|
||||||
|
MODULE.schema_matches(
|
||||||
|
{"title": "Updates"},
|
||||||
|
{"title": "Aktualisierungen"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_translatable_rich_tags_use_opaque_names(self):
|
||||||
|
source = (
|
||||||
|
"Keep <strong>this</strong>, <em>that</em> and "
|
||||||
|
"<code>systemctl restart pveproxy</code>."
|
||||||
|
)
|
||||||
|
protected, mapping = MODULE.protect_rich_tags(source)
|
||||||
|
self.assertIn("<pmxrich0000>this</pmxrich0000>", protected)
|
||||||
|
self.assertIn("<pmxrich0001>that</pmxrich0001>", protected)
|
||||||
|
self.assertIn("<code>systemctl restart pveproxy</code>", protected)
|
||||||
|
self.assertEqual(MODULE.restore_rich_tags(protected, mapping), source)
|
||||||
|
|
||||||
|
def test_source_schema_drives_output(self):
|
||||||
|
source = {"title": "Title", "items": ["One", "Two"]}
|
||||||
|
target = {
|
||||||
|
"title": "Título",
|
||||||
|
"items": ["Uno", "Dos", "Obsoleto"],
|
||||||
|
"removed": "Old",
|
||||||
|
}
|
||||||
|
translated = {
|
||||||
|
("items", 0): "Uno",
|
||||||
|
("items", 1): "Dos",
|
||||||
|
}
|
||||||
|
self.assertEqual(
|
||||||
|
MODULE.merge_tree(source, target, translated),
|
||||||
|
{"title": "Título", "items": ["Uno", "Dos"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_translation_memory_ignores_conflicts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source_root = root / "messages" / "en"
|
||||||
|
target_root = root / "messages" / "es"
|
||||||
|
source_root.mkdir(parents=True)
|
||||||
|
target_root.mkdir(parents=True)
|
||||||
|
(source_root / "one.json").write_text(
|
||||||
|
json.dumps({"label": "Settings"}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(target_root / "one.json").write_text(
|
||||||
|
json.dumps({"label": "Ajustes"}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(source_root / "two.json").write_text(
|
||||||
|
json.dumps({"label": "Settings"}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(target_root / "two.json").write_text(
|
||||||
|
json.dumps({"label": "Configuración"}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
memory = MODULE.collect_memory(source_root, root / "messages", "es")
|
||||||
|
self.assertNotIn("Settings", memory)
|
||||||
|
|
||||||
|
def test_source_state_retranslates_an_existing_changed_leaf(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source_path = root / "messages" / "en" / "page.json"
|
||||||
|
target_path = root / "messages" / "de" / "page.json"
|
||||||
|
source_path.parent.mkdir(parents=True)
|
||||||
|
target_path.parent.mkdir(parents=True)
|
||||||
|
source_path.write_text(json.dumps({"title": "Original"}), encoding="utf-8")
|
||||||
|
target_path.write_text(json.dumps({"title": "Ursprünglich"}), encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.run_generator(root, lambda *_: self.fail("provider called during baseline")),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(read_json_file(target_path), {"title": "Ursprünglich"})
|
||||||
|
|
||||||
|
source_path.write_text(json.dumps({"title": "Changed"}), encoding="utf-8")
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def provider(text, language):
|
||||||
|
calls.append((text, language))
|
||||||
|
return "Geändert"
|
||||||
|
|
||||||
|
self.assertEqual(self.run_generator(root, provider), 0)
|
||||||
|
self.assertEqual(calls, [("Changed", "de")])
|
||||||
|
self.assertEqual(read_json_file(target_path), {"title": "Geändert"})
|
||||||
|
|
||||||
|
def test_new_leaf_preserves_translation_supplied_with_source_change(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source_path = root / "messages" / "en" / "page.json"
|
||||||
|
target_path = root / "messages" / "de" / "page.json"
|
||||||
|
source_path.parent.mkdir(parents=True)
|
||||||
|
target_path.parent.mkdir(parents=True)
|
||||||
|
source_path.write_text(json.dumps({"title": "Original"}), encoding="utf-8")
|
||||||
|
target_path.write_text(json.dumps({"title": "Ursprünglich"}), encoding="utf-8")
|
||||||
|
self.assertEqual(self.run_generator(root, lambda *_: "unused"), 0)
|
||||||
|
|
||||||
|
source_path.write_text(
|
||||||
|
json.dumps({"title": "Original", "new": "New text"}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
target_path.write_text(
|
||||||
|
json.dumps({"title": "Ursprünglich", "new": "Neuer Text"}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.run_generator(root, lambda *_: self.fail("manual translation overwritten")),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
read_json_file(target_path),
|
||||||
|
{"title": "Ursprünglich", "new": "Neuer Text"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_json_file(path):
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
name: Build web documentation translations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
paths:
|
||||||
|
- 'web/messages/en/*.json'
|
||||||
|
- 'web/messages/en/**/*.json'
|
||||||
|
- '.github/scripts/build_web_docs_i18n.py'
|
||||||
|
- '.github/scripts/build_translation_cache.py'
|
||||||
|
- '.github/workflows/build-web-docs-i18n.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
languages:
|
||||||
|
description: 'Comma-separated locales'
|
||||||
|
default: 'es,de,fr,it,pt,sk,sv'
|
||||||
|
section:
|
||||||
|
description: 'File or directory below web/messages/en'
|
||||||
|
default: '.'
|
||||||
|
max_files:
|
||||||
|
description: 'Maximum pending files per locale; 0 means all'
|
||||||
|
default: '0'
|
||||||
|
refresh:
|
||||||
|
description: 'Overwrite existing translations in the selected scope'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
dry_run:
|
||||||
|
description: 'Report pending coverage without writing files'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: build-web-docs-i18n-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
translate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
timeout-minutes: 120
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout develop
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: develop
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install translation provider
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install 'googletrans==4.0.0-rc1' 'httpx==0.13.3' 'httpcore==0.9.1' 'h11==0.9.0'
|
||||||
|
|
||||||
|
- name: Build missing translations
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
LANGUAGES="${{ github.event.inputs.languages }}"
|
||||||
|
LANGUAGES="${LANGUAGES:-es,de,fr,it,pt,sk,sv}"
|
||||||
|
SECTION="${{ github.event.inputs.section }}"
|
||||||
|
SECTION="${SECTION:-.}"
|
||||||
|
MAX_FILES="${{ github.event.inputs.max_files }}"
|
||||||
|
MAX_FILES="${MAX_FILES:-0}"
|
||||||
|
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
if [[ "${{ github.event.inputs.refresh }}" == "true" ]]; then
|
||||||
|
EXTRA_ARGS+=(--refresh)
|
||||||
|
fi
|
||||||
|
if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then
|
||||||
|
EXTRA_ARGS+=(--dry-run)
|
||||||
|
fi
|
||||||
|
|
||||||
|
python .github/scripts/build_web_docs_i18n.py \
|
||||||
|
--source-dir web/messages/en \
|
||||||
|
--messages-dir web/messages \
|
||||||
|
--languages "$LANGUAGES" \
|
||||||
|
--section "$SECTION" \
|
||||||
|
--max-files "$MAX_FILES" \
|
||||||
|
--provider googletrans \
|
||||||
|
--workers 4 \
|
||||||
|
"${EXTRA_ARGS[@]}"
|
||||||
|
|
||||||
|
- name: Validate catalogs
|
||||||
|
run: |
|
||||||
|
python .github/scripts/build_web_docs_i18n.py \
|
||||||
|
--source-dir web/messages/en \
|
||||||
|
--messages-dir web/messages \
|
||||||
|
--languages "${{ github.event.inputs.languages || 'es,de,fr,it,pt,sk,sv' }}" \
|
||||||
|
--check
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if git diff --quiet -- web/messages/; then
|
||||||
|
echo "No documentation translations changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.name "ProxMenuxBot"
|
||||||
|
git config user.email "bot@proxmenux.local"
|
||||||
|
git add web/messages/
|
||||||
|
git commit -m "docs(i18n): update documentation translations"
|
||||||
|
|
||||||
|
for attempt in 1 2 3 4 5; do
|
||||||
|
git fetch origin develop
|
||||||
|
if git rebase origin/develop && git push origin develop; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git rebase --abort 2>/dev/null || true
|
||||||
|
sleep $((attempt * 3))
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
@@ -2402,12 +2402,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
|||||||
detections, so the user gets one-click Restore before hand-
|
detections, so the user gets one-click Restore before hand-
|
||||||
typing a custom app. */}
|
typing a custom app. */}
|
||||||
{apps.length > 0 && (
|
{apps.length > 0 && (
|
||||||
<div className="flex flex-wrap justify-end items-center gap-2">
|
<div className="flex flex-col items-stretch gap-2 max-w-xs mx-auto sm:flex-row sm:flex-wrap sm:justify-end sm:items-center sm:max-w-none sm:mx-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={searchInstalledApplications}
|
onClick={searchInstalledApplications}
|
||||||
disabled={searchingApplications || editMode}
|
disabled={searchingApplications || editMode}
|
||||||
|
className="w-full sm:w-auto order-2 sm:order-1"
|
||||||
>
|
>
|
||||||
{searchingApplications
|
{searchingApplications
|
||||||
? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||||
@@ -2421,6 +2422,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={openBrowseOrEditor}
|
onClick={openBrowseOrEditor}
|
||||||
disabled={editMode}
|
disabled={editMode}
|
||||||
|
className="w-full sm:w-auto order-3 sm:order-2"
|
||||||
>
|
>
|
||||||
<PlusCircle className="h-4 w-4 mr-1.5" />
|
<PlusCircle className="h-4 w-4 mr-1.5" />
|
||||||
{t("vmLxc.appEditor.addAnotherApplication")}
|
{t("vmLxc.appEditor.addAnotherApplication")}
|
||||||
@@ -2433,7 +2435,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditMode((v) => !v)}
|
onClick={() => setEditMode((v) => !v)}
|
||||||
className="h-9 px-3 text-sm rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
|
className="h-9 px-3 text-sm rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center justify-center gap-1.5 w-full sm:w-auto order-1 sm:order-3"
|
||||||
>
|
>
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ interface ChannelConfig {
|
|||||||
url?: string
|
url?: string
|
||||||
token?: string
|
token?: string
|
||||||
webhook_url?: string
|
webhook_url?: string
|
||||||
|
// Pushover channel fields
|
||||||
|
user_key?: string
|
||||||
|
api_token?: string
|
||||||
|
device?: string
|
||||||
|
sound?: string
|
||||||
|
critical_priority?: boolean
|
||||||
// Email channel fields
|
// Email channel fields
|
||||||
host?: string
|
host?: string
|
||||||
port?: string
|
port?: string
|
||||||
@@ -146,7 +152,7 @@ function validateGotifyUrl(url: string): { error?: string; warning?: string } {
|
|||||||
|
|
||||||
const EVENT_CATEGORIES = ["vm_ct", "backup", "resources", "storage", "network", "security", "cluster", "services", "health", "updates", "other"].map(key => ({ key }))
|
const EVENT_CATEGORIES = ["vm_ct", "backup", "resources", "storage", "network", "security", "cluster", "services", "health", "updates", "other"].map(key => ({ key }))
|
||||||
|
|
||||||
const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email", "apprise"] as const
|
const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email", "pushover", "apprise"] as const
|
||||||
|
|
||||||
const AI_PROVIDERS = [
|
const AI_PROVIDERS = [
|
||||||
{
|
{
|
||||||
@@ -242,6 +248,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
gotify: { enabled: false },
|
gotify: { enabled: false },
|
||||||
discord: { enabled: false },
|
discord: { enabled: false },
|
||||||
email: { enabled: false },
|
email: { enabled: false },
|
||||||
|
pushover: { enabled: false, critical_priority: true },
|
||||||
apprise: { enabled: false },
|
apprise: { enabled: false },
|
||||||
},
|
},
|
||||||
event_categories: {
|
event_categories: {
|
||||||
@@ -256,6 +263,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
gotify: { categories: {}, events: {} },
|
gotify: { categories: {}, events: {} },
|
||||||
discord: { categories: {}, events: {} },
|
discord: { categories: {}, events: {} },
|
||||||
email: { categories: {}, events: {} },
|
email: { categories: {}, events: {} },
|
||||||
|
pushover: { categories: {}, events: {} },
|
||||||
apprise: { categories: {}, events: {} },
|
apprise: { categories: {}, events: {} },
|
||||||
},
|
},
|
||||||
ai_enabled: false,
|
ai_enabled: false,
|
||||||
@@ -287,6 +295,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
gotify: "brief",
|
gotify: "brief",
|
||||||
discord: "brief",
|
discord: "brief",
|
||||||
email: "detailed",
|
email: "detailed",
|
||||||
|
pushover: "brief",
|
||||||
apprise: "brief",
|
apprise: "brief",
|
||||||
},
|
},
|
||||||
hostname: "",
|
hostname: "",
|
||||||
@@ -1070,6 +1079,8 @@ export function NotificationSettings() {
|
|||||||
gt_token: { channel: "gotify", field: "token" },
|
gt_token: { channel: "gotify", field: "token" },
|
||||||
dc_hook: { channel: "discord", field: "webhook_url" },
|
dc_hook: { channel: "discord", field: "webhook_url" },
|
||||||
em_pass: { channel: "email", field: "password" },
|
em_pass: { channel: "email", field: "password" },
|
||||||
|
po_user: { channel: "pushover", field: "user_key" },
|
||||||
|
po_token: { channel: "pushover", field: "api_token" },
|
||||||
apprise_url: { channel: "apprise", field: "url" },
|
apprise_url: { channel: "apprise", field: "url" },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1429,7 +1440,7 @@ export function NotificationSettings() {
|
|||||||
|
|
||||||
<div className="rounded-lg border border-border/50 bg-muted/20 p-3">
|
<div className="rounded-lg border border-border/50 bg-muted/20 p-3">
|
||||||
<Tabs defaultValue="telegram" className="w-full">
|
<Tabs defaultValue="telegram" className="w-full">
|
||||||
<TabsList className="w-full grid grid-cols-5 h-8">
|
<TabsList className="w-full grid grid-cols-3 sm:grid-cols-6 h-auto">
|
||||||
<TabsTrigger value="telegram" className="text-xs data-[state=active]:text-blue-500">
|
<TabsTrigger value="telegram" className="text-xs data-[state=active]:text-blue-500">
|
||||||
Telegram
|
Telegram
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -1442,6 +1453,9 @@ export function NotificationSettings() {
|
|||||||
<TabsTrigger value="email" className="text-xs data-[state=active]:text-amber-500">
|
<TabsTrigger value="email" className="text-xs data-[state=active]:text-amber-500">
|
||||||
Email
|
Email
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="pushover" className="text-xs data-[state=active]:text-rose-500">
|
||||||
|
Pushover
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="apprise" className="text-xs data-[state=active]:text-cyan-500">
|
<TabsTrigger value="apprise" className="text-xs data-[state=active]:text-cyan-500">
|
||||||
Apprise
|
Apprise
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -1876,10 +1890,145 @@ export function NotificationSettings() {
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Pushover */}
|
||||||
|
<TabsContent value="pushover" className="space-y-3 pt-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="text-xs font-medium">{t("settings.notifications.ui.enablePushover")}</Label>
|
||||||
|
<a
|
||||||
|
href="https://pushover.net/apps/build"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-[10px] text-rose-500 hover:text-rose-400 hover:underline"
|
||||||
|
>
|
||||||
|
{t("settings.notifications.ui.setupGuide")}
|
||||||
|
<ExternalLink className="h-2.5 w-2.5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||||
|
config.channels.pushover?.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||||
|
} ${!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||||
|
onClick={() => { if (editMode) updateChannel("pushover", "enabled", !config.channels.pushover?.enabled) }}
|
||||||
|
disabled={!editMode}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={config.channels.pushover?.enabled || false}
|
||||||
|
>
|
||||||
|
<span className={`absolute top-[1px] left-[1px] h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||||
|
config.channels.pushover?.enabled ? "translate-x-[18px]" : "translate-x-0"
|
||||||
|
}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{config.channels.pushover?.enabled && (
|
||||||
|
<>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||||
|
{t("settings.notifications.ui.pushoverCredentialsHint")}
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1.5 min-w-0">
|
||||||
|
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverUserKey")}</Label>
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
|
<Input
|
||||||
|
type={showSecrets["po_user"] ? "text" : "password"}
|
||||||
|
className={`h-7 text-xs font-mono min-w-0 flex-1 ${!editMode ? "opacity-50" : ""}`}
|
||||||
|
placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
|
||||||
|
value={secretValue("po_user", config.channels.pushover?.user_key || "")}
|
||||||
|
onChange={e => updateChannel("pushover", "user_key", e.target.value)}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-7 w-7 shrink-0 flex items-center justify-center rounded-md border border-border hover:bg-muted text-muted-foreground"
|
||||||
|
onClick={() => toggleSecret("po_user")}
|
||||||
|
>
|
||||||
|
{showSecrets["po_user"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 min-w-0">
|
||||||
|
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverApiToken")}</Label>
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
|
<Input
|
||||||
|
type={showSecrets["po_token"] ? "text" : "password"}
|
||||||
|
className={`h-7 text-xs font-mono min-w-0 flex-1 ${!editMode ? "opacity-50" : ""}`}
|
||||||
|
placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi"
|
||||||
|
value={secretValue("po_token", config.channels.pushover?.api_token || "")}
|
||||||
|
onChange={e => updateChannel("pushover", "api_token", e.target.value)}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-7 w-7 shrink-0 flex items-center justify-center rounded-md border border-border hover:bg-muted text-muted-foreground"
|
||||||
|
onClick={() => toggleSecret("po_token")}
|
||||||
|
>
|
||||||
|
{showSecrets["po_token"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverDevice")} ({t("settings.notifications.ui.optional")})</Label>
|
||||||
|
<Input
|
||||||
|
className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`}
|
||||||
|
placeholder="iphone"
|
||||||
|
value={config.channels.pushover?.device || ""}
|
||||||
|
onChange={e => updateChannel("pushover", "device", e.target.value)}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverSound")} ({t("settings.notifications.ui.optional")})</Label>
|
||||||
|
<Input
|
||||||
|
className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`}
|
||||||
|
placeholder="pushover"
|
||||||
|
value={config.channels.pushover?.sound || ""}
|
||||||
|
onChange={e => updateChannel("pushover", "sound", e.target.value)}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-3 py-1">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">{t("settings.notifications.ui.pushoverCriticalPriority")}</Label>
|
||||||
|
<p className="text-[10px] text-muted-foreground">{t("settings.notifications.ui.pushoverCriticalPriorityDescription")}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={config.channels.pushover?.critical_priority !== false}
|
||||||
|
disabled={!editMode}
|
||||||
|
className={`relative w-9 h-[18px] shrink-0 rounded-full transition-colors ${
|
||||||
|
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||||
|
} ${config.channels.pushover?.critical_priority !== false ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"}`}
|
||||||
|
onClick={() => { if (editMode) updateChannel("pushover", "critical_priority", config.channels.pushover?.critical_priority === false) }}
|
||||||
|
>
|
||||||
|
<span className={`absolute top-[1px] left-[1px] h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||||
|
config.channels.pushover?.critical_priority !== false ? "translate-x-[18px]" : "translate-x-0"
|
||||||
|
}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{renderChannelCategories("pushover")}
|
||||||
|
{renderQuietHours("pushover")}
|
||||||
|
{renderDailyDigest("pushover")}
|
||||||
|
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||||
|
<button
|
||||||
|
className="h-7 px-3 text-xs rounded-md bg-rose-600 hover:bg-rose-700 text-white transition-colors flex items-center gap-1.5 disabled:opacity-50"
|
||||||
|
onClick={() => handleTest("pushover")}
|
||||||
|
disabled={testing === "pushover" || !config.channels.pushover?.user_key || !config.channels.pushover?.api_token}
|
||||||
|
>
|
||||||
|
{testing === "pushover" ? <Loader2 className="h-3 w-3 animate-spin" /> : <TestTube2 className="h-3 w-3" />}
|
||||||
|
{t("settings.notifications.ui.sendTest")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/* Apprise — issue #207. Single URL talks to ~80
|
{/* Apprise — issue #207. Single URL talks to ~80
|
||||||
notification services. The operator pastes one
|
notification services. The operator pastes one
|
||||||
`tgram://`, `discord://`, `ntfy://`, `matrix://`,
|
`tgram://`, `discord://`, `ntfy://`, `matrix://`,
|
||||||
`pushover://` etc. URL and the AppriseChannel
|
`pover://` etc. URL and the AppriseChannel
|
||||||
backend handles the transport. Mirrors the same
|
backend handles the transport. Mirrors the same
|
||||||
Enable toggle + Test button pattern as the other
|
Enable toggle + Test button pattern as the other
|
||||||
channels. */}
|
channels. */}
|
||||||
@@ -1888,7 +2037,7 @@ export function NotificationSettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="text-xs font-medium">{t("settings.notifications.ui.enableApprise")}</Label>
|
<Label className="text-xs font-medium">{t("settings.notifications.ui.enableApprise")}</Label>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/caronc/apprise/wiki"
|
href="https://appriseit.com/services/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[10px] text-cyan-500 hover:text-cyan-400 hover:underline"
|
className="text-[10px] text-cyan-500 hover:text-cyan-400 hover:underline"
|
||||||
@@ -1946,11 +2095,11 @@ export function NotificationSettings() {
|
|||||||
<code className="text-foreground/80 mx-0.5">slack://</code>,
|
<code className="text-foreground/80 mx-0.5">slack://</code>,
|
||||||
<code className="text-foreground/80 mx-0.5">ntfy://</code>,
|
<code className="text-foreground/80 mx-0.5">ntfy://</code>,
|
||||||
<code className="text-foreground/80 mx-0.5">matrix://</code>,
|
<code className="text-foreground/80 mx-0.5">matrix://</code>,
|
||||||
<code className="text-foreground/80 mx-0.5">pushover://</code>,
|
<code className="text-foreground/80 mx-0.5">pover://</code>,
|
||||||
<code className="text-foreground/80 mx-0.5">mailto://</code>… {t("settings.notifications.ui.seeThe")}
|
<code className="text-foreground/80 mx-0.5">mailto://</code>… {t("settings.notifications.ui.seeThe")}
|
||||||
{" "}
|
{" "}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/caronc/apprise/wiki"
|
href="https://appriseit.com/services/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-cyan-500 hover:underline"
|
className="text-cyan-500 hover:underline"
|
||||||
|
|||||||
@@ -5764,14 +5764,22 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
{t("vmLxc.bulkUpdate.description")}
|
{t("vmLxc.bulkUpdate.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!bulkEditMode && bulkConfigured && (
|
{!bulkEditMode && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setBulkEditMode(true)}
|
onClick={() => {
|
||||||
|
if (!bulkConfigured) {
|
||||||
|
setBulkTargets(["os"])
|
||||||
|
setBulkError(null)
|
||||||
|
}
|
||||||
|
setBulkEditMode(true)
|
||||||
|
}}
|
||||||
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5 flex-shrink-0"
|
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5 flex-shrink-0"
|
||||||
>
|
>
|
||||||
<Settings2 className="h-3.5 w-3.5" />
|
<Settings2 className="h-3.5 w-3.5" />
|
||||||
{t("vmLxc.bulkUpdate.edit")}
|
{bulkConfigured
|
||||||
|
? t("vmLxc.bulkUpdate.edit")
|
||||||
|
: t("vmLxc.bulkUpdate.configure")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -5932,22 +5940,8 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
<div className="text-sm text-muted-foreground">
|
||||||
<div className="text-sm text-muted-foreground">
|
{t("vmLxc.bulkUpdate.notConfigured")}
|
||||||
{t("vmLxc.bulkUpdate.notConfigured")}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setBulkTargets(["os"])
|
|
||||||
setBulkEditMode(true)
|
|
||||||
setBulkError(null)
|
|
||||||
}}
|
|
||||||
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center justify-center gap-1.5 flex-shrink-0"
|
|
||||||
>
|
|
||||||
<Settings2 className="h-3.5 w-3.5" />
|
|
||||||
{t("vmLxc.bulkUpdate.configure")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1839,6 +1839,14 @@
|
|||||||
"toAddresses": "An Adressen (durch Kommas getrennt)",
|
"toAddresses": "An Adressen (durch Kommas getrennt)",
|
||||||
"subjectPrefix": "Betreff-Präfix",
|
"subjectPrefix": "Betreff-Präfix",
|
||||||
"emailHint": "Lassen Sie den SMTP-Host leer, um lokales Sendmail zu verwenden. Verwenden Sie für Gmail ein App-Passwort.",
|
"emailHint": "Lassen Sie den SMTP-Host leer, um lokales Sendmail zu verwenden. Verwenden Sie für Gmail ein App-Passwort.",
|
||||||
|
"enablePushover": "Pushover aktivieren",
|
||||||
|
"pushoverUserKey": "Benutzer- oder Gruppenschlüssel",
|
||||||
|
"pushoverApiToken": "API-Token der Anwendung",
|
||||||
|
"pushoverDevice": "Gerät",
|
||||||
|
"pushoverSound": "Ton",
|
||||||
|
"pushoverCriticalPriority": "Hohe Priorität für kritische Warnungen",
|
||||||
|
"pushoverCriticalPriorityDescription": "KRITISCHE Benachrichtigungen verwenden die hohe Pushover-Priorität. Die Notfallpriorität wird nicht verwendet.",
|
||||||
|
"pushoverCredentialsHint": "Erstellen Sie eine Anwendung in Pushover und kopieren Sie deren API-Token sowie Ihren Benutzer- oder Gruppenschlüssel.",
|
||||||
"enableApprise": "Aktivieren Sie Apprise",
|
"enableApprise": "Aktivieren Sie Apprise",
|
||||||
"urlFormats": "+URL-Formate",
|
"urlFormats": "+URL-Formate",
|
||||||
"appriseUrl": "Apprise-URL",
|
"appriseUrl": "Apprise-URL",
|
||||||
|
|||||||
@@ -1838,6 +1838,14 @@
|
|||||||
"toAddresses": "To addresses (comma-separated)",
|
"toAddresses": "To addresses (comma-separated)",
|
||||||
"subjectPrefix": "Subject prefix",
|
"subjectPrefix": "Subject prefix",
|
||||||
"emailHint": "Leave SMTP host empty to use local sendmail. For Gmail, use an app password.",
|
"emailHint": "Leave SMTP host empty to use local sendmail. For Gmail, use an app password.",
|
||||||
|
"enablePushover": "Enable Pushover",
|
||||||
|
"pushoverUserKey": "User or group key",
|
||||||
|
"pushoverApiToken": "Application API token",
|
||||||
|
"pushoverDevice": "Device",
|
||||||
|
"pushoverSound": "Sound",
|
||||||
|
"pushoverCriticalPriority": "High priority for critical alerts",
|
||||||
|
"pushoverCriticalPriorityDescription": "CRITICAL notifications use Pushover high priority. Emergency priority is not used.",
|
||||||
|
"pushoverCredentialsHint": "Create an application in Pushover and copy its API token and your user or group key.",
|
||||||
"enableApprise": "Enable Apprise",
|
"enableApprise": "Enable Apprise",
|
||||||
"urlFormats": "+URL formats",
|
"urlFormats": "+URL formats",
|
||||||
"appriseUrl": "Apprise URL",
|
"appriseUrl": "Apprise URL",
|
||||||
|
|||||||
@@ -1825,7 +1825,7 @@
|
|||||||
"enableGotify": "Habilitar Gotify",
|
"enableGotify": "Habilitar Gotify",
|
||||||
"serverUrl": "URL del servidor",
|
"serverUrl": "URL del servidor",
|
||||||
"appToken": "Ficha de aplicación",
|
"appToken": "Ficha de aplicación",
|
||||||
"enableDiscord": "Habilitar discordia",
|
"enableDiscord": "Activar Discord",
|
||||||
"webhookUrl": "URL de webhook",
|
"webhookUrl": "URL de webhook",
|
||||||
"enableEmail": "Habilitar correo electrónico",
|
"enableEmail": "Habilitar correo electrónico",
|
||||||
"smtpHost": "servidor SMTP",
|
"smtpHost": "servidor SMTP",
|
||||||
@@ -1839,9 +1839,17 @@
|
|||||||
"toAddresses": "A direcciones (separadas por comas)",
|
"toAddresses": "A direcciones (separadas por comas)",
|
||||||
"subjectPrefix": "Prefijo de asunto",
|
"subjectPrefix": "Prefijo de asunto",
|
||||||
"emailHint": "Deje el host SMTP vacío para usar sendmail local. Para Gmail, utilice una contraseña de aplicación.",
|
"emailHint": "Deje el host SMTP vacío para usar sendmail local. Para Gmail, utilice una contraseña de aplicación.",
|
||||||
"enableApprise": "Habilitar información",
|
"enablePushover": "Activar Pushover",
|
||||||
|
"pushoverUserKey": "Clave de usuario o grupo",
|
||||||
|
"pushoverApiToken": "Token API de la aplicación",
|
||||||
|
"pushoverDevice": "Dispositivo",
|
||||||
|
"pushoverSound": "Sonido",
|
||||||
|
"pushoverCriticalPriority": "Prioridad alta para alertas críticas",
|
||||||
|
"pushoverCriticalPriorityDescription": "Las notificaciones CRÍTICAS usan la prioridad alta de Pushover. No se utiliza la prioridad de emergencia.",
|
||||||
|
"pushoverCredentialsHint": "Cree una aplicación en Pushover y copie su token API y la clave de usuario o grupo.",
|
||||||
|
"enableApprise": "Activar Apprise",
|
||||||
"urlFormats": "+formatos de URL",
|
"urlFormats": "+formatos de URL",
|
||||||
"appriseUrl": "Informar URL",
|
"appriseUrl": "URL de Apprise",
|
||||||
"showUrl": "Mostrar URL",
|
"showUrl": "Mostrar URL",
|
||||||
"hideUrl": "Ocultar URL",
|
"hideUrl": "Ocultar URL",
|
||||||
"appriseDescription": "Una URL permite a Apprise enrutar la notificación al servicio correcto. Ejemplos:",
|
"appriseDescription": "Una URL permite a Apprise enrutar la notificación al servicio correcto. Ejemplos:",
|
||||||
|
|||||||
@@ -1839,9 +1839,17 @@
|
|||||||
"toAddresses": "Aux adresses (séparées par des virgules)",
|
"toAddresses": "Aux adresses (séparées par des virgules)",
|
||||||
"subjectPrefix": "Préfixe du sujet",
|
"subjectPrefix": "Préfixe du sujet",
|
||||||
"emailHint": "Laissez l'hôte SMTP vide pour utiliser sendmail local. Pour Gmail, utilisez un mot de passe d'application.",
|
"emailHint": "Laissez l'hôte SMTP vide pour utiliser sendmail local. Pour Gmail, utilisez un mot de passe d'application.",
|
||||||
|
"enablePushover": "Activer Pushover",
|
||||||
|
"pushoverUserKey": "Clé utilisateur ou groupe",
|
||||||
|
"pushoverApiToken": "Jeton API de l’application",
|
||||||
|
"pushoverDevice": "Appareil",
|
||||||
|
"pushoverSound": "Son",
|
||||||
|
"pushoverCriticalPriority": "Priorité élevée pour les alertes critiques",
|
||||||
|
"pushoverCriticalPriorityDescription": "Les notifications CRITIQUES utilisent la priorité élevée de Pushover. La priorité d’urgence n’est pas utilisée.",
|
||||||
|
"pushoverCredentialsHint": "Créez une application dans Pushover, puis copiez son jeton API et votre clé utilisateur ou groupe.",
|
||||||
"enableApprise": "Activer Apprise",
|
"enableApprise": "Activer Apprise",
|
||||||
"urlFormats": "+Formats d'URL",
|
"urlFormats": "+Formats d'URL",
|
||||||
"appriseUrl": "URL d'information",
|
"appriseUrl": "URL Apprise",
|
||||||
"showUrl": "Afficher l'URL",
|
"showUrl": "Afficher l'URL",
|
||||||
"hideUrl": "Masquer l'URL",
|
"hideUrl": "Masquer l'URL",
|
||||||
"appriseDescription": "Une URL permet à Apprise d'acheminer la notification vers le bon service. Exemples :",
|
"appriseDescription": "Une URL permet à Apprise d'acheminer la notification vers le bon service. Exemples :",
|
||||||
|
|||||||
@@ -1839,9 +1839,17 @@
|
|||||||
"toAddresses": "Agli indirizzi (separati da virgole)",
|
"toAddresses": "Agli indirizzi (separati da virgole)",
|
||||||
"subjectPrefix": "Prefisso oggetto",
|
"subjectPrefix": "Prefisso oggetto",
|
||||||
"emailHint": "Lascia vuoto l'host SMTP per utilizzare sendmail locale. Per Gmail, utilizza una password per l'app.",
|
"emailHint": "Lascia vuoto l'host SMTP per utilizzare sendmail locale. Per Gmail, utilizza una password per l'app.",
|
||||||
"enableApprise": "Abilita Appres",
|
"enablePushover": "Abilita Pushover",
|
||||||
|
"pushoverUserKey": "Chiave utente o gruppo",
|
||||||
|
"pushoverApiToken": "Token API dell’applicazione",
|
||||||
|
"pushoverDevice": "Dispositivo",
|
||||||
|
"pushoverSound": "Suono",
|
||||||
|
"pushoverCriticalPriority": "Priorità alta per gli avvisi critici",
|
||||||
|
"pushoverCriticalPriorityDescription": "Le notifiche CRITICHE usano la priorità alta di Pushover. La priorità di emergenza non viene utilizzata.",
|
||||||
|
"pushoverCredentialsHint": "Crea un’applicazione in Pushover e copia il relativo token API e la chiave utente o gruppo.",
|
||||||
|
"enableApprise": "Abilita Apprise",
|
||||||
"urlFormats": "+Formati URL",
|
"urlFormats": "+Formati URL",
|
||||||
"appriseUrl": "Informare l'URL",
|
"appriseUrl": "URL di Apprise",
|
||||||
"showUrl": "Mostra URL",
|
"showUrl": "Mostra URL",
|
||||||
"hideUrl": "Nascondi l'URL",
|
"hideUrl": "Nascondi l'URL",
|
||||||
"appriseDescription": "Un URL consente ad Apprise di indirizzare la notifica al servizio giusto. Esempi:",
|
"appriseDescription": "Un URL consente ad Apprise di indirizzare la notifica al servizio giusto. Esempi:",
|
||||||
|
|||||||
@@ -1839,9 +1839,17 @@
|
|||||||
"toAddresses": "Para endereços (separados por vírgula)",
|
"toAddresses": "Para endereços (separados por vírgula)",
|
||||||
"subjectPrefix": "Prefixo do assunto",
|
"subjectPrefix": "Prefixo do assunto",
|
||||||
"emailHint": "Deixe o host SMTP vazio para usar o sendmail local. Para Gmail, use uma senha de aplicativo.",
|
"emailHint": "Deixe o host SMTP vazio para usar o sendmail local. Para Gmail, use uma senha de aplicativo.",
|
||||||
"enableApprise": "Habilitar informar",
|
"enablePushover": "Ativar Pushover",
|
||||||
|
"pushoverUserKey": "Chave de utilizador ou grupo",
|
||||||
|
"pushoverApiToken": "Token API da aplicação",
|
||||||
|
"pushoverDevice": "Dispositivo",
|
||||||
|
"pushoverSound": "Som",
|
||||||
|
"pushoverCriticalPriority": "Prioridade alta para alertas críticos",
|
||||||
|
"pushoverCriticalPriorityDescription": "As notificações CRÍTICAS usam a prioridade alta do Pushover. A prioridade de emergência não é utilizada.",
|
||||||
|
"pushoverCredentialsHint": "Crie uma aplicação no Pushover e copie o respetivo token API e a chave de utilizador ou grupo.",
|
||||||
|
"enableApprise": "Ativar Apprise",
|
||||||
"urlFormats": "+Formatos de URL",
|
"urlFormats": "+Formatos de URL",
|
||||||
"appriseUrl": "Informar URL",
|
"appriseUrl": "URL do Apprise",
|
||||||
"showUrl": "Mostrar URL",
|
"showUrl": "Mostrar URL",
|
||||||
"hideUrl": "Ocultar URL",
|
"hideUrl": "Ocultar URL",
|
||||||
"appriseDescription": "Um URL permite que o Apprise encaminhe a notificação para o serviço certo. Exemplos:",
|
"appriseDescription": "Um URL permite que o Apprise encaminhe a notificação para o serviço certo. Exemplos:",
|
||||||
|
|||||||
@@ -1838,6 +1838,14 @@
|
|||||||
"toAddresses": "Adresy príjemcov (oddelené čiarkou)",
|
"toAddresses": "Adresy príjemcov (oddelené čiarkou)",
|
||||||
"subjectPrefix": "Predpona predmetu",
|
"subjectPrefix": "Predpona predmetu",
|
||||||
"emailHint": "Ak SMTP server necháte prázdny, použije sa lokálny sendmail. Pre Gmail použite heslo aplikácie.",
|
"emailHint": "Ak SMTP server necháte prázdny, použije sa lokálny sendmail. Pre Gmail použite heslo aplikácie.",
|
||||||
|
"enablePushover": "Zapnúť Pushover",
|
||||||
|
"pushoverUserKey": "Kľúč používateľa alebo skupiny",
|
||||||
|
"pushoverApiToken": "API token aplikácie",
|
||||||
|
"pushoverDevice": "Zariadenie",
|
||||||
|
"pushoverSound": "Zvuk",
|
||||||
|
"pushoverCriticalPriority": "Vysoká priorita pre kritické upozornenia",
|
||||||
|
"pushoverCriticalPriorityDescription": "KRITICKÉ notifikácie používajú vysokú prioritu Pushover. Núdzová priorita sa nepoužíva.",
|
||||||
|
"pushoverCredentialsHint": "Vytvorte aplikáciu v službe Pushover a skopírujte jej API token a kľúč používateľa alebo skupiny.",
|
||||||
"enableApprise": "Zapnúť Apprise",
|
"enableApprise": "Zapnúť Apprise",
|
||||||
"urlFormats": "+formáty URL",
|
"urlFormats": "+formáty URL",
|
||||||
"appriseUrl": "URL Apprise",
|
"appriseUrl": "URL Apprise",
|
||||||
|
|||||||
@@ -1839,9 +1839,17 @@
|
|||||||
"toAddresses": "Till adresser (kommaseparerade)",
|
"toAddresses": "Till adresser (kommaseparerade)",
|
||||||
"subjectPrefix": "Ämnesprefix",
|
"subjectPrefix": "Ämnesprefix",
|
||||||
"emailHint": "Lämna SMTP-värden tom för att använda lokal sendmail. Använd ett applösenord för Gmail.",
|
"emailHint": "Lämna SMTP-värden tom för att använda lokal sendmail. Använd ett applösenord för Gmail.",
|
||||||
|
"enablePushover": "Aktivera Pushover",
|
||||||
|
"pushoverUserKey": "Användar- eller gruppnyckel",
|
||||||
|
"pushoverApiToken": "Appens API-token",
|
||||||
|
"pushoverDevice": "Enhet",
|
||||||
|
"pushoverSound": "Ljud",
|
||||||
|
"pushoverCriticalPriority": "Hög prioritet för kritiska varningar",
|
||||||
|
"pushoverCriticalPriorityDescription": "KRITISKA aviseringar använder hög prioritet i Pushover. Nödprioritet används inte.",
|
||||||
|
"pushoverCredentialsHint": "Skapa en app i Pushover och kopiera dess API-token och din användar- eller gruppnyckel.",
|
||||||
"enableApprise": "Aktivera Apprise",
|
"enableApprise": "Aktivera Apprise",
|
||||||
"urlFormats": "+URL-format",
|
"urlFormats": "+URL-format",
|
||||||
"appriseUrl": "Upplys URL",
|
"appriseUrl": "Apprise-URL",
|
||||||
"showUrl": "Visa URL",
|
"showUrl": "Visa URL",
|
||||||
"hideUrl": "Dölj URL",
|
"hideUrl": "Dölj URL",
|
||||||
"appriseDescription": "En URL låter Apprise dirigera meddelandet till rätt tjänst. Exempel:",
|
"appriseDescription": "En URL låter Apprise dirigera meddelandet till rätt tjänst. Exempel:",
|
||||||
|
|||||||
@@ -1055,10 +1055,13 @@ PROXMOX_CUSTOM_CERT_PATH = "/etc/pve/local/pveproxy-ssl.pem"
|
|||||||
PROXMOX_CUSTOM_KEY_PATH = "/etc/pve/local/pveproxy-ssl.key"
|
PROXMOX_CUSTOM_KEY_PATH = "/etc/pve/local/pveproxy-ssl.key"
|
||||||
|
|
||||||
_SSL_RUNTIME_LOCK = threading.RLock()
|
_SSL_RUNTIME_LOCK = threading.RLock()
|
||||||
|
_SSL_RUNTIME_REFRESH_LOCK = threading.Lock()
|
||||||
_SSL_RUNTIME_CONTEXT = None
|
_SSL_RUNTIME_CONTEXT = None
|
||||||
_SSL_RUNTIME_FINGERPRINT = ""
|
_SSL_RUNTIME_FINGERPRINT = ""
|
||||||
_SSL_RUNTIME_CERT_PATH = ""
|
_SSL_RUNTIME_CERT_PATH = ""
|
||||||
_SSL_RUNTIME_KEY_PATH = ""
|
_SSL_RUNTIME_KEY_PATH = ""
|
||||||
|
_SSL_RUNTIME_SOURCE = "none"
|
||||||
|
_SSL_RUNTIME_LAST_REFRESH_ERROR = ""
|
||||||
|
|
||||||
|
|
||||||
def load_ssl_config():
|
def load_ssl_config():
|
||||||
@@ -1100,6 +1103,15 @@ def save_ssl_config(config):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_proxmox_certificate_paths():
|
||||||
|
"""Return the certificate pair currently preferred by Proxmox."""
|
||||||
|
if os.path.isfile(PROXMOX_CUSTOM_CERT_PATH) and os.path.isfile(PROXMOX_CUSTOM_KEY_PATH):
|
||||||
|
return PROXMOX_CUSTOM_CERT_PATH, PROXMOX_CUSTOM_KEY_PATH
|
||||||
|
if os.path.isfile(PROXMOX_CERT_PATH) and os.path.isfile(PROXMOX_KEY_PATH):
|
||||||
|
return PROXMOX_CERT_PATH, PROXMOX_KEY_PATH
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
|
||||||
def detect_proxmox_certificates():
|
def detect_proxmox_certificates():
|
||||||
"""
|
"""
|
||||||
Detect available Proxmox certificates.
|
Detect available Proxmox certificates.
|
||||||
@@ -1117,11 +1129,10 @@ def detect_proxmox_certificates():
|
|||||||
"cert_info": None
|
"cert_info": None
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.path.isfile(PROXMOX_CUSTOM_CERT_PATH) and os.path.isfile(PROXMOX_CUSTOM_KEY_PATH):
|
cert_path, key_path = _detect_proxmox_certificate_paths()
|
||||||
result["proxmox_cert"] = PROXMOX_CUSTOM_CERT_PATH
|
if cert_path and key_path:
|
||||||
result["proxmox_key"] = PROXMOX_CUSTOM_KEY_PATH
|
result["proxmox_cert"] = cert_path
|
||||||
result["proxmox_available"] = True
|
result["proxmox_key"] = key_path
|
||||||
elif os.path.isfile(PROXMOX_CERT_PATH) and os.path.isfile(PROXMOX_KEY_PATH):
|
|
||||||
result["proxmox_available"] = True
|
result["proxmox_available"] = True
|
||||||
|
|
||||||
if result["proxmox_available"]:
|
if result["proxmox_available"]:
|
||||||
@@ -1209,17 +1220,112 @@ def _build_server_ssl_context(cert_path, key_path):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _record_ssl_refresh_error(error):
|
||||||
|
"""Log one warning per distinct automatic-refresh failure."""
|
||||||
|
global _SSL_RUNTIME_LAST_REFRESH_ERROR
|
||||||
|
|
||||||
|
message = str(error)
|
||||||
|
with _SSL_RUNTIME_LOCK:
|
||||||
|
if message == _SSL_RUNTIME_LAST_REFRESH_ERROR:
|
||||||
|
return
|
||||||
|
_SSL_RUNTIME_LAST_REFRESH_ERROR = message
|
||||||
|
print(
|
||||||
|
"[ProxMenux] Proxmox TLS certificate refresh skipped; "
|
||||||
|
f"the active certificate remains unchanged: {message}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _persist_active_proxmox_certificate_paths(cert_path, key_path):
|
||||||
|
"""Keep the selected Proxmox pair in sync for the next service start."""
|
||||||
|
config = load_ssl_config()
|
||||||
|
if not config.get("enabled") or config.get("source") != "proxmox":
|
||||||
|
return
|
||||||
|
if config.get("cert_path") == cert_path and config.get("key_path") == key_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
updated_config = dict(config)
|
||||||
|
updated_config["cert_path"] = cert_path
|
||||||
|
updated_config["key_path"] = key_path
|
||||||
|
if not save_ssl_config(updated_config):
|
||||||
|
print(
|
||||||
|
"[ProxMenux] Warning: the renewed Proxmox certificate is active, "
|
||||||
|
"but its paths could not be saved for the next service start",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_proxmox_ssl_context_for_handshake():
|
||||||
|
"""Activate a renewed Proxmox pair just before a TLS handshake.
|
||||||
|
|
||||||
|
This deliberately has no timer and does not depend on inotify (pmxcfs can
|
||||||
|
update /etc/pve without emitting a local event). The small PEM pair is
|
||||||
|
inspected only when a client starts a new TLS connection. Any missing,
|
||||||
|
partial or mismatched pair leaves the already-active context untouched.
|
||||||
|
"""
|
||||||
|
global _SSL_RUNTIME_LAST_REFRESH_ERROR
|
||||||
|
|
||||||
|
with _SSL_RUNTIME_LOCK:
|
||||||
|
if _SSL_RUNTIME_SOURCE != "proxmox" or _SSL_RUNTIME_CONTEXT is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Several browser connections can arrive together. Only one of them may
|
||||||
|
# validate/swap a newly written pair; the others reuse its result.
|
||||||
|
with _SSL_RUNTIME_REFRESH_LOCK:
|
||||||
|
with _SSL_RUNTIME_LOCK:
|
||||||
|
if _SSL_RUNTIME_SOURCE != "proxmox" or _SSL_RUNTIME_CONTEXT is None:
|
||||||
|
return False
|
||||||
|
active_fingerprint = _SSL_RUNTIME_FINGERPRINT
|
||||||
|
active_cert_path = _SSL_RUNTIME_CERT_PATH
|
||||||
|
active_key_path = _SSL_RUNTIME_KEY_PATH
|
||||||
|
|
||||||
|
cert_path, key_path = _detect_proxmox_certificate_paths()
|
||||||
|
if not cert_path or not key_path:
|
||||||
|
raise RuntimeError("No complete Proxmox certificate/key pair was detected")
|
||||||
|
|
||||||
|
candidate_fingerprint = _certificate_pair_fingerprint(cert_path, key_path)
|
||||||
|
paths_changed = cert_path != active_cert_path or key_path != active_key_path
|
||||||
|
if candidate_fingerprint == active_fingerprint and not paths_changed:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# reload_server_ssl_context builds and validates the replacement first
|
||||||
|
# and checks that neither PEM changed while it was being loaded. The
|
||||||
|
# global context is swapped only after all of those checks succeed.
|
||||||
|
changed = reload_server_ssl_context(cert_path, key_path)
|
||||||
|
_persist_active_proxmox_certificate_paths(cert_path, key_path)
|
||||||
|
with _SSL_RUNTIME_LOCK:
|
||||||
|
_SSL_RUNTIME_LAST_REFRESH_ERROR = ""
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
print(
|
||||||
|
f"[ProxMenux] Renewed Proxmox TLS certificate activated from {cert_path}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def create_reloadable_ssl_context(cert_path, key_path):
|
def create_reloadable_ssl_context(cert_path, key_path):
|
||||||
"""Create the server context and register it for manual hot reloads."""
|
"""Create the stable server context used by automatic and manual reloads."""
|
||||||
global _SSL_RUNTIME_CONTEXT
|
global _SSL_RUNTIME_CONTEXT
|
||||||
global _SSL_RUNTIME_FINGERPRINT
|
global _SSL_RUNTIME_FINGERPRINT
|
||||||
global _SSL_RUNTIME_CERT_PATH
|
global _SSL_RUNTIME_CERT_PATH
|
||||||
global _SSL_RUNTIME_KEY_PATH
|
global _SSL_RUNTIME_KEY_PATH
|
||||||
|
global _SSL_RUNTIME_SOURCE
|
||||||
|
global _SSL_RUNTIME_LAST_REFRESH_ERROR
|
||||||
|
|
||||||
context = _build_server_ssl_context(cert_path, key_path)
|
context = _build_server_ssl_context(cert_path, key_path)
|
||||||
fingerprint = _certificate_pair_fingerprint(cert_path, key_path)
|
fingerprint = _certificate_pair_fingerprint(cert_path, key_path)
|
||||||
|
config = load_ssl_config()
|
||||||
|
source = config.get("source", "none") if config.get("enabled") else "none"
|
||||||
|
|
||||||
def _select_active_context(ssl_socket, _server_name, _initial_context):
|
def _select_active_context(ssl_socket, _server_name, _initial_context):
|
||||||
|
try:
|
||||||
|
_refresh_proxmox_ssl_context_for_handshake()
|
||||||
|
except Exception as error:
|
||||||
|
# Never fail a client handshake because Proxmox is between the
|
||||||
|
# certificate and key writes. The previously validated context
|
||||||
|
# remains authoritative until a later connection can load both.
|
||||||
|
_record_ssl_refresh_error(error)
|
||||||
with _SSL_RUNTIME_LOCK:
|
with _SSL_RUNTIME_LOCK:
|
||||||
active_context = _SSL_RUNTIME_CONTEXT
|
active_context = _SSL_RUNTIME_CONTEXT
|
||||||
if active_context is not None and ssl_socket.context is not active_context:
|
if active_context is not None and ssl_socket.context is not active_context:
|
||||||
@@ -1231,6 +1337,8 @@ def create_reloadable_ssl_context(cert_path, key_path):
|
|||||||
_SSL_RUNTIME_FINGERPRINT = fingerprint
|
_SSL_RUNTIME_FINGERPRINT = fingerprint
|
||||||
_SSL_RUNTIME_CERT_PATH = cert_path
|
_SSL_RUNTIME_CERT_PATH = cert_path
|
||||||
_SSL_RUNTIME_KEY_PATH = key_path
|
_SSL_RUNTIME_KEY_PATH = key_path
|
||||||
|
_SSL_RUNTIME_SOURCE = source
|
||||||
|
_SSL_RUNTIME_LAST_REFRESH_ERROR = ""
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ TOOL_METADATA = {
|
|||||||
'kernel_panic': {'name': 'Kernel Panic Configuration', 'function': 'configure_kernel_panic', 'version': '1.0'},
|
'kernel_panic': {'name': 'Kernel Panic Configuration', 'function': 'configure_kernel_panic', 'version': '1.0'},
|
||||||
'apt_ipv4': {'name': 'APT IPv4 Force', 'function': 'force_apt_ipv4', 'version': '1.0'},
|
'apt_ipv4': {'name': 'APT IPv4 Force', 'function': 'force_apt_ipv4', 'version': '1.0'},
|
||||||
'kexec': {'name': 'kexec for quick reboots', 'function': 'enable_kexec', 'version': '1.0'},
|
'kexec': {'name': 'kexec for quick reboots', 'function': 'enable_kexec', 'version': '1.0'},
|
||||||
|
'rpc': {'name': 'RPC / rpcbind Disable', 'function': 'disable_rpc', 'version': '1.0'},
|
||||||
|
'motd': {'name': 'Custom MOTD Banner', 'function': 'setup_motd', 'version': '1.0'},
|
||||||
|
'system_utils': {'name': 'System Utilities', 'function': 'install_system_utils', 'version': '1.0'},
|
||||||
'network_optimization': {'name': 'Network Optimizations', 'function': 'apply_network_optimizations', 'version': '1.0'},
|
'network_optimization': {'name': 'Network Optimizations', 'function': 'apply_network_optimizations', 'version': '1.0'},
|
||||||
'bashrc_custom': {'name': 'Bashrc Customization', 'function': 'customize_bashrc', 'version': '1.0'},
|
'bashrc_custom': {'name': 'Bashrc Customization', 'function': 'customize_bashrc', 'version': '1.0'},
|
||||||
'figurine': {'name': 'Figurine', 'function': 'configure_figurine', 'version': '1.0'},
|
'figurine': {'name': 'Figurine', 'function': 'configure_figurine', 'version': '1.0'},
|
||||||
|
|||||||
@@ -1123,8 +1123,8 @@ def _check_oci_app(entry: dict) -> dict:
|
|||||||
# returns the single newest version, e.g. "580.105.08"
|
# returns the single newest version, e.g. "580.105.08"
|
||||||
# `https://download.nvidia.com/XFree86/Linux-x86_64/`
|
# `https://download.nvidia.com/XFree86/Linux-x86_64/`
|
||||||
# HTML directory listing — we scrape it for per-branch latest
|
# HTML directory listing — we scrape it for per-branch latest
|
||||||
# (so a user on 570.x gets 570.x's latest, not pushed to 580.x
|
# (so a user on 570.x gets 570.x's latest, without an automatic
|
||||||
# unless their kernel forces a branch upgrade).
|
# cross-branch upgrade).
|
||||||
#
|
#
|
||||||
# Cache TTL is 7 days because NVIDIA's release cadence on each branch
|
# Cache TTL is 7 days because NVIDIA's release cadence on each branch
|
||||||
# is roughly monthly. The cache is in-memory only; AppImage restarts
|
# is roughly monthly. The cache is in-memory only; AppImage restarts
|
||||||
@@ -1135,15 +1135,6 @@ _NVIDIA_CACHE_TTL = 7 * 86400
|
|||||||
_nvidia_cache: dict[str, Any] = {"versions": [], "fetched_at": 0}
|
_nvidia_cache: dict[str, Any] = {"versions": [], "fetched_at": 0}
|
||||||
|
|
||||||
|
|
||||||
def _kernel_string() -> str:
|
|
||||||
try:
|
|
||||||
return subprocess.run(
|
|
||||||
["uname", "-r"], capture_output=True, text=True, timeout=2,
|
|
||||||
).stdout.strip()
|
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _version_tuple(v: str) -> tuple:
|
def _version_tuple(v: str) -> tuple:
|
||||||
"""Convert ``580.105.08`` → ``(580, 105, 8)`` for comparison.
|
"""Convert ``580.105.08`` → ``(580, 105, 8)`` for comparison.
|
||||||
Pads to 3 components so ``580.82`` < ``580.105.08``."""
|
Pads to 3 components so ``580.82`` < ``580.105.08``."""
|
||||||
@@ -1216,7 +1207,6 @@ def _check_nvidia_xfree86(entry: dict) -> dict:
|
|||||||
"last_check": _now_iso(),
|
"last_check": _now_iso(),
|
||||||
"error": None,
|
"error": None,
|
||||||
"_upgrade_kind": "patch" if available else None,
|
"_upgrade_kind": "patch" if available else None,
|
||||||
"_kernel": _kernel_string(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
ProxMenux Notification Channels
|
ProxMenux Notification Channels
|
||||||
Provides transport adapters for Telegram, Gotify, and Discord.
|
Provides transport adapters for Telegram, Gotify, Discord, Email, Pushover,
|
||||||
|
and Apprise.
|
||||||
|
|
||||||
Each channel implements send() and test() with:
|
Each channel implements send() and test() with:
|
||||||
- Retry with exponential backoff (3 attempts)
|
- Retry with exponential backoff (3 attempts)
|
||||||
@@ -12,6 +13,7 @@ Author: MacRimi
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@@ -392,6 +394,119 @@ class GotifyChannel(NotificationChannel):
|
|||||||
return self._http_request(url, payload, {'Content-Type': 'application/json'})
|
return self._http_request(url, payload, {'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Pushover ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class PushoverChannel(NotificationChannel):
|
||||||
|
"""Pushover Messages API channel."""
|
||||||
|
|
||||||
|
API_URL = 'https://api.pushover.net/1/messages.json'
|
||||||
|
MAX_TITLE_LENGTH = 250
|
||||||
|
MAX_MESSAGE_LENGTH = 1024
|
||||||
|
_CREDENTIAL_RE = re.compile(r'^[A-Za-z0-9]{30}$')
|
||||||
|
_OPTION_RE = re.compile(r'^[A-Za-z0-9_-]{1,25}$')
|
||||||
|
|
||||||
|
def __init__(self, user_key: str, api_token: str, device: str = '',
|
||||||
|
sound: str = '', critical_priority: str = 'true'):
|
||||||
|
super().__init__()
|
||||||
|
self.user_key = (user_key or '').strip()
|
||||||
|
self.api_token = (api_token or '').strip()
|
||||||
|
self.device = (device or '').strip()
|
||||||
|
self.sound = (sound or '').strip()
|
||||||
|
self.critical_priority = str(critical_priority).lower() == 'true'
|
||||||
|
|
||||||
|
def validate_config(self) -> Tuple[bool, str]:
|
||||||
|
if not self.user_key:
|
||||||
|
return False, 'Pushover user or group key is required'
|
||||||
|
if not self.api_token:
|
||||||
|
return False, 'Pushover application API token is required'
|
||||||
|
if not self._CREDENTIAL_RE.fullmatch(self.user_key):
|
||||||
|
return False, 'Invalid Pushover user or group key format'
|
||||||
|
if not self._CREDENTIAL_RE.fullmatch(self.api_token):
|
||||||
|
return False, 'Invalid Pushover application API token format'
|
||||||
|
if self.device and not self._OPTION_RE.fullmatch(self.device):
|
||||||
|
return False, 'Invalid Pushover device name format'
|
||||||
|
if self.sound and not self._OPTION_RE.fullmatch(self.sound):
|
||||||
|
return False, 'Invalid Pushover sound name format'
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _truncate(value: str, limit: int) -> str:
|
||||||
|
value = value or ''
|
||||||
|
if len(value) <= limit:
|
||||||
|
return value
|
||||||
|
return value[:limit - 1].rstrip() + '…'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _response_error(body: str) -> str:
|
||||||
|
try:
|
||||||
|
payload = json.loads(body or '{}')
|
||||||
|
errors = payload.get('errors')
|
||||||
|
if isinstance(errors, list):
|
||||||
|
clean = [str(item)[:160] for item in errors if item]
|
||||||
|
if clean:
|
||||||
|
return '; '.join(clean)
|
||||||
|
if isinstance(errors, str) and errors:
|
||||||
|
return errors[:200]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return 'Pushover API rejected the request'
|
||||||
|
|
||||||
|
def _post_message(self, title: str, message: str,
|
||||||
|
priority: int) -> Tuple[int, str]:
|
||||||
|
payload = {
|
||||||
|
'token': self.api_token,
|
||||||
|
'user': self.user_key,
|
||||||
|
'title': self._truncate(title, self.MAX_TITLE_LENGTH),
|
||||||
|
'message': self._truncate(message, self.MAX_MESSAGE_LENGTH),
|
||||||
|
'priority': str(priority),
|
||||||
|
}
|
||||||
|
if self.device:
|
||||||
|
payload['device'] = self.device
|
||||||
|
if self.sound:
|
||||||
|
payload['sound'] = self.sound
|
||||||
|
|
||||||
|
body = urllib.parse.urlencode(payload).encode('utf-8')
|
||||||
|
status, response_body = self._http_request(
|
||||||
|
self.API_URL,
|
||||||
|
body,
|
||||||
|
{'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
)
|
||||||
|
if 200 <= status < 300:
|
||||||
|
try:
|
||||||
|
response = json.loads(response_body or '{}')
|
||||||
|
if response.get('status') == 1:
|
||||||
|
return status, ''
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return 400, self._response_error(response_body)
|
||||||
|
return status, self._response_error(response_body)
|
||||||
|
|
||||||
|
def send(self, title: str, message: str, severity: str = 'INFO',
|
||||||
|
data: Optional[Dict] = None) -> Dict[str, Any]:
|
||||||
|
valid, error = self.validate_config()
|
||||||
|
if not valid:
|
||||||
|
return {'success': False, 'error': error, 'channel': 'pushover'}
|
||||||
|
|
||||||
|
priority = (
|
||||||
|
1
|
||||||
|
if self.critical_priority and str(severity or '').upper() == 'CRITICAL'
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
result = self._send_with_retry(
|
||||||
|
lambda: self._post_message(title, message, priority)
|
||||||
|
)
|
||||||
|
result['channel'] = 'pushover'
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test(self) -> Tuple[bool, str]:
|
||||||
|
result = self.send(
|
||||||
|
'ProxMenux Test',
|
||||||
|
'Pushover is configured correctly. This is a test message from ProxMenux Monitor.',
|
||||||
|
'INFO',
|
||||||
|
)
|
||||||
|
return result['success'], result.get('error', '')
|
||||||
|
|
||||||
|
|
||||||
# ─── Discord ─────────────────────────────────────────────────────
|
# ─── Discord ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
class DiscordChannel(NotificationChannel):
|
class DiscordChannel(NotificationChannel):
|
||||||
@@ -1189,7 +1304,7 @@ class AppriseChannel(NotificationChannel):
|
|||||||
Apprise (https://github.com/caronc/apprise) is a Python library that
|
Apprise (https://github.com/caronc/apprise) is a Python library that
|
||||||
normalises a wide catalogue of notification destinations behind a
|
normalises a wide catalogue of notification destinations behind a
|
||||||
single URL scheme: `tgram://`, `discord://`, `slack://`, `gotify://`,
|
single URL scheme: `tgram://`, `discord://`, `slack://`, `gotify://`,
|
||||||
`ntfy://`, `matrix://`, `mailto://`, `pushover://`, `signal://`, etc.
|
`ntfy://`, `matrix://`, `mailto://`, `pover://`, `signal://`, etc.
|
||||||
The operator pastes one URL and ProxMenux delegates the transport.
|
The operator pastes one URL and ProxMenux delegates the transport.
|
||||||
|
|
||||||
Requested in issue #207 by @0berkampf. Implemented as a *separate
|
Requested in issue #207 by @0berkampf. Implemented as a *separate
|
||||||
@@ -1347,6 +1462,13 @@ CHANNEL_TYPES = {
|
|||||||
'from_address', 'to_addresses', 'subject_prefix'],
|
'from_address', 'to_addresses', 'subject_prefix'],
|
||||||
'class': EmailChannel,
|
'class': EmailChannel,
|
||||||
},
|
},
|
||||||
|
'pushover': {
|
||||||
|
'name': 'Pushover',
|
||||||
|
'config_keys': ['user_key', 'api_token', 'device', 'sound',
|
||||||
|
'critical_priority'],
|
||||||
|
'required_keys': ['user_key', 'api_token'],
|
||||||
|
'class': PushoverChannel,
|
||||||
|
},
|
||||||
'apprise': {
|
'apprise': {
|
||||||
'name': 'Apprise',
|
'name': 'Apprise',
|
||||||
'config_keys': ['url'],
|
'config_keys': ['url'],
|
||||||
@@ -1359,7 +1481,8 @@ def create_channel(channel_type: str, config: Dict[str, str]) -> Optional[Notifi
|
|||||||
"""Create a channel instance from type name and config dict.
|
"""Create a channel instance from type name and config dict.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
channel_type: 'telegram', 'gotify', 'discord', 'email', or 'apprise'
|
channel_type: 'telegram', 'gotify', 'discord', 'email', 'pushover',
|
||||||
|
or 'apprise'
|
||||||
config: Dict with channel-specific keys (see CHANNEL_TYPES)
|
config: Dict with channel-specific keys (see CHANNEL_TYPES)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -1383,6 +1506,14 @@ def create_channel(channel_type: str, config: Dict[str, str]) -> Optional[Notifi
|
|||||||
)
|
)
|
||||||
elif channel_type == 'email':
|
elif channel_type == 'email':
|
||||||
return EmailChannel(config)
|
return EmailChannel(config)
|
||||||
|
elif channel_type == 'pushover':
|
||||||
|
return PushoverChannel(
|
||||||
|
user_key=config.get('user_key', ''),
|
||||||
|
api_token=config.get('api_token', ''),
|
||||||
|
device=config.get('device', ''),
|
||||||
|
sound=config.get('sound', ''),
|
||||||
|
critical_priority=config.get('critical_priority', 'true'),
|
||||||
|
)
|
||||||
elif channel_type == 'apprise':
|
elif channel_type == 'apprise':
|
||||||
return AppriseChannel(url=config.get('url', ''))
|
return AppriseChannel(url=config.get('url', ''))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -3817,21 +3817,13 @@ class PollingCollector:
|
|||||||
return 'secure_gateway_update_available', data
|
return 'secure_gateway_update_available', data
|
||||||
|
|
||||||
if item_type == 'nvidia_xfree86':
|
if item_type == 'nvidia_xfree86':
|
||||||
kind = update.get('_upgrade_kind')
|
upgrade_reason = (
|
||||||
if kind == 'branch_upgrade':
|
"Same-branch maintenance update with bug/security fixes. "
|
||||||
upgrade_reason = (
|
"The installer validates the selected release by rebuilding "
|
||||||
"Your current driver branch is no longer compatible with "
|
"its DKMS module against the running kernel."
|
||||||
f"kernel {update.get('_kernel') or 'this kernel'}. "
|
)
|
||||||
"Switch to the recommended branch — the installer will "
|
|
||||||
"rebuild against the running kernel."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
upgrade_reason = (
|
|
||||||
"Same-branch maintenance update with bug/security fixes."
|
|
||||||
)
|
|
||||||
data = {
|
data = {
|
||||||
**common,
|
**common,
|
||||||
'kernel': update.get('_kernel') or '',
|
|
||||||
'upgrade_reason': upgrade_reason,
|
'upgrade_reason': upgrade_reason,
|
||||||
}
|
}
|
||||||
return 'nvidia_driver_update_available', data
|
return 'nvidia_driver_update_available', data
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ ProxMenux Notification Manager
|
|||||||
Central orchestrator for the notification service.
|
Central orchestrator for the notification service.
|
||||||
|
|
||||||
Connects:
|
Connects:
|
||||||
- notification_channels.py (transport: Telegram, Gotify, Discord)
|
- notification_channels.py (transport: Telegram, Gotify, Discord, Email,
|
||||||
|
Pushover, Apprise)
|
||||||
- notification_templates.py (message formatting + optional AI)
|
- notification_templates.py (message formatting + optional AI)
|
||||||
- notification_events.py (event detection: Journal, Task, Polling watchers)
|
- notification_events.py (event detection: Journal, Task, Polling watchers)
|
||||||
- health_persistence.py (DB: config storage, notification_history)
|
- health_persistence.py (DB: config storage, notification_history)
|
||||||
@@ -79,6 +80,8 @@ SENSITIVE_KEYS = {
|
|||||||
'gotify.token',
|
'gotify.token',
|
||||||
'discord.webhook_url',
|
'discord.webhook_url',
|
||||||
'email.password',
|
'email.password',
|
||||||
|
'pushover.user_key',
|
||||||
|
'pushover.api_token',
|
||||||
'apprise.url',
|
'apprise.url',
|
||||||
'webhook_secret',
|
'webhook_secret',
|
||||||
}
|
}
|
||||||
@@ -2520,9 +2523,10 @@ class NotificationManager:
|
|||||||
channels_info = {}
|
channels_info = {}
|
||||||
for ch_type, info in CHANNEL_TYPES.items():
|
for ch_type, info in CHANNEL_TYPES.items():
|
||||||
enabled = self._config.get(f'{ch_type}.enabled', 'false') == 'true'
|
enabled = self._config.get(f'{ch_type}.enabled', 'false') == 'true'
|
||||||
|
required_keys = info.get('required_keys', info['config_keys'])
|
||||||
configured = all(
|
configured = all(
|
||||||
bool(self._config.get(f'{ch_type}.{k}', ''))
|
bool(self._config.get(f'{ch_type}.{k}', ''))
|
||||||
for k in info['config_keys']
|
for k in required_keys
|
||||||
)
|
)
|
||||||
channels_info[ch_type] = {
|
channels_info[ch_type] = {
|
||||||
'name': info['name'],
|
'name': info['name'],
|
||||||
|
|||||||
@@ -1318,7 +1318,7 @@ TEMPLATES = {
|
|||||||
'nvidia_driver_update_available': {
|
'nvidia_driver_update_available': {
|
||||||
'title': '{hostname}: NVIDIA driver update available — v{latest_version}',
|
'title': '{hostname}: NVIDIA driver update available — v{latest_version}',
|
||||||
'body': (
|
'body': (
|
||||||
'A newer NVIDIA driver compatible with kernel {kernel} is available.\n'
|
'A newer maintenance release is available for the installed NVIDIA driver branch.\n'
|
||||||
'🔹 Currently installed: v{current_version}\n'
|
'🔹 Currently installed: v{current_version}\n'
|
||||||
'🟢 Latest available: v{latest_version}\n\n'
|
'🟢 Latest available: v{latest_version}\n\n'
|
||||||
'{upgrade_reason}\n\n'
|
'{upgrade_reason}\n\n'
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import urllib.parse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
if str(SCRIPTS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||||
|
|
||||||
|
from notification_channels import PushoverChannel, create_channel
|
||||||
|
|
||||||
|
|
||||||
|
class PushoverChannelTests(unittest.TestCase):
|
||||||
|
USER_KEY = "u" * 30
|
||||||
|
API_TOKEN = "a" * 30
|
||||||
|
|
||||||
|
def make_channel(self, **kwargs):
|
||||||
|
channel = PushoverChannel(
|
||||||
|
user_key=kwargs.pop("user_key", self.USER_KEY),
|
||||||
|
api_token=kwargs.pop("api_token", self.API_TOKEN),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
channel.MAX_RETRIES = 1
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def test_requires_valid_user_key_and_api_token(self):
|
||||||
|
channel = self.make_channel(user_key="")
|
||||||
|
self.assertEqual(channel.validate_config(), (
|
||||||
|
False, "Pushover user or group key is required"
|
||||||
|
))
|
||||||
|
|
||||||
|
channel = self.make_channel(api_token="short")
|
||||||
|
self.assertEqual(channel.validate_config(), (
|
||||||
|
False, "Invalid Pushover application API token format"
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_optional_device_sound_and_factory(self):
|
||||||
|
channel = create_channel("pushover", {
|
||||||
|
"user_key": self.USER_KEY,
|
||||||
|
"api_token": self.API_TOKEN,
|
||||||
|
"device": "iphone_15",
|
||||||
|
"sound": "magic",
|
||||||
|
"critical_priority": "false",
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsInstance(channel, PushoverChannel)
|
||||||
|
self.assertEqual(channel.validate_config(), (True, ""))
|
||||||
|
self.assertFalse(channel.critical_priority)
|
||||||
|
|
||||||
|
def test_critical_alert_uses_high_priority_and_api_limits(self):
|
||||||
|
channel = self.make_channel(
|
||||||
|
device="iphone_15",
|
||||||
|
sound="magic",
|
||||||
|
critical_priority="true",
|
||||||
|
)
|
||||||
|
request = {}
|
||||||
|
|
||||||
|
def fake_http(url, data, headers):
|
||||||
|
request["url"] = url
|
||||||
|
request["payload"] = urllib.parse.parse_qs(data.decode("utf-8"))
|
||||||
|
request["headers"] = headers
|
||||||
|
return 200, '{"status":1,"request":"test"}'
|
||||||
|
|
||||||
|
channel._http_request = fake_http
|
||||||
|
result = channel.send("T" * 300, "M" * 1200, "critical")
|
||||||
|
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(request["url"], PushoverChannel.API_URL)
|
||||||
|
self.assertEqual(request["payload"]["priority"], ["1"])
|
||||||
|
self.assertEqual(request["payload"]["device"], ["iphone_15"])
|
||||||
|
self.assertEqual(request["payload"]["sound"], ["magic"])
|
||||||
|
self.assertEqual(len(request["payload"]["title"][0]), 250)
|
||||||
|
self.assertEqual(len(request["payload"]["message"][0]), 1024)
|
||||||
|
self.assertTrue(request["payload"]["message"][0].endswith("…"))
|
||||||
|
|
||||||
|
def test_noncritical_alert_uses_normal_priority(self):
|
||||||
|
channel = self.make_channel(critical_priority="true")
|
||||||
|
request = {}
|
||||||
|
|
||||||
|
def fake_http(url, data, headers):
|
||||||
|
request["payload"] = urllib.parse.parse_qs(data.decode("utf-8"))
|
||||||
|
return 200, '{"status":1}'
|
||||||
|
|
||||||
|
channel._http_request = fake_http
|
||||||
|
result = channel.send("Warning", "Message", "WARNING")
|
||||||
|
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(request["payload"]["priority"], ["0"])
|
||||||
|
|
||||||
|
def test_api_error_does_not_expose_credentials(self):
|
||||||
|
channel = self.make_channel()
|
||||||
|
channel._http_request = lambda *args: (
|
||||||
|
400, '{"status":0,"errors":["user identifier is invalid"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = channel.send("Title", "Message")
|
||||||
|
|
||||||
|
self.assertFalse(result["success"])
|
||||||
|
self.assertIn("user identifier is invalid", result["error"])
|
||||||
|
self.assertNotIn(self.USER_KEY, result["error"])
|
||||||
|
self.assertNotIn(self.API_TOKEN, result["error"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+2
-2
@@ -441,8 +441,8 @@
|
|||||||
"CPU set to host,hidden=1,flags=+pcid": "CPU configurada como host,hidden=1,flags=+pcid",
|
"CPU set to host,hidden=1,flags=+pcid": "CPU configurada como host,hidden=1,flags=+pcid",
|
||||||
"CPU vendor (intel/amd):": "Proveedor de CPU (Intel/AMD):",
|
"CPU vendor (intel/amd):": "Proveedor de CPU (Intel/AMD):",
|
||||||
"CRITICAL: The selected disk is referenced by a RUNNING VM or CT.": "CRÍTICO: El disco seleccionado tiene referencia a una VM o CT EN EJECUCIÓN.",
|
"CRITICAL: The selected disk is referenced by a RUNNING VM or CT.": "CRÍTICO: El disco seleccionado tiene referencia a una VM o CT EN EJECUCIÓN.",
|
||||||
"CT": "Connecticut",
|
"CT": "LXC",
|
||||||
"CT started successfully.": "La TC se inició con éxito.",
|
"CT started successfully.": "El LXC se inició con éxito.",
|
||||||
"Cancel": "Cancelar",
|
"Cancel": "Cancelar",
|
||||||
"Cancel restore": "Cancelar restauración",
|
"Cancel restore": "Cancelar restauración",
|
||||||
"Cancel this setup": "cancelar esta configuración",
|
"Cancel this setup": "cancelar esta configuración",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
if [[ -n "${__PROXMENUX_PCI_PASSTHROUGH_HELPERS__}" ]]; then
|
if [[ -n "${__PROXMENUX_PCI_PASSTHROUGH_HELPERS__:-}" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
__PROXMENUX_PCI_PASSTHROUGH_HELPERS__=1
|
__PROXMENUX_PCI_PASSTHROUGH_HELPERS__=1
|
||||||
@@ -378,14 +378,23 @@ function _pci_sriov_role() {
|
|||||||
# PCI subsystem ADD event, which is exactly when we need them.
|
# PCI subsystem ADD event, which is exactly when we need them.
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
PROXMENUX_VFIO_BIND_STATE="/etc/proxmenux/vfio-bind.bdfs"
|
PROXMENUX_SYSFS_ROOT="${PROXMENUX_SYSFS_ROOT:-/sys}"
|
||||||
PROXMENUX_VFIO_BIND_UDEV_RULE="/etc/udev/rules.d/10-proxmenux-vfio-bind.rules"
|
PROXMENUX_ETC_ROOT="${PROXMENUX_ETC_ROOT:-/etc}"
|
||||||
|
PROXMENUX_STATE_ROOT="${PROXMENUX_STATE_ROOT:-${BASE_DIR:-/usr/local/share/proxmenux}}"
|
||||||
|
PROXMENUX_VFIO_BIND_STATE="${PROXMENUX_VFIO_BIND_STATE:-${PROXMENUX_ETC_ROOT}/proxmenux/vfio-bind.bdfs}"
|
||||||
|
PROXMENUX_VFIO_BIND_UDEV_RULE="${PROXMENUX_VFIO_BIND_UDEV_RULE:-${PROXMENUX_ETC_ROOT}/udev/rules.d/10-proxmenux-vfio-bind.rules}"
|
||||||
|
PROXMENUX_VFIO_CONF="${PROXMENUX_VFIO_CONF:-${PROXMENUX_ETC_ROOT}/modprobe.d/vfio.conf}"
|
||||||
# Auto-managed blacklist applied only when *every* NVIDIA GPU on the host
|
# Auto-managed blacklist applied only when *every* NVIDIA GPU on the host
|
||||||
# is in passthrough. Removed when any NVIDIA GPU goes back to the host.
|
# is in passthrough. Removed when any NVIDIA GPU goes back to the host.
|
||||||
PROXMENUX_NVIDIA_VFIO_BLACKLIST="/etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf"
|
PROXMENUX_NVIDIA_VFIO_BLACKLIST="${PROXMENUX_NVIDIA_VFIO_BLACKLIST:-${PROXMENUX_ETC_ROOT}/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf}"
|
||||||
|
PROXMENUX_NVIDIA_SERVICE_STATE="${PROXMENUX_NVIDIA_SERVICE_STATE:-${PROXMENUX_STATE_ROOT}/nvidia-host-services.state}"
|
||||||
|
# A short-lived implementation stored this state under /var/lib. Keep a
|
||||||
|
# one-way migration so upgraded hosts restore the exact service state that was
|
||||||
|
# captured there, then remove the provisional file.
|
||||||
|
PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE="${PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE:-/var/lib/proxmenux/nvidia-host-services.state}"
|
||||||
# Legacy artifact paths from a previous attempt — kept here so we can
|
# Legacy artifact paths from a previous attempt — kept here so we can
|
||||||
# remove them when migrating a host that ran the older init-top hook.
|
# remove them when migrating a host that ran the older init-top hook.
|
||||||
PROXMENUX_VFIO_BIND_LEGACY_HOOK="/etc/initramfs-tools/scripts/init-top/proxmenux-vfio-bind"
|
PROXMENUX_VFIO_BIND_LEGACY_HOOK="${PROXMENUX_VFIO_BIND_LEGACY_HOOK:-${PROXMENUX_ETC_ROOT}/initramfs-tools/scripts/init-top/proxmenux-vfio-bind}"
|
||||||
|
|
||||||
_proxmenux_vfio_bind_write_udev_rule() {
|
_proxmenux_vfio_bind_write_udev_rule() {
|
||||||
# Always nuke the obsolete init-top hook from earlier attempts (if it
|
# Always nuke the obsolete init-top hook from earlier attempts (if it
|
||||||
@@ -428,6 +437,38 @@ _proxmenux_vfio_bind_cleanup_legacy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_proxmenux_mark_host_config_changed() {
|
||||||
|
[[ -n "${HOST_CONFIG_CHANGED+x}" ]] && HOST_CONFIG_CHANGED=true
|
||||||
|
}
|
||||||
|
|
||||||
|
_proxmenux_vfio_bind_has_bdf() {
|
||||||
|
local bdf="$1"
|
||||||
|
[[ -n "$bdf" && -f "$PROXMENUX_VFIO_BIND_STATE" ]] || return 1
|
||||||
|
[[ "$bdf" == 0000:* ]] || bdf="0000:${bdf}"
|
||||||
|
grep -qxF "$bdf" "$PROXMENUX_VFIO_BIND_STATE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
_proxmenux_vfio_bind_has_entries() {
|
||||||
|
[[ -s "$PROXMENUX_VFIO_BIND_STATE" ]] \
|
||||||
|
&& grep -qEv '^[[:space:]]*(#|$)' "$PROXMENUX_VFIO_BIND_STATE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
_proxmenux_vfio_bind_state_has_vendor() {
|
||||||
|
local target_vendor="${1,,}"
|
||||||
|
[[ -n "$target_vendor" && -f "$PROXMENUX_VFIO_BIND_STATE" ]] || return 1
|
||||||
|
|
||||||
|
local bdf full vendor_hex
|
||||||
|
while IFS= read -r bdf; do
|
||||||
|
[[ -z "$bdf" || "$bdf" == \#* ]] && continue
|
||||||
|
full="$bdf"
|
||||||
|
[[ "$full" == 0000:* ]] || full="0000:${full}"
|
||||||
|
vendor_hex=$(cat "${PROXMENUX_SYSFS_ROOT}/bus/pci/devices/${full}/vendor" 2>/dev/null \
|
||||||
|
| sed 's/^0x//' | tr '[:upper:]' '[:lower:]')
|
||||||
|
[[ "$vendor_hex" == "$target_vendor" ]] && return 0
|
||||||
|
done < "$PROXMENUX_VFIO_BIND_STATE"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
_proxmenux_vfio_bind_add_bdfs() {
|
_proxmenux_vfio_bind_add_bdfs() {
|
||||||
# Args: any number of BDFs ("01:00.0" or "0000:01:00.0")
|
# Args: any number of BDFs ("01:00.0" or "0000:01:00.0")
|
||||||
mkdir -p "$(dirname "$PROXMENUX_VFIO_BIND_STATE")"
|
mkdir -p "$(dirname "$PROXMENUX_VFIO_BIND_STATE")"
|
||||||
@@ -450,8 +491,8 @@ _proxmenux_vfio_bind_add_bdfs() {
|
|||||||
done
|
done
|
||||||
if $changed; then
|
if $changed; then
|
||||||
_proxmenux_vfio_bind_write_udev_rule
|
_proxmenux_vfio_bind_write_udev_rule
|
||||||
_proxmenux_nvidia_vfio_blacklist_sync || true
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
[[ -n "${HOST_CONFIG_CHANGED+x}" ]] && HOST_CONFIG_CHANGED=true
|
_proxmenux_mark_host_config_changed
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,10 +516,10 @@ _proxmenux_vfio_bind_remove_bdfs() {
|
|||||||
if ! cmp -s "$tmp" "$PROXMENUX_VFIO_BIND_STATE"; then
|
if ! cmp -s "$tmp" "$PROXMENUX_VFIO_BIND_STATE"; then
|
||||||
mv "$tmp" "$PROXMENUX_VFIO_BIND_STATE"
|
mv "$tmp" "$PROXMENUX_VFIO_BIND_STATE"
|
||||||
_proxmenux_vfio_bind_write_udev_rule
|
_proxmenux_vfio_bind_write_udev_rule
|
||||||
_proxmenux_nvidia_vfio_blacklist_sync || true
|
|
||||||
[[ -n "${HOST_CONFIG_CHANGED+x}" ]] && HOST_CONFIG_CHANGED=true
|
|
||||||
# If empty, remove state file too (keeps host clean)
|
# If empty, remove state file too (keeps host clean)
|
||||||
[[ ! -s "$PROXMENUX_VFIO_BIND_STATE" ]] && rm -f "$PROXMENUX_VFIO_BIND_STATE"
|
[[ ! -s "$PROXMENUX_VFIO_BIND_STATE" ]] && rm -f "$PROXMENUX_VFIO_BIND_STATE"
|
||||||
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
|
_proxmenux_mark_host_config_changed
|
||||||
else
|
else
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
fi
|
fi
|
||||||
@@ -490,9 +531,10 @@ _proxmenux_vfio_bind_remove_bdfs() {
|
|||||||
# or whether the host still needs the nvidia driver loaded for at
|
# or whether the host still needs the nvidia driver loaded for at
|
||||||
# least one GPU (multi-GPU mixed case).
|
# least one GPU (multi-GPU mixed case).
|
||||||
_proxmenux_all_nvidia_in_vfio() {
|
_proxmenux_all_nvidia_in_vfio() {
|
||||||
local -a host_nvidia=() vfio_nvidia=()
|
local -a host_nvidia=()
|
||||||
local d cls vendor
|
local d cls vendor bdf
|
||||||
for d in /sys/bus/pci/devices/*; do
|
for d in "${PROXMENUX_SYSFS_ROOT}/bus/pci/devices/"*; do
|
||||||
|
[[ -d "$d" ]] || continue
|
||||||
vendor=$(cat "$d/vendor" 2>/dev/null)
|
vendor=$(cat "$d/vendor" 2>/dev/null)
|
||||||
[[ "$vendor" != "0x10de" ]] && continue
|
[[ "$vendor" != "0x10de" ]] && continue
|
||||||
cls=$(cat "$d/class" 2>/dev/null)
|
cls=$(cat "$d/class" 2>/dev/null)
|
||||||
@@ -502,23 +544,10 @@ _proxmenux_all_nvidia_in_vfio() {
|
|||||||
done
|
done
|
||||||
(( ${#host_nvidia[@]} == 0 )) && return 1
|
(( ${#host_nvidia[@]} == 0 )) && return 1
|
||||||
|
|
||||||
if [[ -f "$PROXMENUX_VFIO_BIND_STATE" ]]; then
|
for bdf in "${host_nvidia[@]}"; do
|
||||||
local bdf full
|
_proxmenux_vfio_bind_has_bdf "$bdf" || return 1
|
||||||
while IFS= read -r bdf; do
|
done
|
||||||
[[ -z "$bdf" ]] && continue
|
return 0
|
||||||
case "$bdf" in \#*) continue ;; esac
|
|
||||||
full="$bdf"
|
|
||||||
[[ "$full" != 0000:* ]] && full="0000:${full}"
|
|
||||||
vendor=$(cat "/sys/bus/pci/devices/${full}/vendor" 2>/dev/null)
|
|
||||||
[[ "$vendor" != "0x10de" ]] && continue
|
|
||||||
cls=$(cat "/sys/bus/pci/devices/${full}/class" 2>/dev/null)
|
|
||||||
case "$cls" in
|
|
||||||
0x0300*|0x0302*) vfio_nvidia+=("$full") ;;
|
|
||||||
esac
|
|
||||||
done < "$PROXMENUX_VFIO_BIND_STATE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
(( ${#vfio_nvidia[@]} >= ${#host_nvidia[@]} ))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply or remove the auto-managed nvidia blacklist + the nvidia-smi
|
# Apply or remove the auto-managed nvidia blacklist + the nvidia-smi
|
||||||
@@ -526,11 +555,12 @@ _proxmenux_all_nvidia_in_vfio() {
|
|||||||
# passthrough. Returns 0 if anything changed (caller may want to
|
# passthrough. Returns 0 if anything changed (caller may want to
|
||||||
# rebuild initramfs).
|
# rebuild initramfs).
|
||||||
_proxmenux_nvidia_vfio_blacklist_sync() {
|
_proxmenux_nvidia_vfio_blacklist_sync() {
|
||||||
local nvidia_udev_rule="/etc/udev/rules.d/70-nvidia.rules"
|
local nvidia_udev_rule="${PROXMENUX_ETC_ROOT}/udev/rules.d/70-nvidia.rules"
|
||||||
local changed=1
|
local changed=1
|
||||||
|
|
||||||
if _proxmenux_all_nvidia_in_vfio; then
|
if _proxmenux_all_nvidia_in_vfio; then
|
||||||
if [[ ! -f "$PROXMENUX_NVIDIA_VFIO_BLACKLIST" ]]; then
|
if [[ ! -f "$PROXMENUX_NVIDIA_VFIO_BLACKLIST" ]]; then
|
||||||
|
mkdir -p "$(dirname "$PROXMENUX_NVIDIA_VFIO_BLACKLIST")"
|
||||||
cat > "$PROXMENUX_NVIDIA_VFIO_BLACKLIST" <<'EOF'
|
cat > "$PROXMENUX_NVIDIA_VFIO_BLACKLIST" <<'EOF'
|
||||||
# ProxMenux: every NVIDIA GPU on this host is in VFIO passthrough.
|
# ProxMenux: every NVIDIA GPU on this host is in VFIO passthrough.
|
||||||
# Block the nvidia module so it doesn't loop trying to claim devices
|
# Block the nvidia module so it doesn't loop trying to claim devices
|
||||||
@@ -567,55 +597,179 @@ EOF
|
|||||||
return $changed
|
return $changed
|
||||||
}
|
}
|
||||||
|
|
||||||
# Returns the BDF of a PCI bridge sharing the IOMMU group of $1, if any.
|
_proxmenux_nvidia_vfio_softdeps_sync() {
|
||||||
# The kernel refuses to bind vfio-pci to root ports, so when a GPU shares
|
local changed=1
|
||||||
# its IOMMU group with the upstream root port the VFIO setup silently
|
mkdir -p "$(dirname "$PROXMENUX_VFIO_CONF")"
|
||||||
# does nothing — the GPU keeps its native driver and the host can also
|
touch "$PROXMENUX_VFIO_CONF"
|
||||||
# end up with a stuck boot if other devices behind the bridge were
|
|
||||||
# expected to come up under the original driver. Detecting this lets
|
local -a softdeps=(
|
||||||
# callers warn the operator and bail out before writing host config.
|
"softdep nvidia pre: vfio-pci"
|
||||||
_proxmenux_vfio_bind_group_bridge() {
|
"softdep nvidia_drm pre: vfio-pci"
|
||||||
local target="$1"
|
"softdep nvidia_modeset pre: vfio-pci"
|
||||||
[[ "$target" != 0000:* ]] && target="0000:${target}"
|
"softdep nvidia_uvm pre: vfio-pci"
|
||||||
local group_link
|
)
|
||||||
group_link=$(readlink "/sys/bus/pci/devices/${target}/iommu_group" 2>/dev/null) || return 1
|
local line
|
||||||
local group_num
|
if _proxmenux_vfio_bind_state_has_vendor "10de"; then
|
||||||
group_num=$(basename "$group_link")
|
for line in "${softdeps[@]}"; do
|
||||||
local member bdf cls
|
if ! grep -qFx "$line" "$PROXMENUX_VFIO_CONF" 2>/dev/null; then
|
||||||
for member in "/sys/kernel/iommu_groups/${group_num}/devices/"*; do
|
echo "$line" >> "$PROXMENUX_VFIO_CONF"
|
||||||
bdf=$(basename "$member")
|
changed=0
|
||||||
[[ "$bdf" == "$target" ]] && continue
|
fi
|
||||||
cls=$(cat "$member/class" 2>/dev/null)
|
done
|
||||||
# PCI bridge class is 0x0604xx (Normal bridge 0x060400, Subtractive 0x060401).
|
else
|
||||||
if [[ "$cls" == 0x0604* ]]; then
|
for line in "${softdeps[@]}"; do
|
||||||
echo "$bdf"
|
if grep -qFx "$line" "$PROXMENUX_VFIO_CONF" 2>/dev/null; then
|
||||||
return 0
|
sed -i "\|^${line}$|d" "$PROXMENUX_VFIO_CONF"
|
||||||
fi
|
changed=0
|
||||||
done
|
fi
|
||||||
return 1
|
done
|
||||||
|
fi
|
||||||
|
return $changed
|
||||||
}
|
}
|
||||||
|
|
||||||
_proxmenux_vfio_bind_purge_vendor() {
|
# NVIDIA services are host-wide. They must only be stopped when every
|
||||||
# Removes every BDF from the binder state whose PCI vendor matches $1
|
# NVIDIA display controller is assigned to VFIO; on a mixed host they stay
|
||||||
# (hex, e.g. "10de" for NVIDIA, "1002" for AMD, "8086" for Intel).
|
# available for the GPU(s) that remain native. The first transition stores
|
||||||
# Used by switch_gpu_mode to drop all NVIDIA bindings when reverting
|
# the previous service state and later transitions do not overwrite it.
|
||||||
# NVIDIA passthrough — the nvidia module reclaims the GPUs after the
|
_proxmenux_nvidia_host_services_sync() {
|
||||||
# next reboot.
|
command -v systemctl >/dev/null 2>&1 || return 1
|
||||||
local target_vendor="${1,,}"
|
|
||||||
[[ -z "$target_vendor" || ! -f "$PROXMENUX_VFIO_BIND_STATE" ]] && return 0
|
|
||||||
|
|
||||||
local -a to_remove=()
|
local changed=1 svc was_enabled was_active enabled active
|
||||||
local bdf vendor_hex
|
local -a services=(
|
||||||
while IFS= read -r bdf; do
|
"nvidia-persistenced.service"
|
||||||
[[ -z "$bdf" ]] && continue
|
"nvidia-powerd.service"
|
||||||
case "$bdf" in \#*) continue ;; esac
|
"nvidia-fabricmanager.service"
|
||||||
local full="$bdf"
|
)
|
||||||
[[ "$full" != 0000:* ]] && full="0000:${full}"
|
|
||||||
vendor_hex=$(cat "/sys/bus/pci/devices/${full}/vendor" 2>/dev/null | sed 's/^0x//' | tr '[:upper:]' '[:lower:]')
|
|
||||||
[[ "$vendor_hex" == "$target_vendor" ]] && to_remove+=("$full")
|
|
||||||
done < "$PROXMENUX_VFIO_BIND_STATE"
|
|
||||||
|
|
||||||
[[ ${#to_remove[@]} -gt 0 ]] && _proxmenux_vfio_bind_remove_bdfs "${to_remove[@]}"
|
if [[ "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE" != "$PROXMENUX_NVIDIA_SERVICE_STATE" \
|
||||||
|
&& -f "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE" ]]; then
|
||||||
|
mkdir -p "$(dirname "$PROXMENUX_NVIDIA_SERVICE_STATE")"
|
||||||
|
if [[ ! -f "$PROXMENUX_NVIDIA_SERVICE_STATE" ]]; then
|
||||||
|
mv "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE" \
|
||||||
|
"$PROXMENUX_NVIDIA_SERVICE_STATE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
rm -f "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE"
|
||||||
|
fi
|
||||||
|
rmdir "$(dirname "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE")" \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _proxmenux_all_nvidia_in_vfio; then
|
||||||
|
mkdir -p "$(dirname "$PROXMENUX_NVIDIA_SERVICE_STATE")"
|
||||||
|
if [[ ! -f "$PROXMENUX_NVIDIA_SERVICE_STATE" ]]; then
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
for svc in "${services[@]}"; do
|
||||||
|
was_enabled=0
|
||||||
|
was_active=0
|
||||||
|
systemctl is-enabled --quiet "$svc" 2>/dev/null && was_enabled=1
|
||||||
|
systemctl is-active --quiet "$svc" 2>/dev/null && was_active=1
|
||||||
|
if (( was_enabled == 1 || was_active == 1 )); then
|
||||||
|
echo "${svc} enabled=${was_enabled} active=${was_active}" >> "$tmp"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -s "$tmp" ]]; then
|
||||||
|
mv "$tmp" "$PROXMENUX_NVIDIA_SERVICE_STATE"
|
||||||
|
else
|
||||||
|
rm -f "$tmp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for svc in "${services[@]}"; do
|
||||||
|
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||||
|
systemctl stop "$svc" >/dev/null 2>&1 || true
|
||||||
|
changed=0
|
||||||
|
fi
|
||||||
|
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
||||||
|
systemctl disable "$svc" >/dev/null 2>&1 || true
|
||||||
|
changed=0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
elif [[ -f "$PROXMENUX_NVIDIA_SERVICE_STATE" ]]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
svc=${line%% *}
|
||||||
|
enabled=$(printf '%s\n' "$line" | sed -nE 's/.*enabled=([01]).*/\1/p')
|
||||||
|
active=$(printf '%s\n' "$line" | sed -nE 's/.*active=([01]).*/\1/p')
|
||||||
|
[[ "$enabled" == "1" ]] && systemctl enable "$svc" >/dev/null 2>&1 || true
|
||||||
|
[[ "$active" == "1" ]] && systemctl start "$svc" >/dev/null 2>&1 || true
|
||||||
|
done < "$PROXMENUX_NVIDIA_SERVICE_STATE"
|
||||||
|
rm -f "$PROXMENUX_NVIDIA_SERVICE_STATE"
|
||||||
|
changed=0
|
||||||
|
fi
|
||||||
|
return $changed
|
||||||
|
}
|
||||||
|
|
||||||
|
_proxmenux_nvidia_component_status_sync() {
|
||||||
|
declare -F update_component_status >/dev/null 2>&1 || return 1
|
||||||
|
|
||||||
|
local status_file="${BASE_DIR:-/usr/local/share/proxmenux}/components_status.json"
|
||||||
|
local version="" status="installed" patched=false metadata='{"patched":false}'
|
||||||
|
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||||
|
version=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null \
|
||||||
|
| head -1 | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
if [[ -z "$version" && -f "$status_file" ]] && command -v jq >/dev/null 2>&1; then
|
||||||
|
version=$(jq -r '.nvidia_driver.version // ""' "$status_file" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if [[ -f "$status_file" ]] && command -v jq >/dev/null 2>&1; then
|
||||||
|
patched=$(jq -r '.nvidia_driver.patched // false' "$status_file" 2>/dev/null)
|
||||||
|
[[ "$patched" == "true" ]] && metadata='{"patched":true}'
|
||||||
|
fi
|
||||||
|
_proxmenux_all_nvidia_in_vfio && status="vfio_passthrough"
|
||||||
|
update_component_status "nvidia_driver" "$status" "$version" "gpu" \
|
||||||
|
"$metadata" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
_proxmenux_nvidia_vfio_policy_sync() {
|
||||||
|
local changed=1
|
||||||
|
_proxmenux_nvidia_vfio_blacklist_sync && changed=0
|
||||||
|
_proxmenux_nvidia_vfio_softdeps_sync && changed=0
|
||||||
|
_proxmenux_nvidia_host_services_sync && changed=0
|
||||||
|
_proxmenux_nvidia_component_status_sync || true
|
||||||
|
(( changed == 0 )) && _proxmenux_mark_host_config_changed
|
||||||
|
return $changed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert legacy vendor:device NVIDIA entries into exact BDF entries before
|
||||||
|
# removing the old IDs. This preserves the previous host state even when two
|
||||||
|
# GPUs share the same model/PCI ID, while allowing subsequent selective
|
||||||
|
# restore of one GPU without releasing the others.
|
||||||
|
_proxmenux_vfio_bind_migrate_legacy_nvidia_ids() {
|
||||||
|
[[ -f "$PROXMENUX_VFIO_CONF" ]] || return 1
|
||||||
|
local ids_part
|
||||||
|
ids_part=$(grep '^options vfio-pci ids=' "$PROXMENUX_VFIO_CONF" 2>/dev/null \
|
||||||
|
| head -1 | grep -oE 'ids=[^[:space:]]+' | sed 's/^ids=//' | tr '[:upper:]' '[:lower:]')
|
||||||
|
[[ -n "$ids_part" ]] || return 1
|
||||||
|
|
||||||
|
local -a ids=() matched_ids=() bdfs=()
|
||||||
|
IFS=',' read -ra ids <<< "$ids_part"
|
||||||
|
local path vendor device class token existing
|
||||||
|
for path in "${PROXMENUX_SYSFS_ROOT}/bus/pci/devices/"*; do
|
||||||
|
[[ -d "$path" ]] || continue
|
||||||
|
vendor=$(cat "$path/vendor" 2>/dev/null | sed 's/^0x//' | tr '[:upper:]' '[:lower:]')
|
||||||
|
[[ "$vendor" == "10de" ]] || continue
|
||||||
|
class=$(cat "$path/class" 2>/dev/null)
|
||||||
|
[[ "$class" == 0x0600* || "$class" == 0x0604* ]] && continue
|
||||||
|
device=$(cat "$path/device" 2>/dev/null | sed 's/^0x//' | tr '[:upper:]' '[:lower:]')
|
||||||
|
token="${vendor}:${device}"
|
||||||
|
for existing in "${ids[@]}"; do
|
||||||
|
[[ "$existing" == "$token" ]] || continue
|
||||||
|
bdfs+=("$(basename "$path")")
|
||||||
|
if [[ " ${matched_ids[*]} " != *" ${token} "* ]]; then
|
||||||
|
matched_ids+=("$token")
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
done
|
||||||
|
done
|
||||||
|
(( ${#matched_ids[@]} > 0 )) || return 1
|
||||||
|
|
||||||
|
(( ${#bdfs[@]} > 0 )) && _proxmenux_vfio_bind_add_bdfs "${bdfs[@]}"
|
||||||
|
if _clean_vfio_conf_ids "${matched_ids[@]}"; then
|
||||||
|
_proxmenux_mark_host_config_changed
|
||||||
|
fi
|
||||||
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -626,12 +780,12 @@ _proxmenux_vfio_bind_purge_vendor() {
|
|||||||
# ──────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
_proxmenux_nvidia_migrate_legacy_blacklist() {
|
_proxmenux_nvidia_migrate_legacy_blacklist() {
|
||||||
local changed=false
|
local changed=false
|
||||||
local blacklist_file="/etc/modprobe.d/blacklist.conf"
|
local blacklist_file="${PROXMENUX_ETC_ROOT}/modprobe.d/blacklist.conf"
|
||||||
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
|
local nvidia_blacklist="${PROXMENUX_ETC_ROOT}/modprobe.d/nvidia-blacklist.conf"
|
||||||
local udev_disabled="/etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled"
|
local udev_disabled="${PROXMENUX_ETC_ROOT}/udev/rules.d/70-nvidia.rules.proxmenux-disabled"
|
||||||
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
|
local udev_rules="${PROXMENUX_ETC_ROOT}/udev/rules.d/70-nvidia.rules"
|
||||||
local modules_load_disabled="/etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio"
|
local modules_load_disabled="${PROXMENUX_ETC_ROOT}/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio"
|
||||||
local modules_load_active="/etc/modules-load.d/nvidia-vfio.conf"
|
local modules_load_active="${PROXMENUX_ETC_ROOT}/modules-load.d/nvidia-vfio.conf"
|
||||||
|
|
||||||
if [[ -f "$blacklist_file" ]] && grep -qE '^blacklist (nvidia|nvidia_drm|nvidia_modeset|nvidia_uvm|nvidiafb)$' "$blacklist_file"; then
|
if [[ -f "$blacklist_file" ]] && grep -qE '^blacklist (nvidia|nvidia_drm|nvidia_modeset|nvidia_uvm|nvidiafb)$' "$blacklist_file"; then
|
||||||
sed -i \
|
sed -i \
|
||||||
@@ -660,8 +814,14 @@ _proxmenux_nvidia_migrate_legacy_blacklist() {
|
|||||||
changed=true
|
changed=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if _proxmenux_vfio_bind_migrate_legacy_nvidia_ids; then
|
||||||
|
changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
|
|
||||||
if $changed; then
|
if $changed; then
|
||||||
[[ -n "${HOST_CONFIG_CHANGED+x}" ]] && HOST_CONFIG_CHANGED=true
|
_proxmenux_mark_host_config_changed
|
||||||
if declare -F msg_ok >/dev/null 2>&1; then
|
if declare -F msg_ok >/dev/null 2>&1; then
|
||||||
msg_ok "$(declare -F translate >/dev/null 2>&1 && translate 'Migrated legacy ProxMenux NVIDIA blacklist state — module will reload after reboot' || echo 'Migrated legacy ProxMenux NVIDIA blacklist state — module will reload after reboot')"
|
msg_ok "$(declare -F translate >/dev/null 2>&1 && translate 'Migrated legacy ProxMenux NVIDIA blacklist state — module will reload after reboot' || echo 'Migrated legacy ProxMenux NVIDIA blacklist state — module will reload after reboot')"
|
||||||
else
|
else
|
||||||
@@ -676,7 +836,7 @@ _pci_driver_of() {
|
|||||||
[[ -z "$pci" ]] && return
|
[[ -z "$pci" ]] && return
|
||||||
local pci_full="$pci"
|
local pci_full="$pci"
|
||||||
[[ "$pci_full" != 0000:* ]] && pci_full="0000:${pci_full}"
|
[[ "$pci_full" != 0000:* ]] && pci_full="0000:${pci_full}"
|
||||||
local link="/sys/bus/pci/devices/${pci_full}/driver"
|
local link="${PROXMENUX_SYSFS_ROOT}/bus/pci/devices/${pci_full}/driver"
|
||||||
[[ -L "$link" ]] && basename "$(readlink "$link")"
|
[[ -L "$link" ]] && basename "$(readlink "$link")"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,7 +844,7 @@ _pci_driver_of() {
|
|||||||
# /etc/modprobe.d/vfio.conf. Preserves any remaining tokens and any
|
# /etc/modprobe.d/vfio.conf. Preserves any remaining tokens and any
|
||||||
# trailing options on the line. Returns 0 when the file changes.
|
# trailing options on the line. Returns 0 when the file changes.
|
||||||
_clean_vfio_conf_ids() {
|
_clean_vfio_conf_ids() {
|
||||||
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
local vfio_conf="$PROXMENUX_VFIO_CONF"
|
||||||
[[ ! -f "$vfio_conf" ]] && return 1
|
[[ ! -f "$vfio_conf" ]] && return 1
|
||||||
local -a targets=("$@")
|
local -a targets=("$@")
|
||||||
[[ ${#targets[@]} -eq 0 ]] && return 1
|
[[ ${#targets[@]} -eq 0 ]] && return 1
|
||||||
@@ -694,7 +854,7 @@ _clean_vfio_conf_ids() {
|
|||||||
awk -v targets="${targets[*]}" '
|
awk -v targets="${targets[*]}" '
|
||||||
BEGIN {
|
BEGIN {
|
||||||
n = split(targets, a, " ")
|
n = split(targets, a, " ")
|
||||||
for (i = 1; i <= n; i++) drop[a[i]] = 1
|
for (i = 1; i <= n; i++) drop[tolower(a[i])] = 1
|
||||||
}
|
}
|
||||||
/^options vfio-pci ids=/ {
|
/^options vfio-pci ids=/ {
|
||||||
pre = ""; ids = ""; post = ""
|
pre = ""; ids = ""; post = ""
|
||||||
@@ -707,7 +867,7 @@ _clean_vfio_conf_ids() {
|
|||||||
out = ""
|
out = ""
|
||||||
for (i = 1; i <= m; i++) {
|
for (i = 1; i <= m; i++) {
|
||||||
t = tok[i]
|
t = tok[i]
|
||||||
if (!(t in drop)) {
|
if (!(tolower(t) in drop)) {
|
||||||
out = (out == "" ? t : out "," t)
|
out = (out == "" ? t : out "," t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ update_pve_safe() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── 10. Final cleanup ──
|
# ── 10. Final cleanup ──
|
||||||
|
msg_info "$(translate "Running cleanup")"
|
||||||
apt-get -y autoremove >/dev/null 2>&1 || true
|
apt-get -y autoremove >/dev/null 2>&1 || true
|
||||||
apt-get -y autoclean >/dev/null 2>&1 || true
|
apt-get -y autoclean >/dev/null 2>&1 || true
|
||||||
msg_ok "$(translate "Cleanup finished")"
|
msg_ok "$(translate "Cleanup finished")"
|
||||||
|
|||||||
+50
-110
@@ -5,8 +5,8 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# Version : 1.0
|
# Version : 1.1
|
||||||
# Last Updated: 03/04/2026
|
# Last Updated: 26/08/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# Automates full GPU passthrough (VFIO) from Proxmox host to a VM.
|
# Automates full GPU passthrough (VFIO) from Proxmox host to a VM.
|
||||||
@@ -323,25 +323,40 @@ evaluate_host_reboot_requirement() {
|
|||||||
_file_has_exact_line "$mod" "$modules_file" || needs_change=true
|
_file_has_exact_line "$mod" "$modules_file" || needs_change=true
|
||||||
done
|
done
|
||||||
|
|
||||||
# vfio-pci ids
|
# VFIO ownership. NVIDIA uses exact BDFs so another GPU with the same
|
||||||
|
# vendor:device ID can remain native; AMD/Intel keep the legacy IDs list.
|
||||||
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
||||||
local ids_line ids_part
|
local ids_line ids_part
|
||||||
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
|
if [[ "$SELECTED_GPU" == "nvidia" ]]; then
|
||||||
if [[ -z "$ids_line" ]]; then
|
local required_bdf
|
||||||
needs_change=true
|
for required_bdf in "${IOMMU_DEVICES[@]}"; do
|
||||||
else
|
if ! declare -F _proxmenux_vfio_bind_has_bdf >/dev/null 2>&1 \
|
||||||
[[ "$ids_line" == *"disable_vga=1"* ]] || needs_change=true
|
|| ! _proxmenux_vfio_bind_has_bdf "$required_bdf"; then
|
||||||
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//')
|
needs_change=true
|
||||||
local existing_ids=()
|
fi
|
||||||
IFS=',' read -ra existing_ids <<< "$ids_part"
|
|
||||||
local required found existing
|
|
||||||
for required in "${IOMMU_VFIO_IDS[@]}"; do
|
|
||||||
found=false
|
|
||||||
for existing in "${existing_ids[@]}"; do
|
|
||||||
[[ "$existing" == "$required" ]] && found=true && break
|
|
||||||
done
|
|
||||||
$found || needs_change=true
|
|
||||||
done
|
done
|
||||||
|
_file_has_exact_line "softdep nvidia pre: vfio-pci" "$vfio_conf" || needs_change=true
|
||||||
|
_file_has_exact_line "softdep nvidia_drm pre: vfio-pci" "$vfio_conf" || needs_change=true
|
||||||
|
_file_has_exact_line "softdep nvidia_modeset pre: vfio-pci" "$vfio_conf" || needs_change=true
|
||||||
|
_file_has_exact_line "softdep nvidia_uvm pre: vfio-pci" "$vfio_conf" || needs_change=true
|
||||||
|
else
|
||||||
|
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
|
||||||
|
if [[ -z "$ids_line" ]]; then
|
||||||
|
needs_change=true
|
||||||
|
else
|
||||||
|
[[ "$ids_line" == *"disable_vga=1"* ]] || needs_change=true
|
||||||
|
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//')
|
||||||
|
local existing_ids=()
|
||||||
|
IFS=',' read -ra existing_ids <<< "$ids_part"
|
||||||
|
local required found existing
|
||||||
|
for required in "${IOMMU_VFIO_IDS[@]}"; do
|
||||||
|
found=false
|
||||||
|
for existing in "${existing_ids[@]}"; do
|
||||||
|
[[ "$existing" == "$required" ]] && found=true && break
|
||||||
|
done
|
||||||
|
$found || needs_change=true
|
||||||
|
done
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# modprobe options files
|
# modprobe options files
|
||||||
@@ -362,21 +377,16 @@ evaluate_host_reboot_requirement() {
|
|||||||
case "$SELECTED_GPU" in
|
case "$SELECTED_GPU" in
|
||||||
nvidia)
|
nvidia)
|
||||||
_file_has_exact_line "blacklist nouveau" "$blacklist_file" || needs_change=true
|
_file_has_exact_line "blacklist nouveau" "$blacklist_file" || needs_change=true
|
||||||
_file_has_exact_line "blacklist nvidia" "$blacklist_file" || needs_change=true
|
|
||||||
_file_has_exact_line "blacklist nvidia_drm" "$blacklist_file" || needs_change=true
|
|
||||||
_file_has_exact_line "blacklist nvidia_modeset" "$blacklist_file" || needs_change=true
|
|
||||||
_file_has_exact_line "blacklist nvidia_uvm" "$blacklist_file" || needs_change=true
|
|
||||||
_file_has_exact_line "blacklist nvidiafb" "$blacklist_file" || needs_change=true
|
|
||||||
_file_has_exact_line "blacklist lbm-nouveau" "$blacklist_file" || needs_change=true
|
_file_has_exact_line "blacklist lbm-nouveau" "$blacklist_file" || needs_change=true
|
||||||
_file_has_exact_line "options nouveau modeset=0" "$blacklist_file" || needs_change=true
|
_file_has_exact_line "options nouveau modeset=0" "$blacklist_file" || needs_change=true
|
||||||
[[ -f /etc/modules-load.d/nvidia-vfio.conf ]] && needs_change=true
|
# The managed global NVIDIA blacklist is required only when
|
||||||
grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null && needs_change=true
|
# every NVIDIA GPU is in VFIO. On a mixed host it must be absent.
|
||||||
local svc
|
if declare -F _proxmenux_all_nvidia_in_vfio >/dev/null 2>&1 \
|
||||||
for svc in nvidia-persistenced.service nvidia-persistenced nvidia-powerd.service nvidia-fabricmanager.service; do
|
&& _proxmenux_all_nvidia_in_vfio; then
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null || systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
[[ -f /etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf ]] || needs_change=true
|
||||||
needs_change=true
|
else
|
||||||
fi
|
[[ -f /etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf ]] && needs_change=true
|
||||||
done
|
fi
|
||||||
;;
|
;;
|
||||||
amd)
|
amd)
|
||||||
_file_has_exact_line "blacklist radeon" "$blacklist_file" || needs_change=true
|
_file_has_exact_line "blacklist radeon" "$blacklist_file" || needs_change=true
|
||||||
@@ -1611,8 +1621,8 @@ configure_vfio_pci_ids() {
|
|||||||
# NVIDIA: per-BDF binding (multi-GPU safe). The `options vfio-pci
|
# NVIDIA: per-BDF binding (multi-GPU safe). The `options vfio-pci
|
||||||
# ids=VENDOR:DEVICE` approach captures EVERY GPU with the same
|
# ids=VENDOR:DEVICE` approach captures EVERY GPU with the same
|
||||||
# vendor:device ID — fatal when two NVIDIA GPUs share a model.
|
# vendor:device ID — fatal when two NVIDIA GPUs share a model.
|
||||||
# Instead, we list the exact BDF(s) of the target GPU in the
|
# Instead, we list the exact BDF(s) of the target GPU in an early
|
||||||
# initramfs hook, and add `softdep nvidia pre: vfio-pci` so vfio
|
# udev driver_override rule, and add `softdep nvidia pre: vfio-pci` so vfio
|
||||||
# has a chance to claim the BDF before nvidia loads.
|
# has a chance to claim the BDF before nvidia loads.
|
||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
if [[ "$SELECTED_GPU" == "nvidia" ]]; then
|
if [[ "$SELECTED_GPU" == "nvidia" ]]; then
|
||||||
@@ -1649,7 +1659,7 @@ configure_vfio_pci_ids() {
|
|||||||
_add_line_if_missing "softdep nvidia_modeset pre: vfio-pci" "$vfio_conf"
|
_add_line_if_missing "softdep nvidia_modeset pre: vfio-pci" "$vfio_conf"
|
||||||
_add_line_if_missing "softdep nvidia_uvm pre: vfio-pci" "$vfio_conf"
|
_add_line_if_missing "softdep nvidia_uvm pre: vfio-pci" "$vfio_conf"
|
||||||
|
|
||||||
# Per-BDF binder hook. IOMMU_DEVICES has the BDFs for the GPU
|
# Per-BDF binder rule. IOMMU_DEVICES has the BDFs for the GPU
|
||||||
# we're passing (and any same-group functions like the audio
|
# we're passing (and any same-group functions like the audio
|
||||||
# function). Add all of them so the whole IOMMU group goes to
|
# function). Add all of them so the whole IOMMU group goes to
|
||||||
# vfio-pci as Proxmox expects.
|
# vfio-pci as Proxmox expects.
|
||||||
@@ -1755,85 +1765,15 @@ blacklist_gpu_drivers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sanitize_nvidia_host_stack_for_vfio() {
|
sanitize_nvidia_host_stack_for_vfio() {
|
||||||
# In the new per-BDF model we only stop systemd services that could
|
# Host-wide NVIDIA services and module blacklisting are derived from
|
||||||
# actively probe / lock GPUs at boot (persistenced) — but we DO NOT:
|
# the complete per-BDF state. With two NVIDIA GPUs, assigning only one
|
||||||
# - blacklist the nvidia kernel module
|
# to a VM keeps the native driver and services available for the other.
|
||||||
# - remove nvidia entries from /etc/modules
|
|
||||||
# - rename /etc/modules-load.d/nvidia-vfio.conf
|
|
||||||
# - rename /etc/udev/rules.d/70-nvidia.rules
|
|
||||||
# - create /etc/modprobe.d/nvidia-blacklist.conf with install /bin/false
|
|
||||||
# All of those were global and broke multi-GPU NVIDIA scenarios where
|
|
||||||
# one GPU goes to a VM (vfio-pci) and another stays on the host
|
|
||||||
# (nvidia driver). VFIO binding is now per-BDF via driver_override in
|
|
||||||
# an initramfs hook — the nvidia module stays usable for any GPU not
|
|
||||||
# explicitly targeted.
|
|
||||||
msg_info "$(translate 'Sanitizing NVIDIA host services for VFIO mode...')"
|
msg_info "$(translate 'Sanitizing NVIDIA host services for VFIO mode...')"
|
||||||
local changed=false
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
local state_dir="/var/lib/proxmenux"
|
if _proxmenux_all_nvidia_in_vfio; then
|
||||||
local state_file="${state_dir}/nvidia-host-services.state"
|
|
||||||
local svc
|
|
||||||
local -a services=(
|
|
||||||
"nvidia-persistenced.service"
|
|
||||||
"nvidia-powerd.service"
|
|
||||||
"nvidia-fabricmanager.service"
|
|
||||||
)
|
|
||||||
|
|
||||||
mkdir -p "$state_dir" >/dev/null 2>&1 || true
|
|
||||||
: > "$state_file"
|
|
||||||
|
|
||||||
for svc in "${services[@]}"; do
|
|
||||||
local was_enabled=0 was_active=0
|
|
||||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
|
||||||
was_enabled=1
|
|
||||||
fi
|
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
|
||||||
was_active=1
|
|
||||||
fi
|
|
||||||
if (( was_enabled == 1 || was_active == 1 )); then
|
|
||||||
echo "${svc} enabled=${was_enabled} active=${was_active}" >>"$state_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
|
||||||
systemctl stop "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
|
||||||
systemctl disable "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -s "$state_file" ]] || rm -f "$state_file"
|
|
||||||
|
|
||||||
if $changed; then
|
|
||||||
HOST_CONFIG_CHANGED=true
|
|
||||||
msg_ok "$(translate 'NVIDIA host services disabled for VFIO mode')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'NVIDIA host services disabled for VFIO mode')" | tee -a "$screen_capture"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate 'NVIDIA host services already aligned for VFIO mode')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for native mode')" | tee -a "$screen_capture"
|
||||||
fi
|
|
||||||
|
|
||||||
# Sync components_status.json — the host driver stays on disk but is
|
|
||||||
# not in use for this GPU because it now belongs to a VM. Per-BDF
|
|
||||||
# model: on multi-GPU hosts where another NVIDIA card still uses the
|
|
||||||
# nvidia driver, keep the status as "installed" — the driver is
|
|
||||||
# genuinely in use elsewhere. Only flip to "vfio_passthrough" when no
|
|
||||||
# NVIDIA GPU is bound to the host driver anymore.
|
|
||||||
if declare -F update_component_status >/dev/null 2>&1; then
|
|
||||||
local _nvd_ver _nvd_new_status
|
|
||||||
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
|
|
||||||
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
|
|
||||||
_nvd_new_status="vfio_passthrough"
|
|
||||||
# Any NVIDIA PCI device still using the nvidia driver on the host?
|
|
||||||
if lspci -nnk 2>/dev/null | awk '
|
|
||||||
/NVIDIA/{gpu=1; next}
|
|
||||||
gpu && /Kernel driver in use: nvidia$/ {found=1; exit}
|
|
||||||
/^[^\t]/{gpu=0}
|
|
||||||
END{exit !found}
|
|
||||||
'; then
|
|
||||||
_nvd_new_status="installed"
|
|
||||||
fi
|
|
||||||
update_component_status "nvidia_driver" "$_nvd_new_status" \
|
|
||||||
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,18 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# Version : 1.2
|
# Version : 1.3
|
||||||
# Last Updated: 26/03/2026
|
# Last Updated: 26/08/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# Installs and manages the NVIDIA proprietary driver on a
|
# Installs and manages the NVIDIA proprietary driver on a
|
||||||
# Proxmox VE host. Detects hardware, picks a kernel-compatible
|
# Proxmox VE host. Detects hardware, filters NVIDIA branches by
|
||||||
# driver version and handles the full lifecycle
|
# the installed GPU PCI IDs and handles the full lifecycle
|
||||||
# (install / update / remove).
|
# (install / update / remove).
|
||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - GPU detection + VFIO passthrough safety check
|
# - GPU detection + VFIO passthrough safety check
|
||||||
# - Kernel-aware driver version filter (5.15 → 6.17+)
|
# - GPU PCI-ID-aware branch filtering from NVIDIA supportedchips
|
||||||
# - Nouveau blacklist + module unload
|
# - Nouveau blacklist + module unload
|
||||||
# - DKMS-backed install (survives kernel upgrades)
|
# - DKMS-backed install (survives kernel upgrades)
|
||||||
# - udev rules + nvidia-persistenced service
|
# - udev rules + nvidia-persistenced service
|
||||||
@@ -36,6 +36,10 @@ screen_capture="/tmp/proxmenux_nvidia_screen_capture_$$.txt"
|
|||||||
|
|
||||||
NVIDIA_BASE_URL="https://download.nvidia.com/XFree86/Linux-x86_64"
|
NVIDIA_BASE_URL="https://download.nvidia.com/XFree86/Linux-x86_64"
|
||||||
NVIDIA_WORKDIR="/opt/nvidia"
|
NVIDIA_WORKDIR="/opt/nvidia"
|
||||||
|
NVIDIA_NOUVEAU_BLACKLIST="/etc/modprobe.d/proxmenux-nouveau-blacklist.conf"
|
||||||
|
NVIDIA_NOUVEAU_STATE="${BASE_DIR}/nvidia-nouveau-blacklist.state"
|
||||||
|
NVIDIA_NOUVEAU_LEGACY_BLACKLIST="/etc/modprobe.d/nouveau-blacklist.conf"
|
||||||
|
NVIDIA_GLOBAL_BLACKLIST="/etc/modprobe.d/blacklist.conf"
|
||||||
|
|
||||||
# LXC post-install update constants (used only when NVIDIA LXC passthrough
|
# LXC post-install update constants (used only when NVIDIA LXC passthrough
|
||||||
# containers are detected and the user confirms updating them after the host
|
# containers are detected and the user confirms updating them after the host
|
||||||
@@ -541,16 +545,65 @@ ensure_repos_and_headers() {
|
|||||||
msg_ok "$(translate 'Kernel headers and build tools verified.')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'Kernel headers and build tools verified.')" | tee -a "$screen_capture"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_nouveau_legacy_file_is_proxmenux_shape() {
|
||||||
|
[[ -f "$NVIDIA_NOUVEAU_LEGACY_BLACKLIST" ]] || return 1
|
||||||
|
local content
|
||||||
|
content=$(sed '/^[[:space:]]*$/d' "$NVIDIA_NOUVEAU_LEGACY_BLACKLIST" 2>/dev/null)
|
||||||
|
[[ "$content" == $'blacklist nouveau\noptions nouveau modeset=0' ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
_nouveau_state_set() {
|
||||||
|
local key="$1"
|
||||||
|
mkdir -p "$(dirname "$NVIDIA_NOUVEAU_STATE")"
|
||||||
|
touch "$NVIDIA_NOUVEAU_STATE"
|
||||||
|
grep -qFx "${key}=1" "$NVIDIA_NOUVEAU_STATE" 2>/dev/null \
|
||||||
|
|| echo "${key}=1" >> "$NVIDIA_NOUVEAU_STATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_nouveau_after_uninstall() {
|
||||||
|
local remove_global_line=false
|
||||||
|
|
||||||
|
if [[ -f "$NVIDIA_NOUVEAU_STATE" ]] \
|
||||||
|
&& grep -qFx 'blacklist_conf_line_added=1' "$NVIDIA_NOUVEAU_STATE" 2>/dev/null; then
|
||||||
|
remove_global_line=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Migration for installations made by older ProxMenux versions. That
|
||||||
|
# version overwrote this exact two-line file and added the matching line
|
||||||
|
# to blacklist.conf, but had no ownership state yet.
|
||||||
|
if _nouveau_legacy_file_is_proxmenux_shape; then
|
||||||
|
rm -f "$NVIDIA_NOUVEAU_LEGACY_BLACKLIST"
|
||||||
|
remove_global_line=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$NVIDIA_NOUVEAU_BLACKLIST"
|
||||||
|
if $remove_global_line && [[ -f "$NVIDIA_GLOBAL_BLACKLIST" ]]; then
|
||||||
|
sed -i '/^blacklist nouveau$/d' "$NVIDIA_GLOBAL_BLACKLIST"
|
||||||
|
fi
|
||||||
|
rm -f "$NVIDIA_NOUVEAU_STATE"
|
||||||
|
}
|
||||||
|
|
||||||
blacklist_nouveau() {
|
blacklist_nouveau() {
|
||||||
msg_info "$(translate 'Blacklisting nouveau driver...')"
|
msg_info "$(translate 'Blacklisting nouveau driver...')"
|
||||||
|
|
||||||
# Write blacklist config files
|
local legacy_owned=false
|
||||||
if ! grep -q '^blacklist nouveau' /etc/modprobe.d/blacklist.conf 2>/dev/null; then
|
if _nouveau_legacy_file_is_proxmenux_shape; then
|
||||||
echo "blacklist nouveau" >> /etc/modprobe.d/blacklist.conf
|
rm -f "$NVIDIA_NOUVEAU_LEGACY_BLACKLIST"
|
||||||
|
legacy_owned=true
|
||||||
|
_nouveau_state_set "legacy_migrated"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Also write explicit options file to ensure it's fully disabled
|
if ! grep -q '^blacklist nouveau$' "$NVIDIA_GLOBAL_BLACKLIST" 2>/dev/null; then
|
||||||
cat > /etc/modprobe.d/nouveau-blacklist.conf <<'EOF'
|
echo "blacklist nouveau" >> "$NVIDIA_GLOBAL_BLACKLIST"
|
||||||
|
_nouveau_state_set "blacklist_conf_line_added"
|
||||||
|
elif $legacy_owned; then
|
||||||
|
# The legacy ProxMenux file proves ownership of the companion line.
|
||||||
|
_nouveau_state_set "blacklist_conf_line_added"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ProxMenux-owned file: uninstall can now remove only what we created.
|
||||||
|
cat > "$NVIDIA_NOUVEAU_BLACKLIST" <<'EOF'
|
||||||
|
# Managed by ProxMenux NVIDIA installer.
|
||||||
blacklist nouveau
|
blacklist nouveau
|
||||||
options nouveau modeset=0
|
options nouveau modeset=0
|
||||||
EOF
|
EOF
|
||||||
@@ -678,6 +731,7 @@ complete_nvidia_uninstall() {
|
|||||||
rm -f /etc/udev/rules.d/70-nvidia.rules
|
rm -f /etc/udev/rules.d/70-nvidia.rules
|
||||||
rm -rf /usr/lib/modprobe.d/nvidia*.conf
|
rm -rf /usr/lib/modprobe.d/nvidia*.conf
|
||||||
rm -rf /etc/modprobe.d/nvidia*.conf
|
rm -rf /etc/modprobe.d/nvidia*.conf
|
||||||
|
restore_nouveau_after_uninstall
|
||||||
|
|
||||||
if [[ -d "$NVIDIA_WORKDIR" ]]; then
|
if [[ -d "$NVIDIA_WORKDIR" ]]; then
|
||||||
find "$NVIDIA_WORKDIR" -type d -name "nvidia-persistenced" -exec rm -rf {} + 2>/dev/null || true
|
find "$NVIDIA_WORKDIR" -type d -name "nvidia-persistenced" -exec rm -rf {} + 2>/dev/null || true
|
||||||
@@ -709,28 +763,14 @@ ensure_workdir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Kernel + system detection
|
# System detection
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
get_kernel_compatibility_info() {
|
get_system_info() {
|
||||||
local kernel_version
|
|
||||||
kernel_version=$(uname -r)
|
|
||||||
|
|
||||||
if [[ -f /etc/pve/.version ]]; then
|
if [[ -f /etc/pve/.version ]]; then
|
||||||
PVE_VERSION=$(cat /etc/pve/.version)
|
PVE_VERSION=$(cat /etc/pve/.version)
|
||||||
else
|
else
|
||||||
PVE_VERSION="unknown"
|
PVE_VERSION="unknown"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
KERNEL_MAJOR=$(echo "$kernel_version" | cut -d. -f1)
|
|
||||||
KERNEL_MINOR=$(echo "$kernel_version" | cut -d. -f2)
|
|
||||||
|
|
||||||
MIN_DRIVER_VERSION=""
|
|
||||||
RECOMMENDED_BRANCH=""
|
|
||||||
COMPATIBILITY_NOTE=""
|
|
||||||
}
|
|
||||||
|
|
||||||
is_version_compatible() {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1546,7 +1586,7 @@ show_version_menu() {
|
|||||||
|
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "$(translate 'NVIDIA GPU Driver Installation')"
|
msg_title "$(translate 'NVIDIA GPU Driver Installation')"
|
||||||
msg_info "$(translate 'Fetching compatible driver versions for your kernel and GPU...')"
|
msg_info "$(translate 'Fetching NVIDIA driver versions supported by your GPU...')"
|
||||||
|
|
||||||
latest=$(download_latest_version 2>/dev/null)
|
latest=$(download_latest_version 2>/dev/null)
|
||||||
versions_list=$(list_available_versions 2>/dev/null)
|
versions_list=$(list_available_versions 2>/dev/null)
|
||||||
@@ -1573,18 +1613,6 @@ show_version_menu() {
|
|||||||
latest=$(echo "$latest" | tr -d '[:space:]')
|
latest=$(echo "$latest" | tr -d '[:space:]')
|
||||||
|
|
||||||
local current_list="$versions_list"
|
local current_list="$versions_list"
|
||||||
|
|
||||||
# Apply kernel compatibility filter if needed
|
|
||||||
if [[ -n "$MIN_DRIVER_VERSION" ]]; then
|
|
||||||
local filtered_list=""
|
|
||||||
while IFS= read -r ver; do
|
|
||||||
[[ -z "$ver" ]] && continue
|
|
||||||
if is_version_compatible "$ver"; then
|
|
||||||
filtered_list+="$ver"$'\n'
|
|
||||||
fi
|
|
||||||
done <<< "$current_list"
|
|
||||||
current_list="$filtered_list"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$current_list" ]]; then
|
if [[ -n "$current_list" ]]; then
|
||||||
current_list=$(filter_option_c_branch "$current_list" "$CURRENT_DRIVER_VERSION" "")
|
current_list=$(filter_option_c_branch "$current_list" "$CURRENT_DRIVER_VERSION" "")
|
||||||
@@ -1636,7 +1664,7 @@ show_version_menu() {
|
|||||||
# 2. Fresh install (no current driver) → Production Branch head
|
# 2. Fresh install (no current driver) → Production Branch head
|
||||||
# from NVIDIA's Unix drivers page, when present in the list.
|
# from NVIDIA's Unix drivers page, when present in the list.
|
||||||
# 3. Fallback → highest numeric in the list (Production may have
|
# 3. Fallback → highest numeric in the list (Production may have
|
||||||
# been filtered out by kernel-compat / GPU-compat / patch
|
# been filtered out by maintained-branch / GPU PCI-ID / patch
|
||||||
# awareness).
|
# awareness).
|
||||||
latest=""
|
latest=""
|
||||||
if [[ -n "$CURRENT_DRIVER_VERSION" && -n "$current_list" ]]; then
|
if [[ -n "$CURRENT_DRIVER_VERSION" && -n "$current_list" ]]; then
|
||||||
@@ -1665,7 +1693,7 @@ show_version_menu() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local menu_text="$(translate 'Select the NVIDIA driver version to install:')\n\n"
|
local menu_text="$(translate 'Select the NVIDIA driver version to install:')\n\n"
|
||||||
menu_text+="$(translate 'Versions shown are compatible with your kernel and your GPU. The recommended version keeps you on your current driver branch, or defaults to the NVIDIA Production Branch head on a fresh install.')"
|
menu_text+="$(translate 'Versions shown belong to maintained NVIDIA branches that list your GPU PCI ID. DKMS compilation is the final validation against the running kernel. The recommended version keeps the current branch, or uses the NVIDIA Production Branch on a fresh install.')"
|
||||||
if $patch_filtered; then
|
if $patch_filtered; then
|
||||||
menu_text+="\n\n$(translate 'NVENC patch detected — list narrowed to versions supported by keylase/nvidia-patch.')"
|
menu_text+="\n\n$(translate 'NVENC patch detected — list narrowed to versions supported by keylase/nvidia-patch.')"
|
||||||
elif [[ -n "$patch_filter_note" ]]; then
|
elif [[ -n "$patch_filter_note" ]]; then
|
||||||
@@ -1689,7 +1717,7 @@ show_version_menu() {
|
|||||||
choices+=("$ver" "$ver")
|
choices+=("$ver" "$ver")
|
||||||
done <<< "$current_list"
|
done <<< "$current_list"
|
||||||
else
|
else
|
||||||
choices+=("" "$(translate 'No compatible versions found for your kernel')")
|
choices+=("" "$(translate 'No supported NVIDIA versions found for this GPU')")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
stop_spinner
|
stop_spinner
|
||||||
@@ -1741,7 +1769,7 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
get_kernel_compatibility_info
|
get_system_info
|
||||||
|
|
||||||
show_version_menu
|
show_version_menu
|
||||||
if [[ "$DRIVER_VERSION" == "cancel" || -z "$DRIVER_VERSION" ]]; then
|
if [[ "$DRIVER_VERSION" == "cancel" || -z "$DRIVER_VERSION" ]]; then
|
||||||
@@ -2008,4 +2036,4 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
main
|
main
|
||||||
fi
|
fi
|
||||||
|
|||||||
+100
-208
@@ -5,8 +5,8 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# Version : 1.0
|
# Version : 1.1
|
||||||
# Last Updated: 05/04/2026
|
# Last Updated: 26/08/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# Moves an already-assigned GPU between the two modes it can
|
# Moves an already-assigned GPU between the two modes it can
|
||||||
@@ -67,7 +67,8 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
|||||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
||||||
source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh"
|
source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh"
|
||||||
else
|
else
|
||||||
msg_warn "$(translate 'pci_passthrough_helpers.sh missing — SR-IOV / orphan-audio guards will be skipped')"
|
echo "ProxMenux: pci_passthrough_helpers.sh is required; refusing to change GPU ownership." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
@@ -81,6 +82,8 @@ declare -a SELECTED_GPU_IDX=()
|
|||||||
|
|
||||||
declare -a SELECTED_IOMMU_IDS=()
|
declare -a SELECTED_IOMMU_IDS=()
|
||||||
declare -a SELECTED_PCI_SLOTS=()
|
declare -a SELECTED_PCI_SLOTS=()
|
||||||
|
declare -a SELECTED_NVIDIA_BDFS=()
|
||||||
|
declare -a SELECTED_LEGACY_IOMMU_IDS=()
|
||||||
|
|
||||||
declare -a LXC_AFFECTED_CTIDS=()
|
declare -a LXC_AFFECTED_CTIDS=()
|
||||||
declare -a LXC_AFFECTED_NAMES=()
|
declare -a LXC_AFFECTED_NAMES=()
|
||||||
@@ -167,12 +170,30 @@ _get_iommu_group_ids() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_get_iommu_group_bdfs() {
|
||||||
|
local pci_full="$1"
|
||||||
|
local group_link="/sys/bus/pci/devices/${pci_full}/iommu_group"
|
||||||
|
[[ -L "$group_link" ]] || return 0
|
||||||
|
|
||||||
|
local group_dir dev_path dev_class
|
||||||
|
group_dir="/sys/kernel/iommu_groups/$(basename "$(readlink "$group_link")")/devices"
|
||||||
|
for dev_path in "${group_dir}/"*; do
|
||||||
|
[[ -e "$dev_path" ]] || continue
|
||||||
|
dev_class=$(cat "$dev_path/class" 2>/dev/null)
|
||||||
|
# Bridges belong to the isolation boundary, but vfio-pci does not
|
||||||
|
# support PCI bridges. Proxmox passes the endpoint devices only.
|
||||||
|
[[ "$dev_class" == 0x0604* || "$dev_class" == 0x0600* ]] && continue
|
||||||
|
basename "$dev_path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
_read_vfio_ids() {
|
_read_vfio_ids() {
|
||||||
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
||||||
local ids_line ids_part
|
local ids_line ids_part
|
||||||
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
|
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
|
||||||
[[ -z "$ids_line" ]] && return
|
[[ -z "$ids_line" ]] && return
|
||||||
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//')
|
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//' \
|
||||||
|
| tr '[:upper:]' '[:lower:]')
|
||||||
[[ -z "$ids_part" ]] && return
|
[[ -z "$ids_part" ]] && return
|
||||||
tr ',' '\n' <<< "$ids_part" | sed '/^$/d'
|
tr ',' '\n' <<< "$ids_part" | sed '/^$/d'
|
||||||
}
|
}
|
||||||
@@ -213,15 +234,10 @@ _remove_gpu_blacklist() {
|
|||||||
local changed=false
|
local changed=false
|
||||||
case "$gpu_type" in
|
case "$gpu_type" in
|
||||||
nvidia)
|
nvidia)
|
||||||
grep -qE '^blacklist (nouveau|nvidia|nvidiafb|nvidia_drm|nvidia_modeset|nvidia_uvm|lbm-nouveau)$|^options nouveau modeset=0$' "$blacklist_file" 2>/dev/null && changed=true
|
# NVIDIA ownership is per BDF. Never alter the global blacklist here:
|
||||||
sed -i '/^blacklist nouveau$/d' "$blacklist_file"
|
# it may belong to the host-driver installer and another NVIDIA GPU may
|
||||||
sed -i '/^blacklist nvidia$/d' "$blacklist_file"
|
# still need the native driver.
|
||||||
sed -i '/^blacklist nvidiafb$/d' "$blacklist_file"
|
return 1
|
||||||
sed -i '/^blacklist nvidia_drm$/d' "$blacklist_file"
|
|
||||||
sed -i '/^blacklist nvidia_modeset$/d' "$blacklist_file"
|
|
||||||
sed -i '/^blacklist nvidia_uvm$/d' "$blacklist_file"
|
|
||||||
sed -i '/^blacklist lbm-nouveau$/d' "$blacklist_file"
|
|
||||||
sed -i '/^options nouveau modeset=0$/d' "$blacklist_file"
|
|
||||||
;;
|
;;
|
||||||
amd)
|
amd)
|
||||||
grep -qE '^blacklist (radeon|amdgpu)$' "$blacklist_file" 2>/dev/null && changed=true
|
grep -qE '^blacklist (radeon|amdgpu)$' "$blacklist_file" 2>/dev/null && changed=true
|
||||||
@@ -243,14 +259,8 @@ _add_gpu_blacklist() {
|
|||||||
touch "$blacklist_file"
|
touch "$blacklist_file"
|
||||||
case "$gpu_type" in
|
case "$gpu_type" in
|
||||||
nvidia)
|
nvidia)
|
||||||
_add_line_if_missing "blacklist nouveau" "$blacklist_file"
|
# NVIDIA is handled exclusively by the shared per-BDF policy.
|
||||||
_add_line_if_missing "blacklist nvidia" "$blacklist_file"
|
return 0
|
||||||
_add_line_if_missing "blacklist nvidiafb" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist nvidia_drm" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist nvidia_modeset" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist nvidia_uvm" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist lbm-nouveau" "$blacklist_file"
|
|
||||||
_add_line_if_missing "options nouveau modeset=0" "$blacklist_file"
|
|
||||||
;;
|
;;
|
||||||
amd)
|
amd)
|
||||||
_add_line_if_missing "blacklist radeon" "$blacklist_file"
|
_add_line_if_missing "blacklist radeon" "$blacklist_file"
|
||||||
@@ -263,174 +273,18 @@ _add_gpu_blacklist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_sanitize_nvidia_host_stack_for_vfio() {
|
_sanitize_nvidia_host_stack_for_vfio() {
|
||||||
local changed=false
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
local state_dir="/var/lib/proxmenux"
|
|
||||||
local state_file="${state_dir}/nvidia-host-services.state"
|
|
||||||
local svc
|
|
||||||
local -a services=(
|
|
||||||
"nvidia-persistenced.service"
|
|
||||||
"nvidia-powerd.service"
|
|
||||||
"nvidia-fabricmanager.service"
|
|
||||||
)
|
|
||||||
|
|
||||||
mkdir -p "$state_dir" >/dev/null 2>&1 || true
|
|
||||||
: > "$state_file"
|
|
||||||
|
|
||||||
for svc in "${services[@]}"; do
|
|
||||||
local was_enabled=0 was_active=0
|
|
||||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
|
||||||
was_enabled=1
|
|
||||||
fi
|
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
|
||||||
was_active=1
|
|
||||||
fi
|
|
||||||
if (( was_enabled == 1 || was_active == 1 )); then
|
|
||||||
echo "${svc} enabled=${was_enabled} active=${was_active}" >>"$state_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
|
||||||
systemctl stop "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
|
||||||
systemctl disable "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -s "$state_file" ]] || rm -f "$state_file"
|
|
||||||
|
|
||||||
if [[ -f /etc/modules-load.d/nvidia-vfio.conf ]]; then
|
|
||||||
mv /etc/modules-load.d/nvidia-vfio.conf /etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null; then
|
|
||||||
sed -i '/^nvidia$/d;/^nvidia_uvm$/d;/^nvidia_drm$/d;/^nvidia_modeset$/d' /etc/modules
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Disable NVIDIA udev rules that trigger nvidia-smi (causes conflict with vfio-pci)
|
|
||||||
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
|
|
||||||
if [[ -f "$udev_rules" ]]; then
|
|
||||||
mv "$udev_rules" "${udev_rules}.proxmenux-disabled" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
udevadm control --reload-rules >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create hard blacklist to prevent ANY nvidia module loading (even via modprobe/nvidia-smi)
|
|
||||||
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
|
|
||||||
if [[ ! -f "$nvidia_blacklist" ]]; then
|
|
||||||
cat > "$nvidia_blacklist" <<'EOF'
|
|
||||||
# ProxMenux: Hard blacklist to prevent ANY nvidia module loading in VFIO mode
|
|
||||||
# This prevents nvidia-smi and other tools from triggering module load attempts
|
|
||||||
install nvidia /bin/false
|
|
||||||
install nvidia_uvm /bin/false
|
|
||||||
install nvidia_drm /bin/false
|
|
||||||
install nvidia_modeset /bin/false
|
|
||||||
EOF
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $changed; then
|
|
||||||
HOST_CONFIG_CHANGED=true
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload disabled for VFIO mode')" | tee -a "$screen_capture"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for VFIO mode')" | tee -a "$screen_capture"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sync components_status.json — the host driver stays on disk but is
|
|
||||||
# not in use because the GPU now belongs to a VM. Prevents the update
|
|
||||||
# notification path (and any future logic gated on nvidia_driver.status)
|
|
||||||
# from acting on a state that no longer matches reality.
|
|
||||||
if declare -F update_component_status >/dev/null 2>&1; then
|
|
||||||
local _nvd_ver
|
|
||||||
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
|
|
||||||
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
|
|
||||||
update_component_status "nvidia_driver" "vfio_passthrough" \
|
|
||||||
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_restore_nvidia_host_stack_for_lxc() {
|
_restore_nvidia_host_stack_for_lxc() {
|
||||||
local changed=false
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
local state_file="/var/lib/proxmenux/nvidia-host-services.state"
|
if ! _proxmenux_all_nvidia_in_vfio; then
|
||||||
local disabled_file="/etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio"
|
modprobe nvidia >/dev/null 2>&1 || true
|
||||||
local active_file="/etc/modules-load.d/nvidia-vfio.conf"
|
modprobe nvidia_uvm >/dev/null 2>&1 || true
|
||||||
|
modprobe nvidia_modeset >/dev/null 2>&1 || true
|
||||||
# New per-BDF model: drop every NVIDIA BDF from the initramfs binder so
|
modprobe nvidia_drm >/dev/null 2>&1 || true
|
||||||
# the nvidia module reclaims the GPU after the next reboot. Idempotent:
|
|
||||||
# no-op if no NVIDIA BDFs are tracked. Vendor 10de = NVIDIA.
|
|
||||||
if declare -F _proxmenux_vfio_bind_purge_vendor >/dev/null 2>&1; then
|
|
||||||
_proxmenux_vfio_bind_purge_vendor "10de" && changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove hard blacklist that was preventing nvidia module loading
|
|
||||||
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
|
|
||||||
if [[ -f "$nvidia_blacklist" ]]; then
|
|
||||||
rm -f "$nvidia_blacklist" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore NVIDIA udev rules if they were disabled
|
|
||||||
local udev_disabled="/etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled"
|
|
||||||
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
|
|
||||||
if [[ -f "$udev_disabled" ]]; then
|
|
||||||
mv "$udev_disabled" "$udev_rules" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
udevadm control --reload-rules >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore previous modules-load policy if ProxMenux disabled it in VM mode.
|
|
||||||
if [[ -f "$disabled_file" ]]; then
|
|
||||||
mv "$disabled_file" "$active_file" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Best effort: load NVIDIA kernel modules now that we are back in native mode.
|
|
||||||
# If not installed, these calls simply fail silently.
|
|
||||||
modprobe nvidia >/dev/null 2>&1 || true
|
|
||||||
modprobe nvidia_uvm >/dev/null 2>&1 || true
|
|
||||||
modprobe nvidia_modeset >/dev/null 2>&1 || true
|
|
||||||
modprobe nvidia_drm >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
if [[ -f "$state_file" ]]; then
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ -z "$line" ]] && continue
|
|
||||||
local svc enabled active
|
|
||||||
svc=$(echo "$line" | awk '{print $1}')
|
|
||||||
enabled=$(echo "$line" | awk -F'enabled=' '{print $2}' | awk '{print $1}')
|
|
||||||
active=$(echo "$line" | awk -F'active=' '{print $2}' | awk '{print $1}')
|
|
||||||
[[ "$enabled" == "1" ]] && systemctl enable "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
[[ "$active" == "1" ]] && systemctl start "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
done <"$state_file"
|
|
||||||
rm -f "$state_file"
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $changed; then
|
|
||||||
HOST_CONFIG_CHANGED=true
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload restored for native mode')" | tee -a "$screen_capture"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for native mode')" | tee -a "$screen_capture"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sync components_status.json back to installed — the host has reclaimed
|
|
||||||
# the GPU and the nvidia stack is being reloaded. Restores the state to
|
|
||||||
# what it was before the VFIO switch so the update notification path and
|
|
||||||
# the auto-reinstall gate see the driver as active on the host again.
|
|
||||||
if declare -F update_component_status >/dev/null 2>&1; then
|
|
||||||
local _nvd_ver
|
|
||||||
_nvd_ver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1)
|
|
||||||
if [[ -z "$_nvd_ver" ]]; then
|
|
||||||
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
|
|
||||||
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
|
|
||||||
fi
|
|
||||||
update_component_status "nvidia_driver" "installed" \
|
|
||||||
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_add_amd_softdep() {
|
_add_amd_softdep() {
|
||||||
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
||||||
_add_line_if_missing "softdep radeon pre: vfio-pci" "$vfio_conf"
|
_add_line_if_missing "softdep radeon pre: vfio-pci" "$vfio_conf"
|
||||||
@@ -468,6 +322,10 @@ _remove_vfio_modules_if_unused() {
|
|||||||
local vfio_count
|
local vfio_count
|
||||||
vfio_count=$(_read_vfio_ids | wc -l | tr -d '[:space:]')
|
vfio_count=$(_read_vfio_ids | wc -l | tr -d '[:space:]')
|
||||||
[[ "$vfio_count" != "0" ]] && return 1
|
[[ "$vfio_count" != "0" ]] && return 1
|
||||||
|
if declare -F _proxmenux_vfio_bind_has_entries >/dev/null 2>&1 \
|
||||||
|
&& _proxmenux_vfio_bind_has_entries; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
local modules_file="/etc/modules"
|
local modules_file="/etc/modules"
|
||||||
[[ ! -f "$modules_file" ]] && return 1
|
[[ ! -f "$modules_file" ]] && return 1
|
||||||
local had_any=false
|
local had_any=false
|
||||||
@@ -811,25 +669,37 @@ check_sriov_and_block_if_needed() {
|
|||||||
collect_selected_iommu_ids() {
|
collect_selected_iommu_ids() {
|
||||||
SELECTED_IOMMU_IDS=()
|
SELECTED_IOMMU_IDS=()
|
||||||
SELECTED_PCI_SLOTS=()
|
SELECTED_PCI_SLOTS=()
|
||||||
|
SELECTED_NVIDIA_BDFS=()
|
||||||
|
SELECTED_LEGACY_IOMMU_IDS=()
|
||||||
|
|
||||||
local idx pci viddid slot
|
local idx pci viddid slot selected_type bdf vid did gid
|
||||||
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
||||||
pci="${ALL_GPU_PCIS[$idx]}"
|
pci="${ALL_GPU_PCIS[$idx]}"
|
||||||
viddid="${ALL_GPU_VIDDID[$idx]}"
|
viddid="${ALL_GPU_VIDDID[$idx]}"
|
||||||
|
selected_type="${ALL_GPU_TYPES[$idx]}"
|
||||||
slot="${pci#0000:}"
|
slot="${pci#0000:}"
|
||||||
slot="${slot%.*}"
|
slot="${slot%.*}"
|
||||||
SELECTED_PCI_SLOTS+=("$slot")
|
SELECTED_PCI_SLOTS+=("$slot")
|
||||||
|
|
||||||
local -a group_ids=()
|
local -a group_bdfs=()
|
||||||
mapfile -t group_ids < <(_get_iommu_group_ids "$pci")
|
mapfile -t group_bdfs < <(_get_iommu_group_bdfs "$pci")
|
||||||
if [[ ${#group_ids[@]} -gt 0 ]]; then
|
[[ ${#group_bdfs[@]} -gt 0 ]] || group_bdfs=("$pci")
|
||||||
local gid
|
|
||||||
for gid in "${group_ids[@]}"; do
|
for bdf in "${group_bdfs[@]}"; do
|
||||||
|
[[ "$bdf" == 0000:* ]] || bdf="0000:${bdf}"
|
||||||
|
vid=$(cat "/sys/bus/pci/devices/${bdf}/vendor" 2>/dev/null | sed 's/^0x//')
|
||||||
|
did=$(cat "/sys/bus/pci/devices/${bdf}/device" 2>/dev/null | sed 's/^0x//')
|
||||||
|
gid="${vid}:${did}"
|
||||||
|
if [[ -n "$vid" && -n "$did" ]]; then
|
||||||
_contains_in_array "$gid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$gid")
|
_contains_in_array "$gid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$gid")
|
||||||
done
|
if [[ "$selected_type" != "nvidia" ]]; then
|
||||||
elif [[ -n "$viddid" ]]; then
|
_contains_in_array "$gid" "${SELECTED_LEGACY_IOMMU_IDS[@]}" || SELECTED_LEGACY_IOMMU_IDS+=("$gid")
|
||||||
_contains_in_array "$viddid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$viddid")
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [[ "$selected_type" == "nvidia" ]]; then
|
||||||
|
_contains_in_array "$bdf" "${SELECTED_NVIDIA_BDFS[@]}" || SELECTED_NVIDIA_BDFS+=("$bdf")
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,6 +1037,9 @@ apply_vm_action_for_lxc_mode() {
|
|||||||
if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then
|
if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then
|
||||||
SELECTED_IOMMU_IDS+=("$_vd_id")
|
SELECTED_IOMMU_IDS+=("$_vd_id")
|
||||||
fi
|
fi
|
||||||
|
if ! _contains_in_array "$_vd_id" "${SELECTED_LEGACY_IOMMU_IDS[@]}"; then
|
||||||
|
SELECTED_LEGACY_IOMMU_IDS+=("$_vd_id")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1234,6 +1107,12 @@ switch_to_vm_mode() {
|
|||||||
|
|
||||||
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
||||||
|
|
||||||
|
local -a selected_types=()
|
||||||
|
mapfile -t selected_types < <(_selected_types_unique)
|
||||||
|
if _contains_in_array "nvidia" "${selected_types[@]}"; then
|
||||||
|
_proxmenux_nvidia_migrate_legacy_blacklist
|
||||||
|
fi
|
||||||
|
|
||||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
_register_iommu_tool
|
_register_iommu_tool
|
||||||
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
||||||
@@ -1269,24 +1148,30 @@ switch_to_vm_mode() {
|
|||||||
local -a current_ids=()
|
local -a current_ids=()
|
||||||
mapfile -t current_ids < <(_read_vfio_ids)
|
mapfile -t current_ids < <(_read_vfio_ids)
|
||||||
local id
|
local id
|
||||||
for id in "${SELECTED_IOMMU_IDS[@]}"; do
|
for id in "${SELECTED_LEGACY_IOMMU_IDS[@]}"; do
|
||||||
_contains_in_array "$id" "${current_ids[@]}" || current_ids+=("$id")
|
_contains_in_array "$id" "${current_ids[@]}" || current_ids+=("$id")
|
||||||
done
|
done
|
||||||
_write_vfio_ids "${current_ids[@]}"
|
_write_vfio_ids "${current_ids[@]}"
|
||||||
if [[ ${#SELECTED_IOMMU_IDS[@]} -gt 0 ]]; then
|
if [[ ${#SELECTED_LEGACY_IOMMU_IDS[@]} -gt 0 ]]; then
|
||||||
local ids_label
|
local ids_label
|
||||||
ids_label=$(IFS=','; echo "${SELECTED_IOMMU_IDS[*]}")
|
ids_label=$(IFS=','; echo "${SELECTED_LEGACY_IOMMU_IDS[*]}")
|
||||||
msg_ok "$(translate 'vfio-pci IDs configured') (${ids_label})" | tee -a "$screen_capture"
|
msg_ok "$(translate 'vfio-pci IDs configured') (${ids_label})" | tee -a "$screen_capture"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local -a selected_types=()
|
if [[ ${#SELECTED_NVIDIA_BDFS[@]} -gt 0 ]]; then
|
||||||
mapfile -t selected_types < <(_selected_types_unique)
|
_proxmenux_vfio_bind_add_bdfs "${SELECTED_NVIDIA_BDFS[@]}"
|
||||||
local t
|
msg_ok "$(translate 'NVIDIA per-BDF VFIO binding configured') (${SELECTED_NVIDIA_BDFS[*]})" | tee -a "$screen_capture"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local t legacy_blacklist_configured=false
|
||||||
for t in "${selected_types[@]}"; do
|
for t in "${selected_types[@]}"; do
|
||||||
|
[[ "$t" == "nvidia" ]] && continue
|
||||||
_add_gpu_blacklist "$t"
|
_add_gpu_blacklist "$t"
|
||||||
|
legacy_blacklist_configured=true
|
||||||
done
|
done
|
||||||
msg_ok "$(translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf')" | tee -a "$screen_capture"
|
$legacy_blacklist_configured \
|
||||||
_contains_in_array "nvidia" "${selected_types[@]}" && _sanitize_nvidia_host_stack_for_vfio
|
&& msg_ok "$(translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf')" | tee -a "$screen_capture"
|
||||||
|
_contains_in_array "nvidia" "${selected_types[@]}" && _proxmenux_nvidia_vfio_policy_sync || true
|
||||||
_contains_in_array "amd" "${selected_types[@]}" && _add_amd_softdep
|
_contains_in_array "amd" "${selected_types[@]}" && _add_amd_softdep
|
||||||
|
|
||||||
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
|
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
|
||||||
@@ -1320,12 +1205,20 @@ switch_to_lxc_mode() {
|
|||||||
|
|
||||||
msg_info "$(translate 'Removing VFIO ownership for selected GPU(s)...')"
|
msg_info "$(translate 'Removing VFIO ownership for selected GPU(s)...')"
|
||||||
|
|
||||||
|
local -a selected_types=()
|
||||||
|
mapfile -t selected_types < <(_selected_types_unique)
|
||||||
|
if _contains_in_array "nvidia" "${selected_types[@]}"; then
|
||||||
|
_proxmenux_nvidia_migrate_legacy_blacklist
|
||||||
|
[[ ${#SELECTED_NVIDIA_BDFS[@]} -gt 0 ]] \
|
||||||
|
&& _proxmenux_vfio_bind_remove_bdfs "${SELECTED_NVIDIA_BDFS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
local -a current_ids=() remaining_ids=() removed_ids=()
|
local -a current_ids=() remaining_ids=() removed_ids=()
|
||||||
mapfile -t current_ids < <(_read_vfio_ids)
|
mapfile -t current_ids < <(_read_vfio_ids)
|
||||||
local id remove
|
local id remove
|
||||||
for id in "${current_ids[@]}"; do
|
for id in "${current_ids[@]}"; do
|
||||||
remove=false
|
remove=false
|
||||||
_contains_in_array "$id" "${SELECTED_IOMMU_IDS[@]}" && remove=true
|
_contains_in_array "$id" "${SELECTED_LEGACY_IOMMU_IDS[@]}" && remove=true
|
||||||
if $remove; then
|
if $remove; then
|
||||||
removed_ids+=("$id")
|
removed_ids+=("$id")
|
||||||
else
|
else
|
||||||
@@ -1339,17 +1232,16 @@ switch_to_lxc_mode() {
|
|||||||
msg_ok "$(translate 'VFIO device IDs removed from /etc/modprobe.d/vfio.conf') (${ids_label})" | tee -a "$screen_capture"
|
msg_ok "$(translate 'VFIO device IDs removed from /etc/modprobe.d/vfio.conf') (${ids_label})" | tee -a "$screen_capture"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local -a selected_types=()
|
|
||||||
mapfile -t selected_types < <(_selected_types_unique)
|
|
||||||
local t
|
local t
|
||||||
for t in "${selected_types[@]}"; do
|
for t in "${selected_types[@]}"; do
|
||||||
|
if [[ "$t" == "nvidia" ]]; then
|
||||||
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if ! _type_has_remaining_vfio_ids "$t" "${remaining_ids[@]}"; then
|
if ! _type_has_remaining_vfio_ids "$t" "${remaining_ids[@]}"; then
|
||||||
if _remove_gpu_blacklist "$t"; then
|
if _remove_gpu_blacklist "$t"; then
|
||||||
msg_ok "$(translate 'Driver blacklist removed for') ${t}" | tee -a "$screen_capture"
|
msg_ok "$(translate 'Driver blacklist removed for') ${t}" | tee -a "$screen_capture"
|
||||||
fi
|
fi
|
||||||
if [[ "$t" == "nvidia" ]]; then
|
|
||||||
_restore_nvidia_host_stack_for_lxc
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# Author : MacRimi
|
# Author : MacRimi
|
||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# Version : 1.0
|
# Version : 1.1
|
||||||
# Last Updated: 09/04/2026
|
# Last Updated: 26/08/2026
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# This script is a hybrid version for ProxMenux Monitor.
|
# This script is a hybrid version for ProxMenux Monitor.
|
||||||
# It accepts parameters to skip GPU selection and uses
|
# It accepts parameters to skip GPU selection and uses
|
||||||
@@ -36,6 +36,9 @@ if [[ -f "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh" ]]; then
|
|||||||
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
source "$LOCAL_SCRIPTS_LOCAL/global/pci_passthrough_helpers.sh"
|
||||||
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
elif [[ -f "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh" ]]; then
|
||||||
source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh"
|
source "$LOCAL_SCRIPTS_DEFAULT/global/pci_passthrough_helpers.sh"
|
||||||
|
else
|
||||||
|
echo "ProxMenux: pci_passthrough_helpers.sh is required; refusing to change GPU ownership." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
@@ -52,6 +55,8 @@ declare -a SELECTED_GPU_IDX=()
|
|||||||
|
|
||||||
declare -a SELECTED_IOMMU_IDS=()
|
declare -a SELECTED_IOMMU_IDS=()
|
||||||
declare -a SELECTED_PCI_SLOTS=()
|
declare -a SELECTED_PCI_SLOTS=()
|
||||||
|
declare -a SELECTED_NVIDIA_BDFS=()
|
||||||
|
declare -a SELECTED_LEGACY_IOMMU_IDS=()
|
||||||
|
|
||||||
declare -a LXC_AFFECTED_CTIDS=()
|
declare -a LXC_AFFECTED_CTIDS=()
|
||||||
declare -a LXC_AFFECTED_NAMES=()
|
declare -a LXC_AFFECTED_NAMES=()
|
||||||
@@ -145,12 +150,28 @@ _get_iommu_group_ids() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_get_iommu_group_bdfs() {
|
||||||
|
local pci_full="$1"
|
||||||
|
local group_link="/sys/bus/pci/devices/${pci_full}/iommu_group"
|
||||||
|
[[ -L "$group_link" ]] || return 0
|
||||||
|
|
||||||
|
local group_dir dev_path dev_class
|
||||||
|
group_dir="/sys/kernel/iommu_groups/$(basename "$(readlink "$group_link")")/devices"
|
||||||
|
for dev_path in "${group_dir}/"*; do
|
||||||
|
[[ -e "$dev_path" ]] || continue
|
||||||
|
dev_class=$(cat "$dev_path/class" 2>/dev/null)
|
||||||
|
[[ "$dev_class" == 0x0604* || "$dev_class" == 0x0600* ]] && continue
|
||||||
|
basename "$dev_path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
_read_vfio_ids() {
|
_read_vfio_ids() {
|
||||||
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
||||||
local ids_line ids_part
|
local ids_line ids_part
|
||||||
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
|
ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1)
|
||||||
[[ -z "$ids_line" ]] && return
|
[[ -z "$ids_line" ]] && return
|
||||||
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//')
|
ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//' \
|
||||||
|
| tr '[:upper:]' '[:lower:]')
|
||||||
[[ -z "$ids_part" ]] && return
|
[[ -z "$ids_part" ]] && return
|
||||||
tr ',' '\n' <<< "$ids_part" | sed '/^$/d'
|
tr ',' '\n' <<< "$ids_part" | sed '/^$/d'
|
||||||
}
|
}
|
||||||
@@ -191,15 +212,10 @@ _remove_gpu_blacklist() {
|
|||||||
local changed=false
|
local changed=false
|
||||||
case "$gpu_type" in
|
case "$gpu_type" in
|
||||||
nvidia)
|
nvidia)
|
||||||
grep -qE '^blacklist (nouveau|nvidia|nvidiafb|nvidia_drm|nvidia_modeset|nvidia_uvm|lbm-nouveau)$|^options nouveau modeset=0$' "$blacklist_file" 2>/dev/null && changed=true
|
# NVIDIA ownership is per BDF. Never alter the global blacklist here:
|
||||||
sed -i '/^blacklist nouveau$/d' "$blacklist_file"
|
# it may belong to the host-driver installer and another NVIDIA GPU may
|
||||||
sed -i '/^blacklist nvidia$/d' "$blacklist_file"
|
# still need the native driver.
|
||||||
sed -i '/^blacklist nvidiafb$/d' "$blacklist_file"
|
return 1
|
||||||
sed -i '/^blacklist nvidia_drm$/d' "$blacklist_file"
|
|
||||||
sed -i '/^blacklist nvidia_modeset$/d' "$blacklist_file"
|
|
||||||
sed -i '/^blacklist nvidia_uvm$/d' "$blacklist_file"
|
|
||||||
sed -i '/^blacklist lbm-nouveau$/d' "$blacklist_file"
|
|
||||||
sed -i '/^options nouveau modeset=0$/d' "$blacklist_file"
|
|
||||||
;;
|
;;
|
||||||
amd)
|
amd)
|
||||||
grep -qE '^blacklist (radeon|amdgpu)$' "$blacklist_file" 2>/dev/null && changed=true
|
grep -qE '^blacklist (radeon|amdgpu)$' "$blacklist_file" 2>/dev/null && changed=true
|
||||||
@@ -221,14 +237,8 @@ _add_gpu_blacklist() {
|
|||||||
touch "$blacklist_file"
|
touch "$blacklist_file"
|
||||||
case "$gpu_type" in
|
case "$gpu_type" in
|
||||||
nvidia)
|
nvidia)
|
||||||
_add_line_if_missing "blacklist nouveau" "$blacklist_file"
|
# NVIDIA is handled exclusively by the shared per-BDF policy.
|
||||||
_add_line_if_missing "blacklist nvidia" "$blacklist_file"
|
return 0
|
||||||
_add_line_if_missing "blacklist nvidiafb" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist nvidia_drm" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist nvidia_modeset" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist nvidia_uvm" "$blacklist_file"
|
|
||||||
_add_line_if_missing "blacklist lbm-nouveau" "$blacklist_file"
|
|
||||||
_add_line_if_missing "options nouveau modeset=0" "$blacklist_file"
|
|
||||||
;;
|
;;
|
||||||
amd)
|
amd)
|
||||||
_add_line_if_missing "blacklist radeon" "$blacklist_file"
|
_add_line_if_missing "blacklist radeon" "$blacklist_file"
|
||||||
@@ -241,170 +251,18 @@ _add_gpu_blacklist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_sanitize_nvidia_host_stack_for_vfio() {
|
_sanitize_nvidia_host_stack_for_vfio() {
|
||||||
local changed=false
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
local state_dir="/var/lib/proxmenux"
|
|
||||||
local state_file="${state_dir}/nvidia-host-services.state"
|
|
||||||
local svc
|
|
||||||
local -a services=(
|
|
||||||
"nvidia-persistenced.service"
|
|
||||||
"nvidia-powerd.service"
|
|
||||||
"nvidia-fabricmanager.service"
|
|
||||||
)
|
|
||||||
|
|
||||||
mkdir -p "$state_dir" >/dev/null 2>&1 || true
|
|
||||||
: > "$state_file"
|
|
||||||
|
|
||||||
for svc in "${services[@]}"; do
|
|
||||||
local was_enabled=0 was_active=0
|
|
||||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
|
||||||
was_enabled=1
|
|
||||||
fi
|
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
|
||||||
was_active=1
|
|
||||||
fi
|
|
||||||
if (( was_enabled == 1 || was_active == 1 )); then
|
|
||||||
echo "${svc} enabled=${was_enabled} active=${was_active}" >>"$state_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
|
||||||
systemctl stop "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
|
||||||
systemctl disable "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -s "$state_file" ]] || rm -f "$state_file"
|
|
||||||
|
|
||||||
if [[ -f /etc/modules-load.d/nvidia-vfio.conf ]]; then
|
|
||||||
mv /etc/modules-load.d/nvidia-vfio.conf /etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qE '^(nvidia|nvidia_uvm|nvidia_drm|nvidia_modeset)$' /etc/modules 2>/dev/null; then
|
|
||||||
sed -i '/^nvidia$/d;/^nvidia_uvm$/d;/^nvidia_drm$/d;/^nvidia_modeset$/d' /etc/modules
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Disable NVIDIA udev rules that trigger nvidia-smi (causes conflict with vfio-pci)
|
|
||||||
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
|
|
||||||
if [[ -f "$udev_rules" ]]; then
|
|
||||||
mv "$udev_rules" "${udev_rules}.proxmenux-disabled" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
udevadm control --reload-rules >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create hard blacklist to prevent ANY nvidia module loading (even via modprobe/nvidia-smi)
|
|
||||||
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
|
|
||||||
if [[ ! -f "$nvidia_blacklist" ]]; then
|
|
||||||
cat > "$nvidia_blacklist" <<'EOF'
|
|
||||||
# ProxMenux: Hard blacklist to prevent ANY nvidia module loading in VFIO mode
|
|
||||||
# This prevents nvidia-smi and other tools from triggering module load attempts
|
|
||||||
install nvidia /bin/false
|
|
||||||
install nvidia_uvm /bin/false
|
|
||||||
install nvidia_drm /bin/false
|
|
||||||
install nvidia_modeset /bin/false
|
|
||||||
EOF
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $changed; then
|
|
||||||
HOST_CONFIG_CHANGED=true
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload disabled for VFIO mode')" | tee -a "$screen_capture"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for VFIO mode')" | tee -a "$screen_capture"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sync components_status.json — the host driver stays on disk but is
|
|
||||||
# not in use because the GPU now belongs to a VM. Prevents the update
|
|
||||||
# notification path (and any future logic gated on nvidia_driver.status)
|
|
||||||
# from acting on a state that no longer matches reality.
|
|
||||||
if declare -F update_component_status >/dev/null 2>&1; then
|
|
||||||
local _nvd_ver
|
|
||||||
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
|
|
||||||
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
|
|
||||||
update_component_status "nvidia_driver" "vfio_passthrough" \
|
|
||||||
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_restore_nvidia_host_stack_for_lxc() {
|
_restore_nvidia_host_stack_for_lxc() {
|
||||||
local changed=false
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
local state_file="/var/lib/proxmenux/nvidia-host-services.state"
|
if ! _proxmenux_all_nvidia_in_vfio; then
|
||||||
local disabled_file="/etc/modules-load.d/nvidia-vfio.conf.proxmenux-disabled-vfio"
|
modprobe nvidia >/dev/null 2>&1 || true
|
||||||
local active_file="/etc/modules-load.d/nvidia-vfio.conf"
|
modprobe nvidia_uvm >/dev/null 2>&1 || true
|
||||||
|
modprobe nvidia_modeset >/dev/null 2>&1 || true
|
||||||
# New per-BDF model: drop every NVIDIA BDF from the initramfs binder so
|
modprobe nvidia_drm >/dev/null 2>&1 || true
|
||||||
# the nvidia module reclaims the GPU after the next reboot. Idempotent.
|
|
||||||
if declare -F _proxmenux_vfio_bind_purge_vendor >/dev/null 2>&1; then
|
|
||||||
_proxmenux_vfio_bind_purge_vendor "10de" && changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove hard blacklist that was preventing nvidia module loading
|
|
||||||
local nvidia_blacklist="/etc/modprobe.d/nvidia-blacklist.conf"
|
|
||||||
if [[ -f "$nvidia_blacklist" ]]; then
|
|
||||||
rm -f "$nvidia_blacklist" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore NVIDIA udev rules if they were disabled
|
|
||||||
local udev_disabled="/etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled"
|
|
||||||
local udev_rules="/etc/udev/rules.d/70-nvidia.rules"
|
|
||||||
if [[ -f "$udev_disabled" ]]; then
|
|
||||||
mv "$udev_disabled" "$udev_rules" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
udevadm control --reload-rules >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$disabled_file" ]]; then
|
|
||||||
mv "$disabled_file" "$active_file" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
modprobe nvidia >/dev/null 2>&1 || true
|
|
||||||
modprobe nvidia_uvm >/dev/null 2>&1 || true
|
|
||||||
modprobe nvidia_modeset >/dev/null 2>&1 || true
|
|
||||||
modprobe nvidia_drm >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
if [[ -f "$state_file" ]]; then
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ -z "$line" ]] && continue
|
|
||||||
local svc enabled active
|
|
||||||
svc=$(echo "$line" | awk '{print $1}')
|
|
||||||
enabled=$(echo "$line" | awk -F'enabled=' '{print $2}' | awk '{print $1}')
|
|
||||||
active=$(echo "$line" | awk -F'active=' '{print $2}' | awk '{print $1}')
|
|
||||||
[[ "$enabled" == "1" ]] && systemctl enable "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
[[ "$active" == "1" ]] && systemctl start "$svc" >>"$LOG_FILE" 2>&1 || true
|
|
||||||
done <"$state_file"
|
|
||||||
rm -f "$state_file"
|
|
||||||
changed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $changed; then
|
|
||||||
HOST_CONFIG_CHANGED=true
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload restored for native mode')" | tee -a "$screen_capture"
|
|
||||||
else
|
|
||||||
msg_ok "$(translate 'NVIDIA host services/autoload already aligned for native mode')" | tee -a "$screen_capture"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sync components_status.json back to installed — the host has reclaimed
|
|
||||||
# the GPU and the nvidia stack is being reloaded. Restores the state to
|
|
||||||
# what it was before the VFIO switch so the update notification path and
|
|
||||||
# the auto-reinstall gate see the driver as active on the host again.
|
|
||||||
if declare -F update_component_status >/dev/null 2>&1; then
|
|
||||||
local _nvd_ver
|
|
||||||
_nvd_ver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1)
|
|
||||||
if [[ -z "$_nvd_ver" ]]; then
|
|
||||||
_nvd_ver=$(jq -r '.nvidia_driver.version // ""' \
|
|
||||||
/usr/local/share/proxmenux/components_status.json 2>/dev/null)
|
|
||||||
fi
|
|
||||||
update_component_status "nvidia_driver" "installed" \
|
|
||||||
"${_nvd_ver:-}" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 || true
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_add_amd_softdep() {
|
_add_amd_softdep() {
|
||||||
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
local vfio_conf="/etc/modprobe.d/vfio.conf"
|
||||||
_add_line_if_missing "softdep radeon pre: vfio-pci" "$vfio_conf"
|
_add_line_if_missing "softdep radeon pre: vfio-pci" "$vfio_conf"
|
||||||
@@ -442,6 +300,10 @@ _remove_vfio_modules_if_unused() {
|
|||||||
local vfio_count
|
local vfio_count
|
||||||
vfio_count=$(_read_vfio_ids | wc -l | tr -d '[:space:]')
|
vfio_count=$(_read_vfio_ids | wc -l | tr -d '[:space:]')
|
||||||
[[ "$vfio_count" != "0" ]] && return 1
|
[[ "$vfio_count" != "0" ]] && return 1
|
||||||
|
if declare -F _proxmenux_vfio_bind_has_entries >/dev/null 2>&1 \
|
||||||
|
&& _proxmenux_vfio_bind_has_entries; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
local modules_file="/etc/modules"
|
local modules_file="/etc/modules"
|
||||||
[[ ! -f "$modules_file" ]] && return 1
|
[[ ! -f "$modules_file" ]] && return 1
|
||||||
local had_any=false
|
local had_any=false
|
||||||
@@ -632,25 +494,37 @@ validate_vm_mode_blocked_ids() {
|
|||||||
collect_selected_iommu_ids() {
|
collect_selected_iommu_ids() {
|
||||||
SELECTED_IOMMU_IDS=()
|
SELECTED_IOMMU_IDS=()
|
||||||
SELECTED_PCI_SLOTS=()
|
SELECTED_PCI_SLOTS=()
|
||||||
|
SELECTED_NVIDIA_BDFS=()
|
||||||
|
SELECTED_LEGACY_IOMMU_IDS=()
|
||||||
|
|
||||||
local idx pci viddid slot
|
local idx pci viddid slot selected_type bdf vid did gid
|
||||||
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
for idx in "${SELECTED_GPU_IDX[@]}"; do
|
||||||
pci="${ALL_GPU_PCIS[$idx]}"
|
pci="${ALL_GPU_PCIS[$idx]}"
|
||||||
viddid="${ALL_GPU_VIDDID[$idx]}"
|
viddid="${ALL_GPU_VIDDID[$idx]}"
|
||||||
|
selected_type="${ALL_GPU_TYPES[$idx]}"
|
||||||
slot="${pci#0000:}"
|
slot="${pci#0000:}"
|
||||||
slot="${slot%.*}"
|
slot="${slot%.*}"
|
||||||
SELECTED_PCI_SLOTS+=("$slot")
|
SELECTED_PCI_SLOTS+=("$slot")
|
||||||
|
|
||||||
local -a group_ids=()
|
local -a group_bdfs=()
|
||||||
mapfile -t group_ids < <(_get_iommu_group_ids "$pci")
|
mapfile -t group_bdfs < <(_get_iommu_group_bdfs "$pci")
|
||||||
if [[ ${#group_ids[@]} -gt 0 ]]; then
|
[[ ${#group_bdfs[@]} -gt 0 ]] || group_bdfs=("$pci")
|
||||||
local gid
|
|
||||||
for gid in "${group_ids[@]}"; do
|
for bdf in "${group_bdfs[@]}"; do
|
||||||
|
[[ "$bdf" == 0000:* ]] || bdf="0000:${bdf}"
|
||||||
|
vid=$(cat "/sys/bus/pci/devices/${bdf}/vendor" 2>/dev/null | sed 's/^0x//')
|
||||||
|
did=$(cat "/sys/bus/pci/devices/${bdf}/device" 2>/dev/null | sed 's/^0x//')
|
||||||
|
gid="${vid}:${did}"
|
||||||
|
if [[ -n "$vid" && -n "$did" ]]; then
|
||||||
_contains_in_array "$gid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$gid")
|
_contains_in_array "$gid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$gid")
|
||||||
done
|
if [[ "$selected_type" != "nvidia" ]]; then
|
||||||
elif [[ -n "$viddid" ]]; then
|
_contains_in_array "$gid" "${SELECTED_LEGACY_IOMMU_IDS[@]}" || SELECTED_LEGACY_IOMMU_IDS+=("$gid")
|
||||||
_contains_in_array "$viddid" "${SELECTED_IOMMU_IDS[@]}" || SELECTED_IOMMU_IDS+=("$viddid")
|
fi
|
||||||
fi
|
fi
|
||||||
|
if [[ "$selected_type" == "nvidia" ]]; then
|
||||||
|
_contains_in_array "$bdf" "${SELECTED_NVIDIA_BDFS[@]}" || SELECTED_NVIDIA_BDFS+=("$bdf")
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,6 +822,9 @@ apply_vm_action_for_lxc_mode() {
|
|||||||
if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then
|
if ! _contains_in_array "$_vd_id" "${SELECTED_IOMMU_IDS[@]}"; then
|
||||||
SELECTED_IOMMU_IDS+=("$_vd_id")
|
SELECTED_IOMMU_IDS+=("$_vd_id")
|
||||||
fi
|
fi
|
||||||
|
if ! _contains_in_array "$_vd_id" "${SELECTED_LEGACY_IOMMU_IDS[@]}"; then
|
||||||
|
SELECTED_LEGACY_IOMMU_IDS+=("$_vd_id")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1018,6 +895,12 @@ switch_to_vm_mode() {
|
|||||||
|
|
||||||
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
msg_info "$(translate 'Configuring host for GPU -> VM mode...')"
|
||||||
|
|
||||||
|
local -a selected_types=()
|
||||||
|
mapfile -t selected_types < <(_selected_types_unique)
|
||||||
|
if _contains_in_array "nvidia" "${selected_types[@]}"; then
|
||||||
|
_proxmenux_nvidia_migrate_legacy_blacklist
|
||||||
|
fi
|
||||||
|
|
||||||
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
if declare -F _pci_is_iommu_active >/dev/null 2>&1 && _pci_is_iommu_active; then
|
||||||
_register_iommu_tool
|
_register_iommu_tool
|
||||||
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
msg_ok "$(translate 'IOMMU is already active on this system')" | tee -a "$screen_capture"
|
||||||
@@ -1044,24 +927,30 @@ switch_to_vm_mode() {
|
|||||||
local -a current_ids=()
|
local -a current_ids=()
|
||||||
mapfile -t current_ids < <(_read_vfio_ids)
|
mapfile -t current_ids < <(_read_vfio_ids)
|
||||||
local id
|
local id
|
||||||
for id in "${SELECTED_IOMMU_IDS[@]}"; do
|
for id in "${SELECTED_LEGACY_IOMMU_IDS[@]}"; do
|
||||||
_contains_in_array "$id" "${current_ids[@]}" || current_ids+=("$id")
|
_contains_in_array "$id" "${current_ids[@]}" || current_ids+=("$id")
|
||||||
done
|
done
|
||||||
_write_vfio_ids "${current_ids[@]}"
|
_write_vfio_ids "${current_ids[@]}"
|
||||||
if [[ ${#SELECTED_IOMMU_IDS[@]} -gt 0 ]]; then
|
if [[ ${#SELECTED_LEGACY_IOMMU_IDS[@]} -gt 0 ]]; then
|
||||||
local ids_label
|
local ids_label
|
||||||
ids_label=$(IFS=','; echo "${SELECTED_IOMMU_IDS[*]}")
|
ids_label=$(IFS=','; echo "${SELECTED_LEGACY_IOMMU_IDS[*]}")
|
||||||
msg_ok "$(translate 'vfio-pci IDs configured') (${ids_label})" | tee -a "$screen_capture"
|
msg_ok "$(translate 'vfio-pci IDs configured') (${ids_label})" | tee -a "$screen_capture"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local -a selected_types=()
|
if [[ ${#SELECTED_NVIDIA_BDFS[@]} -gt 0 ]]; then
|
||||||
mapfile -t selected_types < <(_selected_types_unique)
|
_proxmenux_vfio_bind_add_bdfs "${SELECTED_NVIDIA_BDFS[@]}"
|
||||||
local t
|
msg_ok "$(translate 'NVIDIA per-BDF VFIO binding configured') (${SELECTED_NVIDIA_BDFS[*]})" | tee -a "$screen_capture"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local t legacy_blacklist_configured=false
|
||||||
for t in "${selected_types[@]}"; do
|
for t in "${selected_types[@]}"; do
|
||||||
|
[[ "$t" == "nvidia" ]] && continue
|
||||||
_add_gpu_blacklist "$t"
|
_add_gpu_blacklist "$t"
|
||||||
|
legacy_blacklist_configured=true
|
||||||
done
|
done
|
||||||
msg_ok "$(translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf')" | tee -a "$screen_capture"
|
$legacy_blacklist_configured \
|
||||||
_contains_in_array "nvidia" "${selected_types[@]}" && _sanitize_nvidia_host_stack_for_vfio
|
&& msg_ok "$(translate 'GPU host driver blacklisted in /etc/modprobe.d/blacklist.conf')" | tee -a "$screen_capture"
|
||||||
|
_contains_in_array "nvidia" "${selected_types[@]}" && _proxmenux_nvidia_vfio_policy_sync || true
|
||||||
_contains_in_array "amd" "${selected_types[@]}" && _add_amd_softdep
|
_contains_in_array "amd" "${selected_types[@]}" && _add_amd_softdep
|
||||||
|
|
||||||
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
|
if [[ "$HOST_CONFIG_CHANGED" == "true" ]]; then
|
||||||
@@ -1095,12 +984,20 @@ switch_to_lxc_mode() {
|
|||||||
|
|
||||||
msg_info "$(translate 'Removing VFIO ownership for selected GPU(s)...')"
|
msg_info "$(translate 'Removing VFIO ownership for selected GPU(s)...')"
|
||||||
|
|
||||||
|
local -a selected_types=()
|
||||||
|
mapfile -t selected_types < <(_selected_types_unique)
|
||||||
|
if _contains_in_array "nvidia" "${selected_types[@]}"; then
|
||||||
|
_proxmenux_nvidia_migrate_legacy_blacklist
|
||||||
|
[[ ${#SELECTED_NVIDIA_BDFS[@]} -gt 0 ]] \
|
||||||
|
&& _proxmenux_vfio_bind_remove_bdfs "${SELECTED_NVIDIA_BDFS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
local -a current_ids=() remaining_ids=() removed_ids=()
|
local -a current_ids=() remaining_ids=() removed_ids=()
|
||||||
mapfile -t current_ids < <(_read_vfio_ids)
|
mapfile -t current_ids < <(_read_vfio_ids)
|
||||||
local id remove
|
local id remove
|
||||||
for id in "${current_ids[@]}"; do
|
for id in "${current_ids[@]}"; do
|
||||||
remove=false
|
remove=false
|
||||||
_contains_in_array "$id" "${SELECTED_IOMMU_IDS[@]}" && remove=true
|
_contains_in_array "$id" "${SELECTED_LEGACY_IOMMU_IDS[@]}" && remove=true
|
||||||
if $remove; then
|
if $remove; then
|
||||||
removed_ids+=("$id")
|
removed_ids+=("$id")
|
||||||
else
|
else
|
||||||
@@ -1114,17 +1011,16 @@ switch_to_lxc_mode() {
|
|||||||
msg_ok "$(translate 'VFIO device IDs removed from /etc/modprobe.d/vfio.conf') (${ids_label})" | tee -a "$screen_capture"
|
msg_ok "$(translate 'VFIO device IDs removed from /etc/modprobe.d/vfio.conf') (${ids_label})" | tee -a "$screen_capture"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local -a selected_types=()
|
|
||||||
mapfile -t selected_types < <(_selected_types_unique)
|
|
||||||
local t
|
local t
|
||||||
for t in "${selected_types[@]}"; do
|
for t in "${selected_types[@]}"; do
|
||||||
|
if [[ "$t" == "nvidia" ]]; then
|
||||||
|
_proxmenux_nvidia_vfio_policy_sync || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if ! _type_has_remaining_vfio_ids "$t" "${remaining_ids[@]}"; then
|
if ! _type_has_remaining_vfio_ids "$t" "${remaining_ids[@]}"; then
|
||||||
if _remove_gpu_blacklist "$t"; then
|
if _remove_gpu_blacklist "$t"; then
|
||||||
msg_ok "$(translate 'Driver blacklist removed for') ${t}" | tee -a "$screen_capture"
|
msg_ok "$(translate 'Driver blacklist removed for') ${t}" | tee -a "$screen_capture"
|
||||||
fi
|
fi
|
||||||
if [[ "$t" == "nvidia" ]]; then
|
|
||||||
_restore_nvidia_host_stack_for_lxc
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
||||||
# Version : 1.0
|
# Version : 1.1
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# Applies a curated set of 14 safe optimizations to a fresh
|
# Applies a curated set of 14 safe optimizations to a fresh
|
||||||
# Proxmox VE host without prompts. Every change is registered
|
# Proxmox VE host without prompts. Reversible changes are registered
|
||||||
# in installed_tools.json so it can be reversed later from the
|
# in installed_tools.json so they can be restored later from the
|
||||||
# Uninstall Optimizations menu.
|
# Uninstall Optimizations menu; package upgrades are intentionally
|
||||||
|
# excluded because they cannot be rolled back safely.
|
||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - Zero-interaction baseline: repos, upgrade, banner, APT
|
# - Zero-interaction baseline: repos, upgrade, banner, APT
|
||||||
@@ -158,6 +159,8 @@ apt_upgrade() {
|
|||||||
|
|
||||||
|
|
||||||
remove_subscription_banner() {
|
remove_subscription_banner() {
|
||||||
|
local FUNC_VERSION="1.1"
|
||||||
|
# description: Patch the Proxmox web UI to suppress the subscription dialog and register a successful patch.
|
||||||
local pve_version
|
local pve_version
|
||||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
||||||
|
|
||||||
@@ -176,15 +179,22 @@ remove_subscription_banner() {
|
|||||||
msg_warn "Banner removal cancelled by user."
|
msg_warn "Banner removal cancelled by user."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"
|
if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"; then
|
||||||
|
msg_error "$(translate "Subscription banner removal failed")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
if ! whiptail --title "Proxmox VE 8.x Subscription Banner Removal" \
|
if ! whiptail --title "Proxmox VE 8.x Subscription Banner Removal" \
|
||||||
--yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then
|
--yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then
|
||||||
msg_warn "Banner removal cancelled by user."
|
msg_warn "Banner removal cancelled by user."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
bash "$LOCAL_SCRIPTS/global/remove-banner-pve8.sh"
|
if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve8.sh"; then
|
||||||
|
msg_error "$(translate "Subscription banner removal failed")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
register_tool "subscription_banner" true "$FUNC_VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
||||||
# Version : 1.3
|
# Version : 1.4
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# Interactive post-installation configurator for Proxmox VE.
|
# Interactive post-installation configurator for Proxmox VE.
|
||||||
@@ -15,8 +15,9 @@
|
|||||||
# System, Virtualization, Network, Storage, Security,
|
# System, Virtualization, Network, Storage, Security,
|
||||||
# Customization, Monitoring, Performance, Optional) and presents
|
# Customization, Monitoring, Performance, Optional) and presents
|
||||||
# a checklist per category so the user picks exactly what to
|
# a checklist per category so the user picks exactly what to
|
||||||
# apply. Every change is registered in installed_tools.json for
|
# apply. Reversible changes are registered in installed_tools.json
|
||||||
# later reversal from Uninstall Optimizations.
|
# for later restoration from Uninstall Optimizations; package upgrades
|
||||||
|
# are intentionally excluded because they cannot be rolled back safely.
|
||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - Checklist UI per category (10 categories, ~30 tools total).
|
# - Checklist UI per category (10 categories, ~30 tools total).
|
||||||
@@ -24,8 +25,8 @@
|
|||||||
# optimizations plus opt-in items (IOMMU/VFIO, Fastfetch,
|
# optimizations plus opt-in items (IOMMU/VFIO, Fastfetch,
|
||||||
# Figurine, Ceph repo, HA, AMD fixes, pigz, ZFS ARC, …).
|
# Figurine, Ceph repo, HA, AMD fixes, pigz, ZFS ARC, …).
|
||||||
# - Idempotent: safe to run repeatedly.
|
# - Idempotent: safe to run repeatedly.
|
||||||
# - Registration + rollback: every tool has a reverse function
|
# - Registration + rollback: every registered tool has a reverse
|
||||||
# in uninstall-tools.sh.
|
# function in uninstall-tools.sh.
|
||||||
#
|
#
|
||||||
# Credits:
|
# Credits:
|
||||||
# Incorporates ideas and snippets originally published under BSD
|
# Incorporates ideas and snippets originally published under BSD
|
||||||
@@ -155,7 +156,7 @@ $(translate "Do you want to continue anyway?")" 13 70
|
|||||||
|
|
||||||
|
|
||||||
enable_kexec() {
|
enable_kexec() {
|
||||||
local FUNC_VERSION="1.0"
|
local FUNC_VERSION="1.1"
|
||||||
# description: Install kexec-tools and add a Ctrl+Alt+K hotkey + systemd unit for fast reboots that skip BIOS/POST.
|
# description: Install kexec-tools and add a Ctrl+Alt+K hotkey + systemd unit for fast reboots that skip BIOS/POST.
|
||||||
msg_info2 "$(translate "Configuring kexec for quick reboots...")"
|
msg_info2 "$(translate "Configuring kexec for quick reboots...")"
|
||||||
NECESSARY_REBOOT=1
|
NECESSARY_REBOOT=1
|
||||||
@@ -169,7 +170,7 @@ enable_kexec() {
|
|||||||
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install kexec-tools > /dev/null 2>&1
|
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install kexec-tools > /dev/null 2>&1
|
||||||
msg_ok "$(translate "kexec-tools installed successfully")"
|
msg_ok "$(translate "kexec-tools installed successfully")"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "kexec-tools installed successfully")"
|
msg_ok "$(translate "kexec-tools is already installed")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create systemd service file
|
# Create systemd service file
|
||||||
@@ -194,7 +195,7 @@ WantedBy=default.target
|
|||||||
EOF
|
EOF
|
||||||
msg_ok "$(translate "kexec-pve service file created")"
|
msg_ok "$(translate "kexec-pve service file created")"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "kexec-pve service file created")"
|
msg_ok "$(translate "kexec-pve service file is already configured")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Enable the service
|
# Enable the service
|
||||||
@@ -202,7 +203,7 @@ EOF
|
|||||||
systemctl enable kexec-pve.service > /dev/null 2>&1
|
systemctl enable kexec-pve.service > /dev/null 2>&1
|
||||||
msg_ok "$(translate "kexec-pve service enabled")"
|
msg_ok "$(translate "kexec-pve service enabled")"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "kexec-pve service enabled")"
|
msg_ok "$(translate "kexec-pve service is already enabled")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f /root/.bash_profile ]; then
|
if [ ! -f /root/.bash_profile ]; then
|
||||||
@@ -213,7 +214,7 @@ EOF
|
|||||||
echo "alias reboot-quick='systemctl kexec'" >> /root/.bash_profile
|
echo "alias reboot-quick='systemctl kexec'" >> /root/.bash_profile
|
||||||
msg_ok "$(translate "reboot-quick alias added")"
|
msg_ok "$(translate "reboot-quick alias added")"
|
||||||
else
|
else
|
||||||
msg_ok "$(translate "reboot-quick alias added")"
|
msg_ok "$(translate "reboot-quick alias is already configured")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_success "$(translate "kexec configured successfully. Use the command: reboot-quick")"
|
msg_success "$(translate "kexec configured successfully. Use the command: reboot-quick")"
|
||||||
@@ -1026,7 +1027,7 @@ EOF
|
|||||||
|
|
||||||
|
|
||||||
install_ceph() {
|
install_ceph() {
|
||||||
local FUNC_VERSION="1.0"
|
local FUNC_VERSION="1.1"
|
||||||
# description: Install Ceph (client + server packages) for distributed RBD/CephFS storage; PVE 8/9 aware repo selection.
|
# description: Install Ceph (client + server packages) for distributed RBD/CephFS storage; PVE 8/9 aware repo selection.
|
||||||
msg_info2 "$(translate "Installing Ceph support...")"
|
msg_info2 "$(translate "Installing Ceph support...")"
|
||||||
|
|
||||||
@@ -1055,6 +1056,11 @@ install_ceph() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! -r /usr/share/keyrings/proxmox-archive-keyring.gpg ]]; then
|
||||||
|
msg_error "$(translate "The Proxmox archive keyring is missing; Ceph installation cannot continue safely")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Configure Ceph repository based on version
|
# Configure Ceph repository based on version
|
||||||
msg_info "$(translate "Configuring Ceph repository for PVE") $pve_version..."
|
msg_info "$(translate "Configuring Ceph repository for PVE") $pve_version..."
|
||||||
|
|
||||||
@@ -1086,10 +1092,10 @@ EOF
|
|||||||
|
|
||||||
# Use legacy format for PVE 8
|
# Use legacy format for PVE 8
|
||||||
msg_info "$(translate "Creating Ceph repository for PVE 8 (legacy format)...")"
|
msg_info "$(translate "Creating Ceph repository for PVE 8 (legacy format)...")"
|
||||||
echo "deb https://download.proxmox.com/debian/ceph-${ceph_version} ${target_codename} no-subscription" > /etc/apt/sources.list.d/ceph-${ceph_version}.list
|
echo "deb [signed-by=/usr/share/keyrings/proxmox-archive-keyring.gpg] https://download.proxmox.com/debian/ceph-${ceph_version} ${target_codename} no-subscription" > /etc/apt/sources.list.d/ceph-${ceph_version}.list
|
||||||
msg_ok "$(translate "Ceph repository configured for PVE 8")"
|
msg_ok "$(translate "Ceph repository configured for PVE 8")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
msg_info "$(translate "Updating package lists...")"
|
msg_info "$(translate "Updating package lists...")"
|
||||||
|
|
||||||
@@ -1102,16 +1108,9 @@ EOF
|
|||||||
msg_warn "$(translate "Package update had issues, checking details...")"
|
msg_warn "$(translate "Package update had issues, checking details...")"
|
||||||
|
|
||||||
|
|
||||||
if echo "$update_output" | grep -q "NO_PUBKEY\|GPG error"; then
|
if echo "$update_output" | grep -Eqi 'NO_PUBKEY|GPG error|EXPKEYSIG|BADSIG|not signed|signatures? (could not|couldn.t) be verified'; then
|
||||||
msg_info "$(translate "Fixing GPG key issues...")"
|
msg_error "$(translate "Ceph repository signature verification failed; installation has been stopped")"
|
||||||
|
return 1
|
||||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $(echo "$update_output" | grep "NO_PUBKEY" | sed 's/.*NO_PUBKEY //' | head -1) 2>/dev/null
|
|
||||||
|
|
||||||
if apt-get update > /dev/null 2>&1; then
|
|
||||||
msg_ok "$(translate "Package lists updated after GPG fix")"
|
|
||||||
else
|
|
||||||
msg_warn "$(translate "Package update still has issues, continuing anyway...")"
|
|
||||||
fi
|
|
||||||
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
|
elif echo "$update_output" | grep -q "404\|Failed to fetch"; then
|
||||||
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
|
msg_warn "$(translate "Some repositories are not available, continuing with available ones...")"
|
||||||
else
|
else
|
||||||
@@ -1544,17 +1543,55 @@ update_snapshot_schedule() {
|
|||||||
|
|
||||||
|
|
||||||
disable_rpc() {
|
disable_rpc() {
|
||||||
|
local FUNC_VERSION="1.1"
|
||||||
|
# description: Disable rpcbind service/socket while preserving their exact previous systemd state for rollback.
|
||||||
|
local state_file="$BASE_DIR/rpcbind.state"
|
||||||
|
local state_tmp="${state_file}.tmp.$$"
|
||||||
|
local unit load_state enabled_state active_state
|
||||||
|
|
||||||
msg_info2 "$(translate "Disabling portmapper/rpcbind for security...")"
|
msg_info2 "$(translate "Disabling portmapper/rpcbind for security...")"
|
||||||
|
|
||||||
msg_info "$(translate "Disabling and stopping rpcbind service...")"
|
mkdir -p "$BASE_DIR"
|
||||||
|
if [[ ! -s "$state_file" ]]; then
|
||||||
|
: > "$state_tmp"
|
||||||
|
for unit in rpcbind.socket rpcbind.service; do
|
||||||
|
load_state="$(systemctl show -p LoadState --value "$unit" 2>/dev/null || true)"
|
||||||
|
[[ -z "$load_state" || "$load_state" == "not-found" ]] && continue
|
||||||
|
enabled_state="$(systemctl is-enabled "$unit" 2>/dev/null || true)"
|
||||||
|
active_state="$(systemctl is-active "$unit" 2>/dev/null || true)"
|
||||||
|
printf '%s|%s|%s\n' "$unit" "${enabled_state:-unknown}" "${active_state:-unknown}" >> "$state_tmp"
|
||||||
|
done
|
||||||
|
|
||||||
# Disable and stop rpcbind
|
if [[ ! -s "$state_tmp" ]]; then
|
||||||
systemctl disable rpcbind > /dev/null 2>&1
|
rm -f "$state_tmp"
|
||||||
systemctl stop rpcbind > /dev/null 2>&1
|
msg_warn "$(translate "rpcbind units were not found; no changes were made")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mv "$state_tmp" "$state_file"
|
||||||
|
fi
|
||||||
|
|
||||||
msg_ok "$(translate "rpcbind service has been disabled and stopped")"
|
# Register as soon as the original state is safely persisted. If a
|
||||||
|
# later systemd operation fails, Uninstall Optimizations must still
|
||||||
|
# expose the recovery path instead of leaving a hidden partial change.
|
||||||
|
register_tool "rpc" true "$FUNC_VERSION"
|
||||||
|
|
||||||
msg_success "$(translate "portmapper/rpcbind has been disabled and removed")"
|
msg_info "$(translate "Disabling and stopping rpcbind service and socket...")"
|
||||||
|
|
||||||
|
systemctl disable --now rpcbind.socket rpcbind.service > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
for unit in rpcbind.socket rpcbind.service; do
|
||||||
|
active_state="$(systemctl is-active "$unit" 2>/dev/null || true)"
|
||||||
|
enabled_state="$(systemctl is-enabled "$unit" 2>/dev/null || true)"
|
||||||
|
if [[ "$active_state" == "active" || "$active_state" == "activating" ||
|
||||||
|
"$enabled_state" == "enabled" || "$enabled_state" == "enabled-runtime" ]]; then
|
||||||
|
msg_error "$(translate "rpcbind could not be disabled completely")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
msg_ok "$(translate "rpcbind service and socket have been disabled and stopped")"
|
||||||
|
|
||||||
|
msg_success "$(translate "portmapper/rpcbind has been disabled")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2008,26 +2045,49 @@ EOF
|
|||||||
|
|
||||||
|
|
||||||
setup_motd() {
|
setup_motd() {
|
||||||
|
local FUNC_VERSION="1.1"
|
||||||
|
# description: Add the ProxMenux MOTD banner while preserving the original file contents or absence for rollback.
|
||||||
msg_info2 "$(translate "Configuring MOTD (Message of the Day) banner...")"
|
msg_info2 "$(translate "Configuring MOTD (Message of the Day) banner...")"
|
||||||
|
|
||||||
local motd_file="/etc/motd"
|
local motd_file="${PROXMENUX_MOTD_FILE:-/etc/motd}"
|
||||||
local custom_message=" This system is optimised by: ProxMenux"
|
local custom_message=" This system is optimised by: ProxMenux"
|
||||||
|
local state_file="$BASE_DIR/motd.state"
|
||||||
|
local original_file="$BASE_DIR/motd.original"
|
||||||
local changes_made=false
|
local changes_made=false
|
||||||
|
|
||||||
msg_info "$(translate "Checking MOTD configuration...")"
|
msg_info "$(translate "Checking MOTD configuration...")"
|
||||||
|
|
||||||
# Check if the custom message already exists
|
mkdir -p "$BASE_DIR"
|
||||||
if grep -q "$custom_message" "$motd_file"; then
|
if [[ ! -f "$state_file" ]]; then
|
||||||
msg_ok "$(translate "Custom message added to MOTD")"
|
if grep -Fqx "$custom_message" "$motd_file" 2>/dev/null; then
|
||||||
else
|
if [[ -f "${motd_file}.bak" ]]; then
|
||||||
# Create a backup of the original MOTD file
|
cp -a "${motd_file}.bak" "$original_file"
|
||||||
if [ ! -f "${motd_file}.bak" ]; then
|
printf 'present\n' > "$state_file"
|
||||||
cp "$motd_file" "${motd_file}.bak"
|
else
|
||||||
msg_ok "$(translate "Backup of original MOTD created")"
|
printf 'legacy-marker\n' > "$state_file"
|
||||||
|
fi
|
||||||
|
elif [[ -e "$motd_file" ]]; then
|
||||||
|
cp -a "$motd_file" "$original_file"
|
||||||
|
printf 'present\n' > "$state_file"
|
||||||
|
else
|
||||||
|
printf 'absent\n' > "$state_file"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the custom message already exists
|
||||||
|
if grep -Fqx "$custom_message" "$motd_file" 2>/dev/null; then
|
||||||
|
msg_ok "$(translate "Custom MOTD message is already configured")"
|
||||||
|
else
|
||||||
# Add the custom message at the beginning of the file
|
# Add the custom message at the beginning of the file
|
||||||
echo -e "$custom_message\n\n$(cat $motd_file)" > "$motd_file"
|
touch "$motd_file"
|
||||||
|
local motd_tmp
|
||||||
|
motd_tmp="$(mktemp)"
|
||||||
|
{
|
||||||
|
printf '%s\n\n' "$custom_message"
|
||||||
|
cat "$motd_file"
|
||||||
|
} > "$motd_tmp"
|
||||||
|
cat "$motd_tmp" > "$motd_file"
|
||||||
|
rm -f "$motd_tmp"
|
||||||
changes_made=true
|
changes_made=true
|
||||||
msg_ok "$(translate "Custom message added to MOTD")"
|
msg_ok "$(translate "Custom message added to MOTD")"
|
||||||
fi
|
fi
|
||||||
@@ -2037,8 +2097,9 @@ setup_motd() {
|
|||||||
if $changes_made; then
|
if $changes_made; then
|
||||||
msg_success "$(translate "MOTD configuration updated successfully")"
|
msg_success "$(translate "MOTD configuration updated successfully")"
|
||||||
else
|
else
|
||||||
msg_success "$(translate "MOTD configuration updated successfully")"
|
msg_success "$(translate "MOTD configuration was already up to date")"
|
||||||
fi
|
fi
|
||||||
|
register_tool "motd" true "$FUNC_VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2094,7 +2155,7 @@ EOF
|
|||||||
|
|
||||||
|
|
||||||
remove_subscription_banner() {
|
remove_subscription_banner() {
|
||||||
local FUNC_VERSION="1.0"
|
local FUNC_VERSION="1.1"
|
||||||
# description: Patch the Proxmox web UI to suppress the "no valid subscription" dialog (PVE 8 + 9 variants supported).
|
# description: Patch the Proxmox web UI to suppress the "no valid subscription" dialog (PVE 8 + 9 variants supported).
|
||||||
local pve_version
|
local pve_version
|
||||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
||||||
@@ -2105,11 +2166,15 @@ remove_subscription_banner() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$pve_version" -ge 9 ]]; then
|
if [[ "$pve_version" -ge 9 ]]; then
|
||||||
|
if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"; then
|
||||||
bash "$LOCAL_SCRIPTS/global/remove-banner-pve-v3.sh"
|
msg_error "$(translate "Subscription banner removal failed")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
|
if ! bash "$LOCAL_SCRIPTS/global/remove-banner-pve8.sh"; then
|
||||||
bash "$LOCAL_SCRIPTS/global/remove-banner-pve8.sh"
|
msg_error "$(translate "Subscription banner removal failed")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
register_tool "subscription_banner" true "$FUNC_VERSION"
|
register_tool "subscription_banner" true "$FUNC_VERSION"
|
||||||
}
|
}
|
||||||
@@ -3056,6 +3121,10 @@ setup_persistent_network() {
|
|||||||
|
|
||||||
|
|
||||||
install_system_utils() {
|
install_system_utils() {
|
||||||
|
local FUNC_VERSION="1.1"
|
||||||
|
# description: Install selected system utilities and track only packages that were newly added by ProxMenux.
|
||||||
|
local state_file="$BASE_DIR/system_utils.packages"
|
||||||
|
local new_packages_tmp=""
|
||||||
msg_info2 "$(translate "Installing system utilities...")"
|
msg_info2 "$(translate "Installing system utilities...")"
|
||||||
|
|
||||||
# Build checklist from global PROXMENUX_UTILS array
|
# Build checklist from global PROXMENUX_UTILS array
|
||||||
@@ -3087,6 +3156,8 @@ install_system_utils() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
new_packages_tmp="$(mktemp)"
|
||||||
|
|
||||||
local success=0 failed=0 warning=0
|
local success=0 failed=0 warning=0
|
||||||
local selected_array
|
local selected_array
|
||||||
IFS=' ' read -ra selected_array <<< "$selected"
|
IFS=' ' read -ra selected_array <<< "$selected"
|
||||||
@@ -3094,6 +3165,10 @@ install_system_utils() {
|
|||||||
for util in "${selected_array[@]}"; do
|
for util in "${selected_array[@]}"; do
|
||||||
util=$(echo "$util" | tr -d '"')
|
util=$(echo "$util" | tr -d '"')
|
||||||
local pkg_cmd="$util" pkg_desc="$util"
|
local pkg_cmd="$util" pkg_desc="$util"
|
||||||
|
local was_installed=false
|
||||||
|
if dpkg-query -W -f='${Status}' "$util" 2>/dev/null | grep -q '^install ok installed$'; then
|
||||||
|
was_installed=true
|
||||||
|
fi
|
||||||
for util_entry in "${PROXMENUX_UTILS[@]}"; do
|
for util_entry in "${PROXMENUX_UTILS[@]}"; do
|
||||||
IFS=':' read -r epkg ecmd edesc <<< "$util_entry"
|
IFS=':' read -r epkg ecmd edesc <<< "$util_entry"
|
||||||
if [[ "$epkg" == "$util" ]]; then
|
if [[ "$epkg" == "$util" ]]; then
|
||||||
@@ -3103,19 +3178,38 @@ install_system_utils() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
install_single_package "$util" "$pkg_cmd" "$pkg_desc"
|
install_single_package "$util" "$pkg_cmd" "$pkg_desc"
|
||||||
case $? in
|
local install_result=$?
|
||||||
|
case $install_result in
|
||||||
0) success=$((success + 1)) ;;
|
0) success=$((success + 1)) ;;
|
||||||
1) failed=$((failed + 1)) ;;
|
1) failed=$((failed + 1)) ;;
|
||||||
2) warning=$((warning + 1)) ;;
|
2) warning=$((warning + 1)) ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ "$was_installed" == false ]] &&
|
||||||
|
dpkg-query -W -f='${Status}' "$util" 2>/dev/null | grep -q '^install ok installed$'; then
|
||||||
|
printf '%s\n' "$util" >> "$new_packages_tmp"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ -s "$new_packages_tmp" ]]; then
|
||||||
|
mkdir -p "$BASE_DIR"
|
||||||
|
{
|
||||||
|
[[ -f "$state_file" ]] && cat "$state_file"
|
||||||
|
cat "$new_packages_tmp"
|
||||||
|
} | sort -u > "${state_file}.tmp"
|
||||||
|
mv "${state_file}.tmp" "$state_file"
|
||||||
|
fi
|
||||||
|
rm -f "$new_packages_tmp"
|
||||||
|
|
||||||
hash -r 2>/dev/null
|
hash -r 2>/dev/null
|
||||||
echo
|
echo
|
||||||
msg_info2 "$(translate "Installation summary"):"
|
msg_info2 "$(translate "Installation summary"):"
|
||||||
[[ $success -gt 0 ]] && msg_ok "$(translate "Successful"): $success"
|
[[ $success -gt 0 ]] && msg_ok "$(translate "Successful"): $success"
|
||||||
[[ $warning -gt 0 ]] && msg_warn "$(translate "With warnings"): $warning"
|
[[ $warning -gt 0 ]] && msg_warn "$(translate "With warnings"): $warning"
|
||||||
[[ $failed -gt 0 ]] && msg_error "$(translate "Failed"): $failed"
|
[[ $failed -gt 0 ]] && msg_error "$(translate "Failed"): $failed"
|
||||||
|
if [[ -s "$state_file" ]]; then
|
||||||
|
register_tool "system_utils" true "$FUNC_VERSION"
|
||||||
|
fi
|
||||||
msg_success "$(translate "Utilities installation completed")"
|
msg_success "$(translate "Utilities installation completed")"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3126,7 +3220,6 @@ custom_post_category_label() {
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
"Basic Settings") translate "Basic Settings" ;;
|
"Basic Settings") translate "Basic Settings" ;;
|
||||||
"System") translate "System" ;;
|
"System") translate "System" ;;
|
||||||
"Hardware") translate "Hardware" ;;
|
|
||||||
"Virtualization") translate "Virtualization" ;;
|
"Virtualization") translate "Virtualization" ;;
|
||||||
"Network") translate "Network" ;;
|
"Network") translate "Network" ;;
|
||||||
"Storage") translate "Storage" ;;
|
"Storage") translate "Storage" ;;
|
||||||
@@ -3255,9 +3348,9 @@ main_menu() {
|
|||||||
HEADER="$(translate "Choose options to configure:")\n\n${header_line}"
|
HEADER="$(translate "Choose options to configure:")\n\n${header_line}"
|
||||||
|
|
||||||
declare -A category_order=(
|
declare -A category_order=(
|
||||||
["Basic Settings"]=1 ["System"]=2 ["Hardware"]=3 ["Virtualization"]=4
|
["Basic Settings"]=1 ["System"]=2 ["Virtualization"]=3
|
||||||
["Network"]=5 ["Storage"]=6 ["Security"]=7 ["Customization"]=8
|
["Network"]=4 ["Storage"]=5 ["Security"]=6 ["Customization"]=7
|
||||||
["Monitoring"]=9 ["Performance"]=10 ["Optional"]=11
|
["Monitoring"]=8 ["Performance"]=9 ["Optional"]=10
|
||||||
)
|
)
|
||||||
|
|
||||||
local options=(
|
local options=(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# Copyright : (c) 2024 MacRimi
|
# Copyright : (c) 2024 MacRimi
|
||||||
# License : GPL-3.0
|
# License : GPL-3.0
|
||||||
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
||||||
# Version : 1.0
|
# Version : 1.1
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
# Description:
|
# Description:
|
||||||
# Reverses post-install optimizations previously applied by
|
# Reverses post-install optimizations previously applied by
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
#
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - Registry-driven: only shows tools currently applied.
|
# - Registry-driven: only shows tools currently applied.
|
||||||
# - Per-tool reverse functions (one for each entry in the
|
# - Per-tool reverse functions for each registered reversible entry
|
||||||
# auto / customizable scripts).
|
# in the auto / customizable scripts.
|
||||||
# - Restores /etc configs from .bak backups when they exist.
|
# - Restores /etc configs from .bak backups when they exist.
|
||||||
# - Reboot prompt for changes that require it (VFIO, kernel
|
# - Reboot prompt for changes that require it (VFIO, kernel
|
||||||
# cmdline, persistent NIC names, …).
|
# cmdline, persistent NIC names, …).
|
||||||
@@ -112,34 +112,6 @@ uninstall_kexec() {
|
|||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
|
|
||||||
uninstall_apt_upgrade() {
|
|
||||||
msg_info "$(translate "Restoring enterprise repositories...")"
|
|
||||||
|
|
||||||
# Re-enable enterprise repos
|
|
||||||
if [ -f /etc/apt/sources.list.d/pve-enterprise.list ]; then
|
|
||||||
sed -i "s/^#deb/deb/g" /etc/apt/sources.list.d/pve-enterprise.list
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f /etc/apt/sources.list.d/ceph.list ]; then
|
|
||||||
sed -i "s/^#deb/deb/g" /etc/apt/sources.list.d/ceph.list
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove public repo
|
|
||||||
rm -f /etc/apt/sources.list.d/pve-public-repo.list
|
|
||||||
|
|
||||||
# Remove firmware warning config
|
|
||||||
rm -f /etc/apt/apt.conf.d/no-bookworm-firmware.conf
|
|
||||||
|
|
||||||
apt-get update > /dev/null 2>&1
|
|
||||||
|
|
||||||
msg_ok "$(translate "Enterprise repositories restored")"
|
|
||||||
register_tool "apt_upgrade" false
|
|
||||||
}
|
|
||||||
|
|
||||||
################################################################
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
uninstall_subscription_banner() {
|
uninstall_subscription_banner() {
|
||||||
msg_info "$(translate "Restoring subscription banner...")"
|
msg_info "$(translate "Restoring subscription banner...")"
|
||||||
|
|
||||||
@@ -226,9 +198,8 @@ uninstall_subscription_banner() {
|
|||||||
|
|
||||||
#systemctl restart pveproxy pvedaemon pvestatd 2>/dev/null || true
|
#systemctl restart pveproxy pvedaemon pvestatd 2>/dev/null || true
|
||||||
|
|
||||||
register_tool "subscription_banner" false
|
|
||||||
|
|
||||||
if [[ "$restored" == true ]]; then
|
if [[ "$restored" == true ]]; then
|
||||||
|
register_tool "subscription_banner" false
|
||||||
msg_ok "$(translate "Subscription banner restored successfully (desktop and mobile)")"
|
msg_ok "$(translate "Subscription banner restored successfully (desktop and mobile)")"
|
||||||
msg_ok "$(translate "Refresh your browser to see changes (server restart may be required)")"
|
msg_ok "$(translate "Refresh your browser to see changes (server restart may be required)")"
|
||||||
else
|
else
|
||||||
@@ -241,6 +212,147 @@ uninstall_subscription_banner() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
uninstall_rpc() {
|
||||||
|
local state_file="$BASE_DIR/rpcbind.state"
|
||||||
|
local unit enabled_state active_state
|
||||||
|
local failed=0
|
||||||
|
|
||||||
|
if [[ ! -s "$state_file" ]]; then
|
||||||
|
msg_error "$(translate "The original rpcbind state is unavailable; no service state was changed")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_info2 "$(translate "Restoring the original rpcbind service state...")"
|
||||||
|
while IFS='|' read -r unit enabled_state active_state; do
|
||||||
|
[[ "$unit" != "rpcbind.service" && "$unit" != "rpcbind.socket" ]] && continue
|
||||||
|
|
||||||
|
case "$enabled_state" in
|
||||||
|
enabled)
|
||||||
|
systemctl enable "$unit" >/dev/null 2>&1 || failed=1
|
||||||
|
;;
|
||||||
|
enabled-runtime)
|
||||||
|
systemctl enable --runtime "$unit" >/dev/null 2>&1 || failed=1
|
||||||
|
;;
|
||||||
|
masked)
|
||||||
|
systemctl mask "$unit" >/dev/null 2>&1 || failed=1
|
||||||
|
;;
|
||||||
|
masked-runtime)
|
||||||
|
systemctl mask --runtime "$unit" >/dev/null 2>&1 || failed=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
systemctl disable "$unit" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$active_state" in
|
||||||
|
active|activating|reloading)
|
||||||
|
systemctl start "$unit" >/dev/null 2>&1 || failed=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
systemctl stop "$unit" >/dev/null 2>&1 || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < "$state_file"
|
||||||
|
|
||||||
|
if [[ "$failed" -ne 0 ]]; then
|
||||||
|
msg_error "$(translate "The original rpcbind state could not be restored completely")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$state_file"
|
||||||
|
register_tool "rpc" false
|
||||||
|
msg_ok "$(translate "The original rpcbind service state has been restored")"
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
uninstall_motd() {
|
||||||
|
local state_file="$BASE_DIR/motd.state"
|
||||||
|
local original_file="$BASE_DIR/motd.original"
|
||||||
|
local motd_file="${PROXMENUX_MOTD_FILE:-/etc/motd}"
|
||||||
|
local custom_message=" This system is optimised by: ProxMenux"
|
||||||
|
local original_state
|
||||||
|
|
||||||
|
if [[ ! -f "$state_file" ]]; then
|
||||||
|
msg_error "$(translate "The original MOTD state is unavailable; no changes were made")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
original_state="$(head -n 1 "$state_file" 2>/dev/null)"
|
||||||
|
case "$original_state" in
|
||||||
|
present)
|
||||||
|
if [[ ! -f "$original_file" ]]; then
|
||||||
|
msg_error "$(translate "The original MOTD backup is unavailable; no changes were made")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
cp -a "$original_file" "$motd_file"
|
||||||
|
;;
|
||||||
|
absent)
|
||||||
|
rm -f "$motd_file"
|
||||||
|
;;
|
||||||
|
legacy-marker)
|
||||||
|
if [[ -f "$motd_file" ]]; then
|
||||||
|
sed -i "\|^${custom_message}$|d" "$motd_file"
|
||||||
|
sed -i '/./,$!d' "$motd_file"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
msg_error "$(translate "The saved MOTD state is invalid; no changes were made")"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
rm -f "$state_file" "$original_file"
|
||||||
|
register_tool "motd" false
|
||||||
|
msg_ok "$(translate "The original MOTD configuration has been restored")"
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
uninstall_system_utils() {
|
||||||
|
local state_file="$BASE_DIR/system_utils.packages"
|
||||||
|
local remaining_file="${state_file}.remaining.$$"
|
||||||
|
local package
|
||||||
|
local packages=()
|
||||||
|
|
||||||
|
if [[ ! -s "$state_file" ]]; then
|
||||||
|
msg_error "$(translate "No ProxMenux-installed utility package list is available")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r package; do
|
||||||
|
[[ "$package" =~ ^[a-z0-9][a-z0-9+.-]*(:[a-z0-9]+)?$ ]] || continue
|
||||||
|
packages+=("$package")
|
||||||
|
done < "$state_file"
|
||||||
|
|
||||||
|
if [[ ${#packages[@]} -eq 0 ]]; then
|
||||||
|
msg_error "$(translate "The saved utility package list is invalid; no packages were removed")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_info2 "$(translate "Removing utilities installed by ProxMenux...")"
|
||||||
|
/usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get purge -y "${packages[@]}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
: > "$remaining_file"
|
||||||
|
for package in "${packages[@]}"; do
|
||||||
|
if dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q '^install ok installed$'; then
|
||||||
|
printf '%s\n' "$package" >> "$remaining_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -s "$remaining_file" ]]; then
|
||||||
|
mv "$remaining_file" "$state_file"
|
||||||
|
msg_error "$(translate "Some utility packages could not be removed; the remaining list has been preserved")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$remaining_file" "$state_file"
|
||||||
|
register_tool "system_utils" false
|
||||||
|
msg_ok "$(translate "Utilities installed by ProxMenux have been removed")"
|
||||||
|
}
|
||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
|
|
||||||
uninstall_time_sync() {
|
uninstall_time_sync() {
|
||||||
@@ -835,7 +947,9 @@ uninstall_ceph() {
|
|||||||
apt-get purge -y 'ceph-*' 'librados*' 'librbd*' 'libcephfs*' 'python3-ceph*' >/dev/null 2>&1 || true
|
apt-get purge -y 'ceph-*' 'librados*' 'librbd*' 'libcephfs*' 'python3-ceph*' >/dev/null 2>&1 || true
|
||||||
apt-get autoremove -y >/dev/null 2>&1 || true
|
apt-get autoremove -y >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
rm -f /etc/apt/sources.list.d/ceph.list /etc/apt/sources.list.d/ceph.sources 2>/dev/null
|
rm -f /etc/apt/sources.list.d/ceph.list \
|
||||||
|
/etc/apt/sources.list.d/ceph-squid.list \
|
||||||
|
/etc/apt/sources.list.d/ceph.sources 2>/dev/null
|
||||||
rm -f /etc/apt/trusted.gpg.d/ceph.asc /etc/apt/trusted.gpg.d/ceph-release.gpg 2>/dev/null
|
rm -f /etc/apt/trusted.gpg.d/ceph.asc /etc/apt/trusted.gpg.d/ceph-release.gpg 2>/dev/null
|
||||||
apt-get update -qq >/dev/null 2>&1 || true
|
apt-get update -qq >/dev/null 2>&1 || true
|
||||||
msg_ok "$(translate 'Ceph packages and repository removed')"
|
msg_ok "$(translate 'Ceph packages and repository removed')"
|
||||||
@@ -1038,10 +1152,10 @@ show_uninstall_menu() {
|
|||||||
local menu_options=()
|
local menu_options=()
|
||||||
for tool in "${tools_installed[@]}"; do
|
for tool in "${tools_installed[@]}"; do
|
||||||
case "$tool" in
|
case "$tool" in
|
||||||
lvm_repair) desc="LVM PV Headers Repair";;
|
|
||||||
repo_cleanup) desc="Repository Cleanup";;
|
|
||||||
#apt_upgrade) desc="APT Upgrade & Repository Config";;
|
|
||||||
subscription_banner) desc="Subscription Banner Removal";;
|
subscription_banner) desc="Subscription Banner Removal";;
|
||||||
|
rpc) desc="RPC / rpcbind Disable";;
|
||||||
|
motd) desc="Custom MOTD Banner";;
|
||||||
|
system_utils) desc="System Utilities installed by ProxMenux";;
|
||||||
time_sync) desc="Time Synchronization";;
|
time_sync) desc="Time Synchronization";;
|
||||||
apt_languages) desc="APT Language Skip";;
|
apt_languages) desc="APT Language Skip";;
|
||||||
journald) desc="Journald Optimization";;
|
journald) desc="Journald Optimization";;
|
||||||
|
|||||||
+4
-12
@@ -51,6 +51,7 @@ DARK_GRAY="\033[38;5;244m"
|
|||||||
ORANGE="\033[38;5;208m"
|
ORANGE="\033[38;5;208m"
|
||||||
YW="\033[33m"
|
YW="\033[33m"
|
||||||
YWB="\033[1;33m"
|
YWB="\033[1;33m"
|
||||||
|
MG="\033[35m"
|
||||||
GN="\033[1;92m"
|
GN="\033[1;92m"
|
||||||
RD="\033[01;31m"
|
RD="\033[01;31m"
|
||||||
CL="\033[m"
|
CL="\033[m"
|
||||||
@@ -74,8 +75,8 @@ spinner() {
|
|||||||
local interval=0.1
|
local interval=0.1
|
||||||
printf "\e[?25l"
|
printf "\e[?25l"
|
||||||
|
|
||||||
local color="${YW}"
|
local color="${MG}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
printf "\r ${color}%s${CL}" "${frames[spin_i]}"
|
printf "\r ${color}%s${CL}" "${frames[spin_i]}"
|
||||||
spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
|
spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
|
||||||
@@ -118,19 +119,10 @@ stop_spinner() {
|
|||||||
SPINNER_PID=""
|
SPINNER_PID=""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Display trnaslate message with spinner
|
|
||||||
msg_lang() {
|
|
||||||
local msg="$1"
|
|
||||||
echo -ne "${TAB}${YW}${HOLD}${msg}"
|
|
||||||
spinner &
|
|
||||||
SPINNER_PID=$!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Display info message with spinner
|
# Display info message with spinner
|
||||||
msg_info() {
|
msg_info() {
|
||||||
local msg="$1"
|
local msg="$1"
|
||||||
echo -ne "${TAB}${YW}${HOLD}${msg}"
|
echo -ne "${TAB}${MG}${HOLD}${msg}"
|
||||||
spinner &
|
spinner &
|
||||||
SPINNER_PID=$!
|
SPINNER_PID=$!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Contributing translations
|
# Contributing translations
|
||||||
|
|
||||||
The ProxMenux documentation site is built with Next.js (App Router) and
|
The ProxMenux documentation site is built with Next.js (App Router) and
|
||||||
serves every page under two URLs:
|
serves every published page under locale-prefixed URLs:
|
||||||
|
|
||||||
- `/en/<path>` — English, the source of truth
|
- `/en/<path>` — English, the source of truth
|
||||||
- `/es/<path>` — Spanish, in progress
|
- `/es/<path>` — Spanish
|
||||||
|
- `/sk/<path>` — Slovak, with English fallback where needed
|
||||||
|
|
||||||
We use [`next-intl`](https://next-intl.dev) for the i18n plumbing. Anyone
|
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
|
can translate the docs without writing TypeScript: most of the work is
|
||||||
@@ -20,8 +21,8 @@ filling in a JSON file. This guide explains the workflow end to end.
|
|||||||
|
|
||||||
Out of the box you get:
|
Out of the box you get:
|
||||||
|
|
||||||
- Routing under `app/[locale]/...` — every page already renders at both
|
- Routing under `app/[locale]/...` — every page renders for every locale
|
||||||
`/en/...` and `/es/...`.
|
enabled in `i18n/routing.ts`.
|
||||||
- Locale-aware navigation via `@/i18n/navigation` (`<Link>`, `useRouter`,
|
- Locale-aware navigation via `@/i18n/navigation` (`<Link>`, `useRouter`,
|
||||||
`usePathname`). Use these instead of `next/link` for internal hrefs so
|
`usePathname`). Use these instead of `next/link` for internal hrefs so
|
||||||
the active `[locale]` prefix is preserved.
|
the active `[locale]` prefix is preserved.
|
||||||
@@ -50,11 +51,13 @@ web/
|
|||||||
│ │ └── docs/
|
│ │ └── docs/
|
||||||
│ │ └── monitor/
|
│ │ └── monitor/
|
||||||
│ │ └── index.json # page-specific strings for /docs/monitor
|
│ │ └── index.json # page-specific strings for /docs/monitor
|
||||||
│ └── es/
|
│ ├── es/
|
||||||
│ ├── common.json
|
│ ├── common.json
|
||||||
│ └── docs/
|
│ └── docs/
|
||||||
│ └── monitor/
|
│ └── monitor/
|
||||||
│ └── index.json
|
│ └── index.json
|
||||||
|
│ └── sk/
|
||||||
|
│ └── ...
|
||||||
└── app/[locale]/
|
└── app/[locale]/
|
||||||
└── docs/
|
└── docs/
|
||||||
└── monitor/
|
└── monitor/
|
||||||
@@ -81,7 +84,7 @@ web/
|
|||||||
|
|
||||||
Browse `app/[locale]/docs/` and find a page that:
|
Browse `app/[locale]/docs/` and find a page that:
|
||||||
|
|
||||||
- Has no entry yet under `messages/es/<same-path>/` (Spanish), **and**
|
- Has no entry yet under `messages/<locale>/<same-path>/`, **and**
|
||||||
- Is not already mid-translation by someone else (check open PRs).
|
- Is not already mid-translation by someone else (check open PRs).
|
||||||
|
|
||||||
If you're translating to a new locale, start with the smallest pages so
|
If you're translating to a new locale, start with the smallest pages so
|
||||||
@@ -175,6 +178,45 @@ also had to refactor the `.tsx` (case B) or only added JSON (case A).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Automated baseline and incremental updates
|
||||||
|
|
||||||
|
The `Build web documentation translations` GitHub Action can create a
|
||||||
|
machine-translated baseline and fill newly added English keys later. It uses
|
||||||
|
`.github/scripts/build_web_docs_i18n.py` and supports a locale list, a file or
|
||||||
|
directory scope, a per-locale file limit and a dry run.
|
||||||
|
|
||||||
|
The builder is resumable and writes each completed JSON file atomically. By
|
||||||
|
default it preserves every non-empty translated value, including wording
|
||||||
|
reviewed by native speakers. It also protects rich-text tags, placeholders,
|
||||||
|
URLs, paths, commands, variables and official product names before sending a
|
||||||
|
string to the translation provider. `--refresh` deliberately overwrites the
|
||||||
|
selected scope and should only be used when those translations are going to be
|
||||||
|
reviewed again.
|
||||||
|
|
||||||
|
Run a coverage report without contacting a translation service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .github/scripts/build_web_docs_i18n.py \
|
||||||
|
--languages de,fr,it,pt,sk,sv \
|
||||||
|
--section docs \
|
||||||
|
--check
|
||||||
|
```
|
||||||
|
|
||||||
|
Translate a small resumable batch locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python .github/scripts/build_web_docs_i18n.py \
|
||||||
|
--languages de \
|
||||||
|
--section docs/monitor \
|
||||||
|
--max-files 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Machine translation is a starting point, not the final authority. Native
|
||||||
|
contributors can edit the generated JSON normally; later incremental runs
|
||||||
|
will keep their non-empty wording unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Workflow: convert a page from hard-coded English to i18n (case B)
|
## Workflow: convert a page from hard-coded English to i18n (case B)
|
||||||
|
|
||||||
This is the more involved path. Use the pilot
|
This is the more involved path. Use the pilot
|
||||||
@@ -268,7 +310,12 @@ registers a renderer for them.
|
|||||||
|
|
||||||
If you want to add a language that isn't in the project yet:
|
If you want to add a language that isn't in the project yet:
|
||||||
|
|
||||||
1. Add the locale code to `routing.ts`:
|
1. Create `messages/<locale>/common.json` and the page catalogs. The automated
|
||||||
|
builder can provide the initial baseline.
|
||||||
|
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
|
||||||
|
exposed publicly:
|
||||||
```ts
|
```ts
|
||||||
export const routing = defineRouting({
|
export const routing = defineRouting({
|
||||||
locales: ["en", "es", "fr"], // add your code here
|
locales: ["en", "es", "fr"], // add your code here
|
||||||
@@ -276,12 +323,9 @@ If you want to add a language that isn't in the project yet:
|
|||||||
localePrefix: "always",
|
localePrefix: "always",
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
2. Create the `messages/<locale>/` folder.
|
4. Add its human-readable label to the language switcher.
|
||||||
3. Copy `messages/en/common.json` over and translate it. **This is
|
5. Continue reviewing individual pages one PR at a time.
|
||||||
mandatory** — without it the navbar and footer fall back to English
|
6. Mention in your first PR that you're seeding the locale so reviewers
|
||||||
on every page.
|
|
||||||
4. Start translating individual pages one PR at a time.
|
|
||||||
5. Mention in your first PR that you're seeding the locale so reviewers
|
|
||||||
know to expect a follow-up batch.
|
know to expect a follow-up batch.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -305,9 +349,8 @@ Three common causes:
|
|||||||
### What about translations of the Monitor (the AppImage), not just the docs?
|
### What about translations of the Monitor (the AppImage), not just the docs?
|
||||||
|
|
||||||
This guide only covers the **public documentation site** in `web/`.
|
This guide only covers the **public documentation site** in `web/`.
|
||||||
The Monitor's dashboard UI in `AppImage/` is a separate project and
|
The Monitor dashboard uses the separate catalogs under
|
||||||
not currently i18n-enabled. Translating the Monitor would require a
|
`AppImage/messages/` and its own translation workflow.
|
||||||
parallel effort.
|
|
||||||
|
|
||||||
### Where can I see what's missing?
|
### Where can I see what's missing?
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ function ImageWithCaption({ src, alt, caption }: { src: string; alt: string; cap
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StepNumber({ number }: { number: number }) {
|
function StepHeading({ number, label, title, id }: { number: number; label: string; title: string; id?: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex items-center gap-3 mb-4" id={id}>
|
||||||
className="inline-flex items-center justify-center w-8 h-8 mr-3 text-white bg-blue-500 rounded-full"
|
<span className="inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-2.5 py-0.5 text-xs font-semibold text-blue-800">
|
||||||
aria-hidden="true"
|
{label} {number}
|
||||||
>
|
</span>
|
||||||
<span className="text-sm font-bold">{number}</span>
|
<h2 className="text-xl font-semibold m-0">{title}</h2>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,10 +271,7 @@ export default function Page() {
|
|||||||
const media = step.loaders[activeLoader] || []
|
const media = step.loaders[activeLoader] || []
|
||||||
return (
|
return (
|
||||||
<section key={step.id} className="mb-12 border-b pb-8">
|
<section key={step.id} className="mb-12 border-b pb-8">
|
||||||
<h2 className="text-xl font-semibold mb-4 flex items-center" id={step.id}>
|
<StepHeading number={stepIdx + 1} label={t("stepLabel")} title={step.title} id={step.id} />
|
||||||
<StepNumber number={stepIdx + 1} />
|
|
||||||
{step.title}
|
|
||||||
</h2>
|
|
||||||
<p className="mb-4">{step.intro}</p>
|
<p className="mb-4">{step.intro}</p>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|||||||
@@ -169,18 +169,11 @@ export default async function GpuVmPassthroughPage({
|
|||||||
├─ Not in SR-IOV
|
├─ Not in SR-IOV
|
||||||
├─ Not D3cold (AMD)
|
├─ Not D3cold (AMD)
|
||||||
├─ Has FLR or equivalent reset
|
├─ Has FLR or equivalent reset
|
||||||
|
├─ Not on the block-list (Intel Arc, Apollo Lake)
|
||||||
├─ Warn if single-GPU host
|
├─ Warn if single-GPU host
|
||||||
└─ Resolve IOMMU group
|
└─ Resolve IOMMU group
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Audio companion
|
|
||||||
├─ Has .1 sibling? (dGPU: NVIDIA/AMD HDMI)
|
|
||||||
│ → auto-include (never used by host)
|
|
||||||
└─ No .1 sibling? (Intel iGPU, split audio)
|
|
||||||
→ checklist of host audio controllers,
|
|
||||||
default = none (user opts in)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
User selects VM
|
User selects VM
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -190,14 +183,24 @@ export default async function GpuVmPassthroughPage({
|
|||||||
▼
|
▼
|
||||||
GPU already assigned elsewhere?
|
GPU already assigned elsewhere?
|
||||||
│
|
│
|
||||||
├─ To LXC → offer to remove it from LXC
|
├─ To LXC → menu: keep + disable onboot
|
||||||
├─ To other VM → offer to remove it there
|
│ OR remove GPU lines + keep onboot
|
||||||
│ + clean up orphan audio
|
├─ To other VM (running) → abort
|
||||||
│ (skips audio whose
|
├─ To other VM (stopped) → menu: keep + disable onboot
|
||||||
│ display sibling stays)
|
│ OR remove GPU lines + keep onboot
|
||||||
|
│ (fast-path: already vfio-pci → no
|
||||||
|
│ host reconfig, no reboot needed)
|
||||||
└─ Free → continue
|
└─ Free → continue
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
|
Audio companion
|
||||||
|
├─ Has .1 sibling? (dGPU: NVIDIA/AMD HDMI)
|
||||||
|
│ → auto-include (never used by host)
|
||||||
|
└─ No .1 sibling? (Intel iGPU, split audio)
|
||||||
|
→ checklist of host audio controllers,
|
||||||
|
default = none (user opts in)
|
||||||
|
│
|
||||||
|
▼
|
||||||
Show confirmation summary
|
Show confirmation summary
|
||||||
(GPU + IOMMU siblings + audio + target VM)
|
(GPU + IOMMU siblings + audio + target VM)
|
||||||
│
|
│
|
||||||
@@ -209,12 +212,17 @@ export default async function GpuVmPassthroughPage({
|
|||||||
▼
|
▼
|
||||||
Host:
|
Host:
|
||||||
├─ /etc/modules (vfio_*)
|
├─ /etc/modules (vfio_*)
|
||||||
├─ /etc/modprobe.d/vfio.conf (ids=...)
|
├─ /etc/modprobe.d/vfio.conf (ids=... disable_vga=1)
|
||||||
├─ /etc/modprobe.d/blacklist.conf
|
├─ /etc/modprobe.d/blacklist.conf (vendor drivers only)
|
||||||
├─ kernel cmdline (IOMMU if missing)
|
├─ kernel cmdline (IOMMU if missing)
|
||||||
├─ NVIDIA: disable udev rule + hard blacklist
|
├─ NVIDIA: per-BDF udev rule at
|
||||||
├─ AMD: dump ROM → /usr/share/kvm/*.bin
|
│ 10-proxmenux-vfio-bind.rules
|
||||||
|
│ + BDF state at vfio-bind.bdfs
|
||||||
|
│ (blacklist nvidia only when
|
||||||
|
│ every NVIDIA GPU is in VFIO)
|
||||||
|
├─ AMD: dump ROM → vbios_<vendor>_<device>.bin
|
||||||
└─ update-initramfs -u -k all
|
└─ update-initramfs -u -k all
|
||||||
|
+ proxmox-boot-tool refresh
|
||||||
|
|
||||||
VM config (qm set <vmid>):
|
VM config (qm set <vmid>):
|
||||||
├─ hostpci0 = GPU (x-vga=1 unless Intel iGPU)
|
├─ hostpci0 = GPU (x-vga=1 unless Intel iGPU)
|
||||||
@@ -336,7 +344,7 @@ export default async function GpuVmPassthroughPage({
|
|||||||
code={`# Example — what ends up in the VM config after a GPU + audio passthrough
|
code={`# Example — what ends up in the VM config after a GPU + audio passthrough
|
||||||
# (you don't type this, ProxMenux does it for you)
|
# (you don't type this, ProxMenux does it for you)
|
||||||
|
|
||||||
hostpci0: 0000:01:00.0,pcie=1,x-vga=1[,romfile=vbios_card.bin] # GPU video
|
hostpci0: 0000:01:00.0,pcie=1,x-vga=1[,romfile=vbios_1002_15dd.bin] # GPU video (AMD ROM file named vbios_<vendor>_<device>.bin)
|
||||||
hostpci1: 0000:01:00.1,pcie=1 # GPU audio
|
hostpci1: 0000:01:00.1,pcie=1 # GPU audio
|
||||||
vga: std
|
vga: std
|
||||||
|
|
||||||
@@ -455,9 +463,9 @@ qm set <vmid> --delete hostpci0
|
|||||||
# Release the GPU back to the host driver:
|
# Release the GPU back to the host driver:
|
||||||
rm -f /etc/modprobe.d/vfio.conf
|
rm -f /etc/modprobe.d/vfio.conf
|
||||||
rm -f /etc/modprobe.d/blacklist.conf # careful — this file may have other blacklists
|
rm -f /etc/modprobe.d/blacklist.conf # careful — this file may have other blacklists
|
||||||
# NVIDIA only — re-enable the udev rule + unpin the hard blacklist
|
# NVIDIA only — re-enable the udev rule + drop the all-NVIDIA-in-VFIO blacklist (if present)
|
||||||
mv /etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled /etc/udev/rules.d/70-nvidia.rules 2>/dev/null
|
mv /etc/udev/rules.d/70-nvidia.rules.proxmenux-disabled /etc/udev/rules.d/70-nvidia.rules 2>/dev/null
|
||||||
rm -f /etc/modprobe.d/nvidia-blacklist.conf
|
rm -f /etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf
|
||||||
|
|
||||||
update-initramfs -u -k all
|
update-initramfs -u -k all
|
||||||
reboot`}
|
reboot`}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export async function generateMetadata({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MatrixRow = { kernel: string; pve: string; minCode: string; minTail: string }
|
|
||||||
type StringItem = string
|
type StringItem = string
|
||||||
type RelatedItem = { label: string; href: string; tail?: string }
|
type RelatedItem = { label: string; href: string; tail?: string }
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@ export default async function NvidiaHostPage({
|
|||||||
const messages = (await getMessages({ locale })) as unknown as {
|
const messages = (await getMessages({ locale })) as unknown as {
|
||||||
docs: { hardware: { nvidiaHost: {
|
docs: { hardware: { nvidiaHost: {
|
||||||
walkthrough: {
|
walkthrough: {
|
||||||
version: { rows: MatrixRow[] }
|
|
||||||
prepare: { items: StringItem[] }
|
prepare: { items: StringItem[] }
|
||||||
}
|
}
|
||||||
reinstallUninstall: { uninstallItems: StringItem[] }
|
reinstallUninstall: { uninstallItems: StringItem[] }
|
||||||
@@ -46,7 +44,6 @@ export default async function NvidiaHostPage({
|
|||||||
related: { items: RelatedItem[] }
|
related: { items: RelatedItem[] }
|
||||||
} } }
|
} } }
|
||||||
}
|
}
|
||||||
const matrixRows = messages.docs.hardware.nvidiaHost.walkthrough.version.rows
|
|
||||||
const prepareItems = messages.docs.hardware.nvidiaHost.walkthrough.prepare.items
|
const prepareItems = messages.docs.hardware.nvidiaHost.walkthrough.prepare.items
|
||||||
const uninstallItems = messages.docs.hardware.nvidiaHost.reinstallUninstall.uninstallItems
|
const uninstallItems = messages.docs.hardware.nvidiaHost.reinstallUninstall.uninstallItems
|
||||||
const kindsItems = messages.docs.hardware.nvidiaHost.updates.kindsItems
|
const kindsItems = messages.docs.hardware.nvidiaHost.updates.kindsItems
|
||||||
@@ -255,29 +252,6 @@ export default async function NvidiaHostPage({
|
|||||||
<p className="mb-3 text-gray-800">{t.rich("walkthrough.version.body1", { strong, em })}</p>
|
<p className="mb-3 text-gray-800">{t.rich("walkthrough.version.body1", { strong, em })}</p>
|
||||||
<p className="mb-3 text-gray-800">{t("walkthrough.version.body2")}</p>
|
<p className="mb-3 text-gray-800">{t("walkthrough.version.body2")}</p>
|
||||||
|
|
||||||
<div className="my-4 overflow-x-auto">
|
|
||||||
<table className="min-w-full border border-gray-200 text-sm">
|
|
||||||
<thead className="bg-gray-100">
|
|
||||||
<tr>
|
|
||||||
<th className="border border-gray-200 px-3 py-2 text-left text-gray-900">{t("walkthrough.version.headerKernel")}</th>
|
|
||||||
<th className="border border-gray-200 px-3 py-2 text-left text-gray-900">{t("walkthrough.version.headerPve")}</th>
|
|
||||||
<th className="border border-gray-200 px-3 py-2 text-left text-gray-900">{t("walkthrough.version.headerMin")}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="text-gray-800">
|
|
||||||
{matrixRows.map((row, idx) => (
|
|
||||||
<tr key={idx}>
|
|
||||||
<td className="border border-gray-200 px-3 py-2">{row.kernel}</td>
|
|
||||||
<td className="border border-gray-200 px-3 py-2">{row.pve}</td>
|
|
||||||
<td className="border border-gray-200 px-3 py-2">
|
|
||||||
<code>{row.minCode}</code>{row.minTail}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Callout variant="tip" title={t("walkthrough.version.whyTitle")}>
|
<Callout variant="tip" title={t("walkthrough.version.whyTitle")}>
|
||||||
{t("walkthrough.version.whyBody")}
|
{t("walkthrough.version.whyBody")}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ export default async function SwitchGpuModePage({
|
|||||||
Validations
|
Validations
|
||||||
├─ SR-IOV VF / active PF? → block
|
├─ SR-IOV VF / active PF? → block
|
||||||
├─ Target = VM and blocked ID? → block
|
├─ Target = VM and blocked ID? → block
|
||||||
└─ IOMMU parameter present? → warn if missing
|
└─ IOMMU parameter present? → auto-add to boot
|
||||||
|
cmdline if missing
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Find affected workloads
|
Find affected workloads
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ export default async function UpdatesTabPage({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<Callout variant="tip">
|
||||||
|
{t.rich("mechanisms.officialReference", { helper: linkHelper })}
|
||||||
|
</Callout>
|
||||||
<Callout variant="warning">{t.rich("mechanisms.callout", { strong, em, code })}</Callout>
|
<Callout variant="warning">{t.rich("mechanisms.callout", { strong, em, code })}</Callout>
|
||||||
|
|
||||||
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("docker.heading")}</h2>
|
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("docker.heading")}</h2>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export async function generateMetadata({
|
|||||||
"proxmox discord",
|
"proxmox discord",
|
||||||
"proxmox email alerts",
|
"proxmox email alerts",
|
||||||
"proxmox gotify",
|
"proxmox gotify",
|
||||||
|
"proxmox pushover",
|
||||||
"proxmox apprise",
|
"proxmox apprise",
|
||||||
"proxmox ntfy",
|
"proxmox ntfy",
|
||||||
"proxmox matrix notifications",
|
"proxmox matrix notifications",
|
||||||
@@ -72,6 +73,7 @@ export default async function NotificationsPage({
|
|||||||
discord: { items: string[] }
|
discord: { items: string[] }
|
||||||
gotify: { items: string[] }
|
gotify: { items: string[] }
|
||||||
email: { gmailItems: string[]; outlookItems: string[] }
|
email: { gmailItems: string[]; outlookItems: string[] }
|
||||||
|
pushover: { steps: string[] }
|
||||||
apprise: { listItems: string[]; steps: string[] }
|
apprise: { listItems: string[]; steps: string[] }
|
||||||
rich: { togglesItems: string[] }
|
rich: { togglesItems: string[] }
|
||||||
quiet: { purposeItems: string[]; howItems: string[] }
|
quiet: { purposeItems: string[]; howItems: string[] }
|
||||||
@@ -101,6 +103,7 @@ export default async function NotificationsPage({
|
|||||||
const gotifyItems = n.gotify.items
|
const gotifyItems = n.gotify.items
|
||||||
const gmailItems = n.email.gmailItems
|
const gmailItems = n.email.gmailItems
|
||||||
const outlookItems = n.email.outlookItems
|
const outlookItems = n.email.outlookItems
|
||||||
|
const pushoverSteps = n.pushover.steps
|
||||||
const appriseListItems = n.apprise.listItems
|
const appriseListItems = n.apprise.listItems
|
||||||
const appriseSteps = n.apprise.steps
|
const appriseSteps = n.apprise.steps
|
||||||
const togglesItems = n.rich.togglesItems
|
const togglesItems = n.rich.togglesItems
|
||||||
@@ -152,7 +155,7 @@ export default async function NotificationsPage({
|
|||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
description={t("header.description")}
|
description={t("header.description")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
estimatedMinutes={18}
|
estimatedMinutes={20}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -392,6 +395,34 @@ export default async function NotificationsPage({
|
|||||||
{t("email.relayBody")}
|
{t("email.relayBody")}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
<h3 id="pushover" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("pushover.heading")}</h3>
|
||||||
|
|
||||||
|
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||||
|
{t.rich("pushover.intro", { a: ext("https://pushover.net/api") })}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("pushover.stepsTitle")}</h4>
|
||||||
|
|
||||||
|
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
|
||||||
|
{pushoverSteps.map((_, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
{t.rich(`pushover.steps.${idx}`, {
|
||||||
|
em,
|
||||||
|
code,
|
||||||
|
a: ext(idx === 2 ? "https://pushover.net/apps/build" : "https://pushover.net/"),
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<Callout variant="info" title={t("pushover.priorityTitle")}>
|
||||||
|
{t.rich("pushover.priorityBody", { strong })}
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<Callout variant="warning" title={t("pushover.secretTitle")}>
|
||||||
|
{t("pushover.secretBody")}
|
||||||
|
</Callout>
|
||||||
|
|
||||||
<h3 id="apprise" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("apprise.heading")}</h3>
|
<h3 id="apprise" className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("apprise.heading")}</h3>
|
||||||
|
|
||||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("apprise.intro")}</p>
|
<p className="mb-4 text-gray-800 leading-relaxed">{t("apprise.intro")}</p>
|
||||||
@@ -402,7 +433,7 @@ export default async function NotificationsPage({
|
|||||||
{appriseListItems.map((_, idx) => (
|
{appriseListItems.map((_, idx) => (
|
||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
{t.rich(`apprise.listItems.${idx}`, {
|
{t.rich(`apprise.listItems.${idx}`, {
|
||||||
a: idx === 0 ? ext("https://github.com/caronc/apprise/wiki") : ext("https://github.com/caronc/apprise/wiki/URLBasics"),
|
a: idx === 0 ? ext("https://appriseit.com/services/") : ext("https://github.com/caronc/apprise/wiki/URLBasics"),
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -412,7 +443,7 @@ export default async function NotificationsPage({
|
|||||||
|
|
||||||
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
|
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
|
||||||
{appriseSteps.map((_, idx) => (
|
{appriseSteps.map((_, idx) => (
|
||||||
<li key={idx}>{t.rich(`apprise.steps.${idx}`, { em, code, a: ext("https://github.com/caronc/apprise/wiki") })}</li>
|
<li key={idx}>{t.rich(`apprise.steps.${idx}`, { em, code, a: ext("https://appriseit.com/services/") })}</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default async function AutomatedPage({
|
|||||||
<td className="px-4 py-2 text-gray-500 font-mono">{i + 1}</td>
|
<td className="px-4 py-2 text-gray-500 font-mono">{i + 1}</td>
|
||||||
<td className="px-4 py-2 font-semibold">{o.tool}</td>
|
<td className="px-4 py-2 font-semibold">{o.tool}</td>
|
||||||
<td className="px-4 py-2 text-gray-700 leading-relaxed">
|
<td className="px-4 py-2 text-gray-700 leading-relaxed">
|
||||||
{t.rich(`optimizations.${i}.what`, { link: log2ramLink })}
|
{t.rich(`optimizations.${i}.what`, { link: log2ramLink, code: (chunks) => <code>{chunks}</code>, strong: (chunks) => <strong>{chunks}</strong>, em: (chunks) => <em>{chunks}</em> })}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default async function PostInstallBasicSettingsPage({
|
|||||||
<DocHeader
|
<DocHeader
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={10}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -229,8 +230,7 @@ Acquire::Languages "none";`}
|
|||||||
|
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`# Remove a utility you no longer want
|
code={`# Remove a utility you no longer want
|
||||||
apt purge htop
|
apt purge htop`}
|
||||||
apt autoremove --purge`}
|
|
||||||
className="my-4"
|
className="my-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default async function PostInstallCustomizationPage({
|
|||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
description={t("header.description")}
|
description={t("header.description")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={7}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -142,7 +143,7 @@ cat /etc/motd
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="tip" title={t("verify.reversibleTitle")}>
|
<Callout variant="tip" title={t("verify.reversibleTitle")}>
|
||||||
{t.rich("verify.reversibleBody", { code, link: uninstallLink })}
|
{t.rich("verify.reversibleBody", { code, strong, link: uninstallLink })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("related.heading")}</h2>
|
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("related.heading")}</h2>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export default async function PostInstallMonitoringPage({
|
|||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
description={t("header.description")}
|
description={t("header.description")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={6}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -110,10 +111,6 @@ journalctl -u ovh-rtm --since "10 min ago"`}
|
|||||||
|
|
||||||
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("ovh.troubleTitle")}</h3>
|
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("ovh.troubleTitle")}</h3>
|
||||||
|
|
||||||
<Callout variant="tip" title={t("ovh.spuriousTitle")}>
|
|
||||||
{t.rich("ovh.spuriousBody", { em, code })}
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
<Callout variant="tip" title={t("ovh.revertTitle")}>
|
<Callout variant="tip" title={t("ovh.revertTitle")}>
|
||||||
{t.rich("ovh.revertBody", { code })}
|
{t.rich("ovh.revertBody", { code })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default async function PostInstallNetworkPage({
|
|||||||
<DocHeader
|
<DocHeader
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={8}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -116,7 +117,7 @@ export default async function PostInstallNetworkPage({
|
|||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Callout variant="warning" title={t("ovs.revertTitle")}>
|
<Callout variant="warning" title={t("ovs.revertTitle")}>
|
||||||
{t("ovs.revertBody")}
|
{t.rich("ovs.revertBody", { code, em, link: uninstallLink })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
@@ -158,8 +159,8 @@ sysctl net.ipv4.tcp_fastopen`}
|
|||||||
{t("bbr.impactBody")}
|
{t("bbr.impactBody")}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Callout variant="warning" title={t("bbr.revertTitle")}>
|
<Callout variant="tip" title={t("bbr.revertTitle")}>
|
||||||
{t("bbr.revertBody")}
|
{t.rich("bbr.revertBody", { code, link: uninstallLink })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
|
|||||||
@@ -38,10 +38,13 @@ export async function generateMetadata({
|
|||||||
|
|
||||||
type Logo = { name: string; alt: string; src: string }
|
type Logo = { name: string; alt: string; src: string }
|
||||||
|
|
||||||
function StepNumber({ number }: { number: number }) {
|
function StepHeading({ number, label, title }: { number: number; label: string; title: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center justify-center w-8 h-8 mr-3 text-white bg-blue-500 rounded-full">
|
<div className="flex items-center gap-3 mt-16 mb-4">
|
||||||
<span className="text-sm font-bold">{number}</span>
|
<span className="inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-2.5 py-0.5 text-xs font-semibold text-blue-800">
|
||||||
|
{label} {number}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-xl font-semibold m-0">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -60,7 +63,7 @@ export default async function OptionalSettingsPage({
|
|||||||
ceph: { doesItems: string[] }
|
ceph: { doesItems: string[] }
|
||||||
amd: { doesItems: string[] }
|
amd: { doesItems: string[] }
|
||||||
ha: { doesItems: string[] }
|
ha: { doesItems: string[] }
|
||||||
testing: { doesItems: string[] }
|
pveam: { doesItems: string[] }
|
||||||
fastfetch: { doesItems: string[]; customItems: string[]; logos: Logo[] }
|
fastfetch: { doesItems: string[]; customItems: string[]; logos: Logo[] }
|
||||||
figurine: { doesItems: string[] }
|
figurine: { doesItems: string[] }
|
||||||
log2ram: { doesItems: string[] }
|
log2ram: { doesItems: string[] }
|
||||||
@@ -69,7 +72,7 @@ export default async function OptionalSettingsPage({
|
|||||||
const cephItems = messages.docs.postInstall.optional.ceph.doesItems
|
const cephItems = messages.docs.postInstall.optional.ceph.doesItems
|
||||||
const amdItems = messages.docs.postInstall.optional.amd.doesItems
|
const amdItems = messages.docs.postInstall.optional.amd.doesItems
|
||||||
const haItems = messages.docs.postInstall.optional.ha.doesItems
|
const haItems = messages.docs.postInstall.optional.ha.doesItems
|
||||||
const testingItems = messages.docs.postInstall.optional.testing.doesItems
|
const pveamItems = messages.docs.postInstall.optional.pveam.doesItems
|
||||||
const fastfetchItems = messages.docs.postInstall.optional.fastfetch.doesItems
|
const fastfetchItems = messages.docs.postInstall.optional.fastfetch.doesItems
|
||||||
const fastfetchCustomItems = messages.docs.postInstall.optional.fastfetch.customItems
|
const fastfetchCustomItems = messages.docs.postInstall.optional.fastfetch.customItems
|
||||||
const fastfetchLogos = messages.docs.postInstall.optional.fastfetch.logos
|
const fastfetchLogos = messages.docs.postInstall.optional.fastfetch.logos
|
||||||
@@ -91,10 +94,7 @@ export default async function OptionalSettingsPage({
|
|||||||
</p>
|
</p>
|
||||||
<h2 className="text-2xl font-semibold mt-8 mb-4">{t("available")}</h2>
|
<h2 className="text-2xl font-semibold mt-8 mb-4">{t("available")}</h2>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={1} label={t("stepLabel")} title={t("ceph.title")} />
|
||||||
<StepNumber number={1} />
|
|
||||||
{t("ceph.title")}
|
|
||||||
</h3>
|
|
||||||
<p className="mb-4">{t("ceph.intro")}</p>
|
<p className="mb-4">{t("ceph.intro")}</p>
|
||||||
<p className="mb-4">{t("ceph.doesIntro")}</p>
|
<p className="mb-4">{t("ceph.doesIntro")}</p>
|
||||||
<ul className="list-disc pl-5 mb-4">
|
<ul className="list-disc pl-5 mb-4">
|
||||||
@@ -106,8 +106,18 @@ export default async function OptionalSettingsPage({
|
|||||||
<p className="text-lg mb-2">{t("ceph.automates")}</p>
|
<p className="text-lg mb-2">{t("ceph.automates")}</p>
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`
|
code={`
|
||||||
# Add Ceph repository
|
# On Proxmox VE 9 (Debian trixie) — deb822 format
|
||||||
echo "deb https://download.proxmox.com/debian/ceph-squid $(lsb_release -cs) no-subscription" > /etc/apt/sources.list.d/ceph-squid.list
|
cat > /etc/apt/sources.list.d/ceph.sources <<'EOF'
|
||||||
|
Types: deb
|
||||||
|
URIs: http://download.proxmox.com/debian/ceph-squid
|
||||||
|
Suites: trixie
|
||||||
|
Components: no-subscription
|
||||||
|
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# On Proxmox VE 8 (Debian bookworm) — legacy one-liner
|
||||||
|
# echo "deb https://download.proxmox.com/debian/ceph-squid $(lsb_release -cs) no-subscription" \\
|
||||||
|
# > /etc/apt/sources.list.d/ceph-squid.list
|
||||||
|
|
||||||
# Update package lists
|
# Update package lists
|
||||||
apt-get update
|
apt-get update
|
||||||
@@ -120,10 +130,7 @@ pveceph status
|
|||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={2} label={t("stepLabel")} title={t("amd.title")} />
|
||||||
<StepNumber number={2} />
|
|
||||||
{t("amd.title")}
|
|
||||||
</h3>
|
|
||||||
<p className="mb-4">{t("amd.intro")}</p>
|
<p className="mb-4">{t("amd.intro")}</p>
|
||||||
<p className="mb-4">{t("amd.doesIntro")}</p>
|
<p className="mb-4">{t("amd.doesIntro")}</p>
|
||||||
<ul className="list-disc pl-5 mb-4">
|
<ul className="list-disc pl-5 mb-4">
|
||||||
@@ -135,23 +142,21 @@ pveceph status
|
|||||||
<p className="text-lg mb-2">{t("amd.automates")}</p>
|
<p className="text-lg mb-2">{t("amd.automates")}</p>
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`
|
code={`
|
||||||
# Set kernel parameter
|
# Set kernel parameter — GRUB path
|
||||||
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="/GRUB_CMDLINE_LINUX_DEFAULT="idle=nomwait /g' /etc/default/grub
|
sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="/GRUB_CMDLINE_LINUX_DEFAULT="idle=nomwait /g' /etc/default/grub
|
||||||
update-grub
|
update-grub
|
||||||
|
|
||||||
|
# Set kernel parameter — systemd-boot path (ZFS-on-root)
|
||||||
|
# Adds 'idle=nomwait' to /etc/kernel/cmdline and runs:
|
||||||
|
# proxmox-boot-tool refresh
|
||||||
|
|
||||||
# Configure KVM
|
# Configure KVM
|
||||||
echo "options kvm ignore_msrs=Y" >> /etc/modprobe.d/kvm.conf
|
echo "options kvm ignore_msrs=Y" >> /etc/modprobe.d/kvm.conf
|
||||||
echo "options kvm report_ignored_msrs=N" >> /etc/modprobe.d/kvm.conf
|
echo "options kvm report_ignored_msrs=N" >> /etc/modprobe.d/kvm.conf
|
||||||
|
|
||||||
# Install latest Proxmox VE kernel
|
|
||||||
apt-get install pve-kernel-$(uname -r | cut -d'-' -f1-2)
|
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={3} label={t("stepLabel")} title={t("ha.title")} />
|
||||||
<StepNumber number={3} />
|
|
||||||
{t("ha.title")}
|
|
||||||
</h3>
|
|
||||||
<p className="mb-4">{t("ha.intro")}</p>
|
<p className="mb-4">{t("ha.intro")}</p>
|
||||||
<p className="mb-4">{t("ha.doesIntro")}</p>
|
<p className="mb-4">{t("ha.doesIntro")}</p>
|
||||||
<ul className="list-disc pl-5 mb-4">
|
<ul className="list-disc pl-5 mb-4">
|
||||||
@@ -167,39 +172,23 @@ systemctl enable --now pve-ha-lrm pve-ha-crm corosync
|
|||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={4} label={t("stepLabel")} title={t("pveam.title")} />
|
||||||
<StepNumber number={4} />
|
<p className="mb-4">{t.rich("pveam.intro", { code })}</p>
|
||||||
{t("testing.title")}
|
<p className="mb-4">{t("pveam.doesIntro")}</p>
|
||||||
</h3>
|
|
||||||
<p className="mb-4">{t("testing.intro")}</p>
|
|
||||||
<p className="mb-4">{t("testing.doesIntro")}</p>
|
|
||||||
<ul className="list-disc pl-5 mb-4">
|
<ul className="list-disc pl-5 mb-4">
|
||||||
{testingItems.map((_, idx) => (
|
{pveamItems.map((_, idx) => (
|
||||||
<li key={idx}>{t(`testing.doesItems.${idx}`)}</li>
|
<li key={idx}>{t.rich(`pveam.doesItems.${idx}`, { code })}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mb-4">{t("testing.howUse")}</p>
|
<p className="mb-4">{t("pveam.howUse")}</p>
|
||||||
<p className="text-lg mb-2">{t("testing.manualIntro")}</p>
|
<p className="text-lg mb-2">{t("pveam.automates")}</p>
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`
|
code={`
|
||||||
# Add Proxmox testing repository
|
pveam update
|
||||||
echo "deb http://download.proxmox.com/debian/pve $(lsb_release -cs) pvetest" | sudo tee /etc/apt/sources.list.d/pve-testing-repo.list
|
|
||||||
|
|
||||||
# Update package lists
|
|
||||||
sudo apt update
|
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<p className="mt-4 text-sm text-gray-600">
|
|
||||||
<strong>{t("testing.noteLabel")}</strong> {t("testing.noteBody")}
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 text-yellow-600">
|
|
||||||
<strong>{t("testing.warnLabel")}</strong> {t("testing.warnBody")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={5} label={t("stepLabel")} title={t("fastfetch.title")} />
|
||||||
<StepNumber number={5} />
|
|
||||||
{t("fastfetch.title")}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="mb-4">{t("fastfetch.intro")}</p>
|
<p className="mb-4">{t("fastfetch.intro")}</p>
|
||||||
|
|
||||||
@@ -255,24 +244,39 @@ systemctl enable --now pve-ha-lrm pve-ha-crm corosync
|
|||||||
<p className="text-lg mb-2">{t("fastfetch.automates")}</p>
|
<p className="text-lg mb-2">{t("fastfetch.automates")}</p>
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`
|
code={`
|
||||||
# Download and install the latest version of Fastfetch
|
# Remove any previous install so the newest .deb lands clean
|
||||||
FASTFETCH_URL=$(curl -s https://api.github.com/repos/fastfetch-cli/fastfetch/releases/latest | grep "browser_download_url.*fastfetch-linux-amd64.deb" | cut -d '"' -f 4)
|
apt-get remove --purge -y fastfetch 2>/dev/null
|
||||||
|
rm -f /usr/bin/fastfetch /usr/local/bin/fastfetch
|
||||||
|
|
||||||
|
# Download and install the latest .deb via the GitHub Releases API
|
||||||
|
FASTFETCH_URL=$(curl -sSf --connect-timeout 5 --max-time 15 \\
|
||||||
|
https://api.github.com/repos/fastfetch-cli/fastfetch/releases/latest \\
|
||||||
|
| jq -r '.assets[] | select(.name | test("fastfetch-linux-amd64.deb")) | .browser_download_url')
|
||||||
wget -q -O /tmp/fastfetch.deb "$FASTFETCH_URL"
|
wget -q -O /tmp/fastfetch.deb "$FASTFETCH_URL"
|
||||||
dpkg -i /tmp/fastfetch.deb
|
dpkg -i /tmp/fastfetch.deb
|
||||||
apt-get install -f -y
|
apt-get install -f -y
|
||||||
|
|
||||||
# Configure Fastfetch (logo selection remains interactive)
|
# Configure Fastfetch (logo selection remains interactive)
|
||||||
# The configuration is done through a series of jq commands
|
# The configuration is done through a series of jq commands.
|
||||||
|
# A custom "System optimised by ProxMenux" line is prepended
|
||||||
|
# to the modules array so it shows above the standard sections.
|
||||||
|
|
||||||
# Set Fastfetch to run at login
|
# Wire Fastfetch into ~/.bashrc — inside a marker block so the
|
||||||
echo "clear && fastfetch" >> ~/.bashrc
|
# same block can be replaced on re-run without polluting the file.
|
||||||
|
# The block is guarded so it only runs on interactive shells that
|
||||||
|
# actually have the fastfetch binary available.
|
||||||
|
cat >> ~/.bashrc <<'EOF'
|
||||||
|
# BEGIN FASTFETCH
|
||||||
|
if [[ $- == *i* ]] && command -v fastfetch >/dev/null 2>&1; then
|
||||||
|
clear
|
||||||
|
fastfetch
|
||||||
|
fi
|
||||||
|
# END FASTFETCH
|
||||||
|
EOF
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={6} label={t("stepLabel")} title={t("figurine.title")} />
|
||||||
<StepNumber number={6} />
|
|
||||||
{t("figurine.title")}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="mb-4">{t("figurine.intro")}</p>
|
<p className="mb-4">{t("figurine.intro")}</p>
|
||||||
|
|
||||||
@@ -330,10 +334,7 @@ chmod +x "/etc/profile.d/figurine.sh"
|
|||||||
|
|
||||||
<p className="mt-4">{t("figurine.outro")}</p>
|
<p className="mt-4">{t("figurine.outro")}</p>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
|
<StepHeading number={7} label={t("stepLabel")} title={t("log2ram.title")} />
|
||||||
<StepNumber number={7} />
|
|
||||||
{t("log2ram.title")}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
{t.rich("log2ram.intro", { code, em })}
|
{t.rich("log2ram.intro", { code, em })}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export default async function PostInstallPage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("threeWays.heading")}</h2>
|
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("threeWays.heading")}</h2>
|
||||||
<p className="mb-6 text-gray-800 leading-relaxed">{t("threeWays.body")}</p>
|
<p className="mb-6 text-gray-800 leading-relaxed">{t.rich("threeWays.body", { em: (chunks) => <em>{chunks}</em> })}</p>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8 not-prose">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8 not-prose">
|
||||||
{ROUTE_CONFIG.map(({ key, href, Icon, accent, iconBg }, idx) => {
|
{ROUTE_CONFIG.map(({ key, href, Icon, accent, iconBg }, idx) => {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default async function PostInstallPerformancePage({
|
|||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
description={t("header.description")}
|
description={t("header.description")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -77,6 +78,10 @@ export default async function PostInstallPerformancePage({
|
|||||||
sed -i "s/#pigz:.*/pigz: 1/" /etc/vzdump.conf
|
sed -i "s/#pigz:.*/pigz: 1/" /etc/vzdump.conf
|
||||||
apt-get -y install pigz
|
apt-get -y install pigz
|
||||||
|
|
||||||
|
# The wrapper pins PATH so gzip resolves inside the script even under sudo,
|
||||||
|
# and sets GZIP="-1" — fastest compression level, lowest ratio. The trade
|
||||||
|
# is intentional: on multi-core hosts wall-clock time drops far more than
|
||||||
|
# the archive grows.
|
||||||
cat > /bin/pigzwrapper <<'EOF'
|
cat > /bin/pigzwrapper <<'EOF'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
PATH=/bin:$PATH
|
PATH=/bin:$PATH
|
||||||
@@ -95,8 +100,8 @@ chmod +x /bin/pigzwrapper
|
|||||||
{t.rich("pigz.replacesBody", { code })}
|
{t.rich("pigz.replacesBody", { code })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Callout variant="danger" title={t("pigz.revertTitle")}>
|
<Callout variant="tip" title={t("pigz.revertTitle")}>
|
||||||
{t.rich("pigz.revertBody", { strong })}
|
{t.rich("pigz.revertBody", { strong, link: (chunks) => <Link href="/docs/post-install/uninstall" className="text-blue-600 hover:underline">{chunks}</Link>, code: (chunks) => <code>{chunks}</code> })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default async function PostInstallSecurityPage({
|
|||||||
<DocHeader
|
<DocHeader
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -72,9 +73,8 @@ export default async function PostInstallSecurityPage({
|
|||||||
|
|
||||||
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("rpcbind.runsTitle")}</h3>
|
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("rpcbind.runsTitle")}</h3>
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`# Stop and disable the rpcbind service
|
code={`# Stop and disable both activation paths
|
||||||
systemctl stop rpcbind
|
systemctl disable --now rpcbind.socket rpcbind.service`}
|
||||||
systemctl disable rpcbind`}
|
|
||||||
className="my-4"
|
className="my-4"
|
||||||
/>
|
/>
|
||||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("rpcbind.runsOutro")}</p>
|
<p className="mb-4 text-gray-800 leading-relaxed">{t("rpcbind.runsOutro")}</p>
|
||||||
@@ -84,14 +84,14 @@ systemctl disable rpcbind`}
|
|||||||
{t.rich("rpcbind.verifyBody", { code })}
|
{t.rich("rpcbind.verifyBody", { code })}
|
||||||
</p>
|
</p>
|
||||||
<CopyableCode
|
<CopyableCode
|
||||||
code={`systemctl is-active rpcbind # should report: inactive
|
code={`systemctl is-active rpcbind.service rpcbind.socket
|
||||||
systemctl is-enabled rpcbind # should report: disabled
|
systemctl is-enabled rpcbind.service rpcbind.socket
|
||||||
ss -tulpn | grep ':111 ' # should return nothing`}
|
ss -tulpn | grep ':111 ' # should return nothing`}
|
||||||
className="my-4"
|
className="my-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="tip" title={t("rpcbind.reversibleTitle")}>
|
<Callout variant="warning" title={t("rpcbind.reversibleTitle")}>
|
||||||
{t.rich("rpcbind.reversibleBody", { em, link: uninstallLink })}
|
{t.rich("rpcbind.reversibleBody", { em, strong, code, link: uninstallLink })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("related.heading")}</h2>
|
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("related.heading")}</h2>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default async function PostInstallStoragePage({
|
|||||||
<DocHeader
|
<DocHeader
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={12}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -75,8 +76,8 @@ export default async function PostInstallStoragePage({
|
|||||||
})}
|
})}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Callout variant="warning" title={t("notTrackedTitle")}>
|
<Callout variant="tip" title={t("trackedTitle")}>
|
||||||
{t.rich("notTrackedBody", { strong })}
|
{t.rich("trackedBody", { strong, link: (chunks) => <Link href="/docs/post-install/uninstall" className="text-blue-600 hover:underline">{chunks}</Link>, code: (chunks) => <code>{chunks}</code> })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("arc.heading")}</h2>
|
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("arc.heading")}</h2>
|
||||||
@@ -286,8 +287,8 @@ ionice: 5 # Lower I/O priority (5 = best-effort class, lowest priority in
|
|||||||
className="my-4"
|
className="my-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="warning" title={t("vzdump.noBackupTitle")}>
|
<Callout variant="tip" title={t("vzdump.backupTitle")}>
|
||||||
{t.rich("vzdump.noBackupBody", { strong, code, em })}
|
{t.rich("vzdump.backupBody", { strong, code })}
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Callout variant="tip" title={t("vzdump.skipTitle")}>
|
<Callout variant="tip" title={t("vzdump.skipTitle")}>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default async function PostInstallSystemPage({
|
|||||||
<DocHeader
|
<DocHeader
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={10}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -89,7 +90,9 @@ export default async function PostInstallSystemPage({
|
|||||||
daily
|
daily
|
||||||
su root adm
|
su root adm
|
||||||
rotate 7
|
rotate 7
|
||||||
create
|
create 0640 root adm
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
compress
|
compress
|
||||||
size 10M
|
size 10M
|
||||||
delaycompress
|
delaycompress
|
||||||
@@ -141,9 +144,10 @@ include /etc/logrotate.d`}
|
|||||||
vm.swappiness = 10 # Avoid swapping unless truly necessary
|
vm.swappiness = 10 # Avoid swapping unless truly necessary
|
||||||
vm.dirty_ratio = 15 # Start writeback sooner (default 20)
|
vm.dirty_ratio = 15 # Start writeback sooner (default 20)
|
||||||
vm.dirty_background_ratio = 5 # Start async writeback earlier (default 10)
|
vm.dirty_background_ratio = 5 # Start async writeback earlier (default 10)
|
||||||
vm.overcommit_memory = 1 # Allow overcommit (needed by many applications)
|
|
||||||
vm.max_map_count = 262144 # Enough for modern apps (ES, Docker, some games)
|
vm.max_map_count = 262144 # Enough for modern apps (ES, Docker, some games)
|
||||||
vm.compaction_proactiveness = 20 # Only on kernels that support it`}
|
vm.compaction_proactiveness = 20 # Only on kernels that support it
|
||||||
|
# Note: the kernel's memory-overcommit policy (vm.overcommit_memory) is
|
||||||
|
# NOT modified — Proxmox's default is left in place.`}
|
||||||
className="my-4"
|
className="my-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default async function UninstallOptimizationsPage({
|
|||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
description={t("header.description")}
|
description={t("header.description")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={8}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ export default async function PostInstallUpdatesPage({
|
|||||||
))}
|
))}
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
<Callout variant="warning" title={t("applying.jqTitle")}>
|
||||||
|
{t.rich("applying.jqBody", { code })}
|
||||||
|
</Callout>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("differs.heading")}</h2>
|
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("differs.heading")}</h2>
|
||||||
|
|
||||||
<div className="overflow-x-auto mb-6">
|
<div className="overflow-x-auto mb-6">
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default async function PostInstallVirtualizationPage({
|
|||||||
<DocHeader
|
<DocHeader
|
||||||
title={t("header.title")}
|
title={t("header.title")}
|
||||||
section={t("header.section")}
|
section={t("header.section")}
|
||||||
|
estimatedMinutes={9}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Callout variant="info" title={t("intro.title")}>
|
<Callout variant="info" title={t("intro.title")}>
|
||||||
@@ -146,6 +147,10 @@ pcie_acs_override=downstream,multifunction`}
|
|||||||
className="my-4"
|
className="my-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Callout variant="warning" title={t("vfio.acsTitle")}>
|
||||||
|
{t.rich("vfio.acsBody", { code, strong })}
|
||||||
|
</Callout>
|
||||||
|
|
||||||
<p className="mb-3 text-gray-800 leading-relaxed">
|
<p className="mb-3 text-gray-800 leading-relaxed">
|
||||||
{t.rich("vfio.modulesIntro", { code })}
|
{t.rich("vfio.modulesIntro", { code })}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
import Navbar from "@/components/navbar"
|
import Navbar from "@/components/navbar"
|
||||||
import MouseMoveEffect from "@/components/mouse-move-effect"
|
|
||||||
import { PagefindHighlighter } from "@/components/pagefind-highlighter"
|
import { PagefindHighlighter } from "@/components/pagefind-highlighter"
|
||||||
import { LocaleHtmlSync } from "@/components/locale-html-sync"
|
import { LocaleHtmlSync } from "@/components/locale-html-sync"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
@@ -175,7 +174,6 @@ export default async function LocaleLayout({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<MouseMoveEffect />
|
|
||||||
<div className="pt-16 md:pt-16">{children}</div>
|
<div className="pt-16 md:pt-16">{children}</div>
|
||||||
<script src="/pagefind/pagefind-highlight.js" type="module" defer />
|
<script src="/pagefind/pagefind-highlight.js" type="module" defer />
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
export default function MouseMoveEffect() {
|
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
|
||||||
setMousePosition({ x: event.clientX, y: event.clientY })
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("mousemove", handleMouseMove)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("mousemove", handleMouseMove)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="pointer-events-none fixed inset-0 z-30 transition-opacity duration-300"
|
|
||||||
style={{
|
|
||||||
background: `radial-gradient(600px at ${mousePosition.x}px ${mousePosition.y}px, rgba(29, 78, 216, 0.15), transparent 80%)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Synology VM Creator Script",
|
"title": "Synology VM Creator Script",
|
||||||
|
"stepLabel": "Step",
|
||||||
"intro": {
|
"intro": {
|
||||||
"heading": "Introduction",
|
"heading": "Introduction",
|
||||||
"intro": "ProxMenux provides an automated script that creates and configures a virtual machine (VM) to install Synology DSM (DiskStation Manager) on Proxmox VE. This script simplifies the process by downloading and adding one of the available loaders to the VM boot, giving you the option between four different choices:",
|
"intro": "ProxMenux provides an automated script that creates and configures a virtual machine (VM) to install Synology DSM (DiskStation Manager) on Proxmox VE. This script simplifies the process by downloading and adding one of the available loaders to the VM boot, giving you the option between four different choices:",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"heading": "Walking through the flow",
|
"heading": "Walking through the flow",
|
||||||
"detect": {
|
"detect": {
|
||||||
"title": "Detect GPUs and check IOMMU",
|
"title": "Detect GPUs and check IOMMU",
|
||||||
"body": "The script lists every GPU it finds. If IOMMU isn't already enabled in the running kernel cmdline, you'll get a yes/no prompt to append <code>intel_iommu=on</code> (or <code>amd_iommu=on</code>) + <code>iommu=pt</code> to the right boot file — <code>/etc/kernel/cmdline</code> on ZFS (systemd-boot) or <code>/etc/default/grub</code> on LVM/ext4. If you accept and the kernel cmdline changes, the script flags that the reboot prompt at the end will be required.",
|
"body": "The script lists every GPU it finds. If IOMMU isn't already enabled in the running kernel cmdline, you'll get a yes/no prompt to append <code>intel_iommu=on</code> (or <code>amd_iommu=on</code>) + <code>iommu=pt</code> to the right boot file. Selection is based on the bootloader: <code>/etc/kernel/cmdline</code> when the host boots via systemd-boot (detected by the presence of <code>root=ZFS=</code> in that file, the default on ProxmoxVE-installed ZFS-on-root systems), otherwise <code>/etc/default/grub</code>. If you accept and the kernel cmdline changes, the script flags that a reboot will be needed at the end.",
|
||||||
"tipTitle": "Already ran post-install?",
|
"tipTitle": "Already ran post-install?",
|
||||||
"tipBody": "If you previously enabled <postLink>VFIO IOMMU support</postLink> from the post-install scripts, IOMMU is already on and this step silently passes. Good.",
|
"tipBody": "If you previously enabled <postLink>VFIO IOMMU support</postLink> from the post-install scripts, IOMMU is already on and this step silently passes. Good.",
|
||||||
"imageAlt": "List of detected GPUs with vendor and PCI address"
|
"imageAlt": "List of detected GPUs with vendor and PCI address"
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
"intro": "The script scans every VM config and every LXC config on the host looking for the GPU you picked. Three possible outcomes:",
|
"intro": "The script scans every VM config and every LXC config on the host looking for the GPU you picked. Three possible outcomes:",
|
||||||
"items": [
|
"items": [
|
||||||
"<strong>GPU is free.</strong> Nothing to do, continue.",
|
"<strong>GPU is free.</strong> Nothing to do, continue.",
|
||||||
"<strong>GPU is in a different VM.</strong> You're offered to remove it from that other VM before assigning it here. If you decline, the script aborts — two VMs can't share an exclusive VFIO assignment.",
|
"<strong>GPU is in a different VM.</strong> If the source VM is currently running, the script aborts — two VMs can't share an exclusive VFIO assignment, and the source VM has to be stopped first. If the source VM is stopped, you get two options: <em>Keep GPU in the source VM's config but disable Start on boot</em>, or <em>Remove the GPU lines from the source VM's config and keep Start on boot</em>. A fast-path also exists: if the GPU is already bound to <code>vfio-pci</code> and the source VM is a plain VM→VM swap, no host reconfiguration is done and no reboot is needed.",
|
||||||
"<strong>GPU is in an LXC (shared mode).</strong> You're offered to remove the LXC passthrough configuration (<code>lxc.cgroup2.devices.allow</code> + <code>lxc.mount.entry</code> lines). The LXC won't see the GPU anymore, but the VM will — this is the \"switch mode\" mechanic that gives this menu entry its secondary label."
|
"<strong>GPU is in an LXC (shared mode).</strong> You get two options on a menu: <em>Keep GPU in the LXC config but disable Start on boot</em>, or <em>Remove the GPU lines from the LXC config (<code>lxc.cgroup2.devices.allow</code> / <code>lxc.mount.entry</code>) and keep Start on boot</em>. Either way the LXC won't see the GPU after the switch, and the VM will — this is the \"switch mode\" mechanic that gives this menu entry its secondary label."
|
||||||
],
|
],
|
||||||
"imageAlt": "Dialog offering to remove the GPU from an LXC before assigning it to the VM",
|
"imageAlt": "Dialog offering to remove the GPU from an LXC before assigning it to the VM",
|
||||||
"smartTitle": "Audio siblings are cleaned up smartly too",
|
"smartTitle": "Audio siblings are cleaned up smartly too",
|
||||||
@@ -100,10 +100,10 @@
|
|||||||
"<code>/etc/modules</code> — adds <code>vfio</code>, <code>vfio_iommu_type1</code>, <code>vfio_pci</code> (plus <code>vfio_virqfd</code> on kernels < 6.2).",
|
"<code>/etc/modules</code> — adds <code>vfio</code>, <code>vfio_iommu_type1</code>, <code>vfio_pci</code> (plus <code>vfio_virqfd</code> on kernels < 6.2).",
|
||||||
"<code>/etc/modprobe.d/vfio.conf</code> — for AMD / Intel, sets <code>options vfio-pci ids=<vendor:device,...> disable_vga=1</code> so VFIO claims the GPU early at boot. For NVIDIA the file only adds <code>softdep nvidia pre: vfio-pci</code> (plus <code>_drm</code>/<code>_modeset</code>/<code>_uvm</code>) — actual binding is per-BDF via the udev rule below. On AMD, also adds <code>softdep</code> lines forcing <code>vfio-pci</code> to load before <code>radeon</code> / <code>amdgpu</code>.",
|
"<code>/etc/modprobe.d/vfio.conf</code> — for AMD / Intel, sets <code>options vfio-pci ids=<vendor:device,...> disable_vga=1</code> so VFIO claims the GPU early at boot. For NVIDIA the file only adds <code>softdep nvidia pre: vfio-pci</code> (plus <code>_drm</code>/<code>_modeset</code>/<code>_uvm</code>) — actual binding is per-BDF via the udev rule below. On AMD, also adds <code>softdep</code> lines forcing <code>vfio-pci</code> to load before <code>radeon</code> / <code>amdgpu</code>.",
|
||||||
"<code>/etc/modprobe.d/iommu_unsafe_interrupts.conf</code> and <code>kvm.conf</code> — sensible workarounds that most Windows / macOS VMs need (<code>allow_unsafe_interrupts=1</code>, <code>ignore_msrs=1</code>).",
|
"<code>/etc/modprobe.d/iommu_unsafe_interrupts.conf</code> and <code>kvm.conf</code> — sensible workarounds that most Windows / macOS VMs need (<code>allow_unsafe_interrupts=1</code>, <code>ignore_msrs=1</code>).",
|
||||||
"<code>/etc/modprobe.d/blacklist.conf</code> — blacklists the open-source companion drivers (<code>nouveau</code>, <code>amdgpu</code>, <code>radeon</code>, <code>i915</code>) that would otherwise grab the GPU before VFIO. The proprietary <code>nvidia</code> module is <strong>never blacklisted</strong> — it stays available for any OTHER NVIDIA GPU you keep on the host.",
|
"<code>/etc/modprobe.d/blacklist.conf</code> — blacklists only the open-source drivers for the selected vendor (<code>nouveau</code>/<code>lbm-nouveau</code> for NVIDIA; <code>radeon</code>+<code>amdgpu</code> for AMD; <code>i915</code> for Intel), so drivers for other vendors on the same host stay loaded. The proprietary <code>nvidia</code> module is <strong>only</strong> blacklisted (via a separate <code>/etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf</code>) once <em>every</em> NVIDIA GPU on the host has been switched to VFIO — until that point it stays loaded so any NVIDIA card you keep on the host keeps working.",
|
||||||
"<code>/etc/udev/rules.d/10-proxmenux-vfio-bind.rules</code> + <code>/etc/proxmenux/vfio-bind.bdfs</code> — <strong>NVIDIA only</strong>. Per-BDF binding state. The udev rule applies <code>ATTR'{'driver_override'}'=\"vfio-pci\"</code> at the PCI ADD event for each tracked Bus:Device.Function, so only the GPU(s) you've explicitly passed go to VFIO. This is what makes multi-GPU NVIDIA work — your other NVIDIA cards keep their <code>nvidia</code> driver and stay usable on the host.",
|
"<code>/etc/udev/rules.d/10-proxmenux-vfio-bind.rules</code> + <code>/etc/proxmenux/vfio-bind.bdfs</code> — <strong>NVIDIA only</strong>. Per-BDF binding state. The udev rule applies <code>ATTR'{'driver_override'}'=\"vfio-pci\"</code> at the PCI ADD event for each tracked Bus:Device.Function, so only the GPU(s) you've explicitly passed go to VFIO. This is what makes multi-GPU NVIDIA work — your other NVIDIA cards keep their <code>nvidia</code> driver and stay usable on the host.",
|
||||||
"<strong>AMD only.</strong> Dumps the GPU ROM from sysfs (<code>/sys/bus/pci/.../rom</code>) or the ACPI VFCT table to <code>/usr/share/kvm/vbios_<card>.bin</code>. The VM references it via <code>romfile=</code> so cards that misreport their own VBIOS still initialise correctly.",
|
"<strong>AMD only.</strong> Dumps the GPU ROM from sysfs (<code>/sys/bus/pci/.../rom</code>) or the ACPI VFCT table to <code>/usr/share/kvm/vbios_<card>.bin</code>. The VM references it via <code>romfile=</code> so cards that misreport their own VBIOS still initialise correctly.",
|
||||||
"<strong>NVIDIA only.</strong> Stops and disables host NVIDIA services that could probe / lock the GPU at boot (<code>nvidia-persistenced</code>, <code>nvidia-powerd</code>, <code>nvidia-fabricmanager</code>). The <code>nvidia</code> module itself is left loaded so other NVIDIA GPUs on the host keep working with <code>nvidia-smi</code>.",
|
"<strong>NVIDIA only.</strong> Host NVIDIA services (<code>nvidia-persistenced</code>, <code>nvidia-powerd</code>, <code>nvidia-fabricmanager</code>) are stopped and disabled only when every NVIDIA GPU is assigned to VFIO. With a mixed host they remain active, together with the <code>nvidia</code> module, for the GPU(s) that stay native.",
|
||||||
"<code>update-initramfs -u -k all</code> — only runs if any of the above actually changed."
|
"<code>update-initramfs -u -k all</code> — only runs if any of the above actually changed."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Install NVIDIA Drivers on the Host | ProxMenux Documentation",
|
"title": "Install NVIDIA Drivers on the Host | ProxMenux Documentation",
|
||||||
"description": "Install and configure NVIDIA proprietary drivers on a Proxmox VE host using ProxMenux. Covers kernel compatibility, VFIO setup, persistence service, optional NVENC patch and automatic LXC propagation."
|
"description": "Install and configure NVIDIA proprietary drivers on a Proxmox VE host using ProxMenux. Covers GPU support filtering, DKMS validation, persistence service, optional NVENC patch and automatic LXC propagation."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Install NVIDIA Drivers on the Host",
|
"title": "Install NVIDIA Drivers on the Host",
|
||||||
"description": "Install the NVIDIA proprietary driver on a Proxmox VE host using ProxMenux. The installer handles kernel compatibility, nouveau blacklisting, VFIO configuration, persistence service and can propagate the driver to any LXC container that already has NVIDIA passthrough configured.",
|
"description": "Install the NVIDIA proprietary driver on a Proxmox VE host using ProxMenux. The installer filters maintained branches by GPU PCI ID, validates the selected release through DKMS, manages nouveau, installs the persistence service and can propagate the driver to LXC containers with NVIDIA passthrough.",
|
||||||
"section": "Hardware: GPUs and Coral-TPU"
|
"section": "Hardware: GPUs and Coral-TPU"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "What this does",
|
"title": "What this does",
|
||||||
"body": "ProxMenux automates the whole NVIDIA driver lifecycle on the host: detects your GPU, picks a driver version that is compatible with your running kernel, blacklists <code>nouveau</code>, downloads and runs the official NVIDIA <code>.run</code> installer with DKMS, installs the <code>nvidia-persistenced</code> service and udev rules, and offers to apply the optional NVENC patch. If you already have LXC containers with NVIDIA passthrough, it can update the userspace libraries inside them so their version matches the host."
|
"body": "ProxMenux automates the whole NVIDIA driver lifecycle on the host: detects your GPU, offers maintained NVIDIA branches that list its PCI Device ID, blacklists <code>nouveau</code>, downloads and runs the official NVIDIA <code>.run</code> installer with DKMS, installs the <code>nvidia-persistenced</code> service and udev rules, and offers to apply the optional NVENC patch. The DKMS build is the final compatibility check against the running kernel. If you already have LXC containers with NVIDIA passthrough, it can update their userspace libraries to match the host."
|
||||||
},
|
},
|
||||||
"who": {
|
"who": {
|
||||||
"heading": "Who is this for?",
|
"heading": "Who is this for?",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"gpuCheck": "lspci | grep -i nvidia",
|
"gpuCheck": "lspci | grep -i nvidia",
|
||||||
"notVm": "The GPU <strong>is not currently assigned to a VM via VFIO passthrough</strong>. If it is, the script will refuse to install the host driver to avoid breaking the passthrough config.",
|
"notVm": "The GPU <strong>is not currently assigned to a VM via VFIO passthrough</strong>. If it is, the script will refuse to install the host driver to avoid breaking the passthrough config.",
|
||||||
"internet": "Internet access on the host. The installer downloads the driver from <code>download.nvidia.com</code> and, optionally, clones <code>nvidia-persistenced</code> and <code>nvidia-patch</code> from GitHub.",
|
"internet": "Internet access on the host. The installer downloads the driver from <code>download.nvidia.com</code> and, optionally, clones <code>nvidia-persistenced</code> and <code>nvidia-patch</code> from GitHub.",
|
||||||
"space": "About <strong>2 GB of free space</strong> in <code>/opt/nvidia</code> (workdir) plus the RAM used during the install. A reboot is required at the end."
|
"space": "Some free space in <code>/opt/nvidia</code> for the <code>.run</code> installer plus the RAM used during the build. When propagating to LXCs on non-Arch distros, each container needs at least 1.5 GB free; ProxMenux temporarily raises container RAM to 2 GB and restores it after. A reboot is required at the end of the host install."
|
||||||
},
|
},
|
||||||
"vmWarn": {
|
"vmWarn": {
|
||||||
"title": "GPU assigned to a VM? Stop here",
|
"title": "GPU assigned to a VM? Stop here",
|
||||||
@@ -46,40 +46,11 @@
|
|||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"title": "Choose the driver version",
|
"title": "Choose the driver version",
|
||||||
"body1": "ProxMenux fetches the list of available drivers from NVIDIA and <strong>filters out versions that are not compatible with your running kernel</strong>. The <em>Latest available</em> option is almost always the right pick.",
|
"body1": "ProxMenux fetches the list of available drivers from NVIDIA and narrows the picker to versions that <strong>list your GPU's PCI Device ID in the supported chips table</strong> of the corresponding branch on <code>nvidia.com</code>. Additional heuristics discard developer / beta CDN drops that would otherwise appear at the top. The first entry is labelled <em><version> — Recommended</em>: it prefers the head of the branch of the driver already installed on the host (bugfix in place), otherwise the head of the current Production Branch, otherwise the highest supported numeric.",
|
||||||
"body2": "The compatibility matrix the script uses:",
|
"body2": "If the currently installed driver was patched via keylase (NVENC), the picker auto-narrows to versions still covered by the patch table, so applying <em>Reinstall / update</em> without losing the patch is one click.",
|
||||||
"headerKernel": "Kernel",
|
"whyTitle": "How kernel compatibility is validated",
|
||||||
"headerPve": "Typical PVE version",
|
"whyBody": "The version list is filtered by NVIDIA branch maintenance and GPU PCI support, not by a hard-coded kernel/driver matrix. After selection, DKMS builds the module against the running kernel. A failed build stops the installation from being treated as valid; choose another maintained branch if NVIDIA has not adapted that release to your kernel.",
|
||||||
"headerMin": "Minimum NVIDIA driver",
|
"imageAlt": "Driver version selector with GPU-supported NVIDIA branches and the Recommended entry on top"
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"kernel": "6.17+",
|
|
||||||
"pve": "Proxmox VE 9.x",
|
|
||||||
"minCode": "580.82.07",
|
|
||||||
"minTail": " or newer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kernel": "6.8 – 6.16",
|
|
||||||
"pve": "Proxmox VE 8.2+",
|
|
||||||
"minCode": "550.x",
|
|
||||||
"minTail": " or newer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kernel": "6.2 – 6.7",
|
|
||||||
"pve": "Proxmox VE 8.0 – 8.1",
|
|
||||||
"minCode": "535.x",
|
|
||||||
"minTail": " or newer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kernel": "5.15+",
|
|
||||||
"pve": "Proxmox VE 7.x (legacy)",
|
|
||||||
"minCode": "470.x",
|
|
||||||
"minTail": " or newer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"whyTitle": "Why kernel matters",
|
|
||||||
"whyBody": "Kernel 6.17 introduced internal API changes that break older NVIDIA drivers. If you install a driver below the minimum for your kernel, DKMS will fail to build the module and the GPU will not be available after reboot. ProxMenux filters the list so you can't pick an incompatible version by accident.",
|
|
||||||
"imageAlt": "Driver version selector with kernel-compatible versions, Latest available on top"
|
|
||||||
},
|
},
|
||||||
"uninstall": {
|
"uninstall": {
|
||||||
"title": "Clean uninstall (only if reinstalling)",
|
"title": "Clean uninstall (only if reinstalling)",
|
||||||
@@ -90,8 +61,8 @@
|
|||||||
"body": "Behind a single confirmation, the script:",
|
"body": "Behind a single confirmation, the script:",
|
||||||
"items": [
|
"items": [
|
||||||
"Installs <code>pve-headers-$(uname -r)</code> (or <code>proxmox-headers-$(uname -r)</code>), <code>build-essential</code> and <code>dkms</code>.",
|
"Installs <code>pve-headers-$(uname -r)</code> (or <code>proxmox-headers-$(uname -r)</code>), <code>build-essential</code> and <code>dkms</code>.",
|
||||||
"Creates <code>/etc/modprobe.d/nouveau-blacklist.conf</code> blacklisting <code>nouveau</code>, and tries to unload it immediately.",
|
"Creates the ProxMenux-owned <code>/etc/modprobe.d/proxmenux-nouveau-blacklist.conf</code> with <code>blacklist nouveau</code> and <code>options nouveau modeset=0</code>, records whether it added the companion line to <code>blacklist.conf</code>, and tries to unload the module immediately.",
|
||||||
"Writes <code>/etc/modules-load.d/nvidia-vfio.conf</code> with <code>vfio</code>, <code>vfio_pci</code>, <code>nvidia</code>, <code>nvidia_uvm</code> and related modules."
|
"Writes <code>/etc/modules-load.d/nvidia-vfio.conf</code> with <code>nvidia</code> and <code>nvidia_uvm</code> so the modules load early at boot."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
@@ -112,7 +83,7 @@
|
|||||||
"propagate": {
|
"propagate": {
|
||||||
"title": "Optional: propagate the driver to LXC containers",
|
"title": "Optional: propagate the driver to LXC containers",
|
||||||
"body1": "If the overview screen listed containers with NVIDIA passthrough, ProxMenux now offers to update the userspace libraries inside each one to match the host. Host kernel module and container userspace <strong>must be the exact same version</strong> — otherwise <code>nvidia-smi</code> inside the container will fail with a \"version mismatch\" error.",
|
"body1": "If the overview screen listed containers with NVIDIA passthrough, ProxMenux now offers to update the userspace libraries inside each one to match the host. Host kernel module and container userspace <strong>must be the exact same version</strong> — otherwise <code>nvidia-smi</code> inside the container will fail with a \"version mismatch\" error.",
|
||||||
"body2": "The update is distro-aware: <code>apk</code> for Alpine, <code>pacman</code> for Arch, and the same <code>.run</code> installer (with <code>--no-kernel-modules --no-dkms --no-install-compat32-libs</code>) for Debian/Ubuntu and other distros. It temporarily raises container RAM to 2 GB if lower, runs the install, then restores the original RAM setting.",
|
"body2": "The update is distro-aware. For Debian / Ubuntu and other glibc distros, the same <code>.run</code> installer (with <code>--no-kernel-modules --no-dkms --no-install-compat32-libs</code>) is pushed into the container and executed; container RAM is temporarily raised to 2 GB if lower and restored after. For <strong>Arch, Manjaro and EndeavourOS</strong> the update is a <code>pacman -Syu nvidia-utils</code> pinned to the host's driver branch. <strong>Alpine</strong> uses a different path — the <code>.run</code> is extracted on the host, only the userspace libraries are packaged as a tarball and pushed with <code>pct push</code>, then <code>gcompat</code> + <code>binutils</code> shims are installed via <code>apk</code> and SONAME symlinks are recreated with <code>readelf</code> so the glibc-linked libraries load correctly on musl.",
|
||||||
"imageAlt": "Prompt listing LXCs with NVIDIA passthrough and current driver version, with Yes/No to update them all"
|
"imageAlt": "Prompt listing LXCs with NVIDIA passthrough and current driver version, with Yes/No to update them all"
|
||||||
},
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
@@ -122,33 +93,32 @@
|
|||||||
},
|
},
|
||||||
"reinstallUninstall": {
|
"reinstallUninstall": {
|
||||||
"heading": "Reinstall or uninstall",
|
"heading": "Reinstall or uninstall",
|
||||||
"intro": "When the installer detects that a NVIDIA driver is already loaded (<code>nvidia-smi</code> returns a version), it doesn't silently re-install on top. Instead it shows an action menu so you can choose what to do.",
|
"intro": "When the installer detects that the <code>nvidia</code> kernel module is currently loaded and <code>nvidia-smi</code> returns a version, it doesn't silently re-install on top. Instead it shows an action menu so you can choose what to do. (Binaries present on disk but the module not loaded do not count as installed — the module has to be live.)",
|
||||||
"imageAlt": "NVIDIA action menu offered when a driver is already installed — two choices: Reinstall / update driver, or Uninstall the NVIDIA driver completely",
|
"imageAlt": "NVIDIA action menu offered when a driver is already installed — two choices: Reinstall / update driver, or Uninstall the NVIDIA driver completely",
|
||||||
"imageCaption": "The action menu only appears when an NVIDIA driver is currently active on the host.",
|
"imageCaption": "The action menu only appears when an NVIDIA driver is currently active on the host.",
|
||||||
"reinstallHeading": "Reinstall / update",
|
"reinstallHeading": "Reinstall / update",
|
||||||
"reinstallBody": "Continues with the normal install flow but, before downloading anything, runs a clean removal of the current driver (apt purge + DKMS entries dropped + leftover modules unloaded). This is the safe path to apply a newer driver version, switch branches when the kernel demands it, or recover from a half-broken state. The LXC propagation and NVENC patch prompts re-run at the end.",
|
"reinstallBody": "Continues with the normal install flow but, before downloading anything, runs a clean removal of the current driver (apt purge + DKMS entries dropped + leftover modules unloaded). This is the safe path to apply a newer same-branch version, choose another maintained branch when needed, or recover from a half-broken state. The LXC propagation and NVENC patch prompts re-run at the end.",
|
||||||
"uninstallHeading": "Uninstall — what gets removed",
|
"uninstallHeading": "Uninstall — what gets removed",
|
||||||
"uninstallIntro": "Confirms with a yes/no dialog first. Then performs a full, idempotent rollback:",
|
"uninstallIntro": "Confirms with a yes/no dialog first. Then performs a full, idempotent rollback:",
|
||||||
"uninstallItems": [
|
"uninstallItems": [
|
||||||
"Stops and disables <code>nvidia-persistenced</code>, unloads the kernel modules (<code>nvidia_uvm</code>, <code>nvidia_drm</code>, <code>nvidia_modeset</code>, <code>nvidia</code>) — any LXC container with NVIDIA passthrough will be cleanly cut off.",
|
"Runs <code>nvidia-uninstall --silent</code> first (the counterpart to the <code>.run</code> installer), then stops and disables <code>nvidia-persistenced</code> and <code>nvidia-powerd</code>, and unloads the kernel modules (<code>nvidia_uvm</code>, <code>nvidia_drm</code>, <code>nvidia_modeset</code>, <code>nvidia</code>) — any LXC container with NVIDIA passthrough will be cleanly cut off.",
|
||||||
"Runs <code>apt purge</code> on every NVIDIA package, removes the DKMS source tree and the <code>/opt/nvidia</code> .run installer cache.",
|
"Runs <code>apt purge</code> on <code>nvidia-*</code>, <code>libnvidia-*</code>, <code>cuda-*</code> and <code>libcudnn*</code>, removes the DKMS source tree and the <code>/opt/nvidia</code> .run installer cache.",
|
||||||
"Reverts the nouveau blacklist (<code>/etc/modprobe.d/nouveau-blacklist.conf</code>) and the modules-load config (<code>/etc/modules-load.d/nvidia-vfio.conf</code>) so nouveau can come back if you want generic graphics again.",
|
"Removes the modules-load config (<code>/etc/modules-load.d/nvidia-vfio.conf</code>) and the nouveau blacklist entries owned by ProxMenux. Legacy two-line ProxMenux blacklist files are migrated and removed too; modified or unrelated administrator files are preserved.",
|
||||||
"Removes the udev rules (<code>/etc/udev/rules.d/70-nvidia.rules</code>) and the NVENC patch state file (if the keylase patch was applied earlier).",
|
"Removes the udev rules (<code>/etc/udev/rules.d/70-nvidia.rules</code>) and clears the NVENC patch state (a field in the ProxMenux managed-installs registry, set to <em>removed</em> — no separate file to delete).",
|
||||||
"Rebuilds <code>initramfs</code> for all kernels and prompts for a reboot to finalise (the nouveau unblacklist only takes effect after restart)."
|
"Rebuilds <code>initramfs</code> for all kernels, runs <code>proxmox-boot-tool refresh</code> on systemd-boot hosts, and prompts for a reboot to finalise."
|
||||||
],
|
],
|
||||||
"lxcWarnTitle": "LXC containers with NVIDIA passthrough",
|
"lxcWarnTitle": "LXC containers with NVIDIA passthrough",
|
||||||
"lxcWarnBody": "Removing the host driver invalidates the device paths and CUDA libraries mapped into any LXC with NVIDIA passthrough. Plan the operation during a maintenance window if Frigate / Plex / Jellyfin / Ollama (or anything else) depends on it."
|
"lxcWarnBody": "Removing the host driver invalidates the device paths and CUDA libraries mapped into any LXC with NVIDIA passthrough. Plan the operation during a maintenance window if Frigate / Plex / Jellyfin / Ollama (or anything else) depends on it."
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"heading": "Update notifications",
|
"heading": "Update notifications",
|
||||||
"body": "The installed NVIDIA driver is tracked in ProxMenux's managed-installs registry. On startup and every 24h the Monitor checks the upstream listing at <code>download.nvidia.com/XFree86/Linux-x86_64/</code> against the version <code>nvidia-smi</code> reports, and fires a notification when a newer compatible version is available.",
|
"body": "The installed NVIDIA driver is tracked in ProxMenux's managed-installs registry. On startup and every 24h the Monitor checks the upstream listing at <code>download.nvidia.com/XFree86/Linux-x86_64/</code> against the version <code>nvidia-smi</code> reports, and notifies only when a newer maintenance release exists in the installed branch.",
|
||||||
"kindsHeading": "Two kinds of update message",
|
"kindsHeading": "Update message",
|
||||||
"kindsItems": [
|
"kindsItems": [
|
||||||
"<strong>Same-branch patch.</strong> A newer maintenance release in your current driver branch (e.g. installed 580.65.06 → available 580.105.08). Bug fixes and security patches without changing branch.",
|
"<strong>Same-branch maintenance.</strong> A newer release in your current driver branch (e.g. installed 580.65.06 → available 580.105.08). The Monitor does not infer cross-branch kernel compatibility."
|
||||||
"<strong>Branch upgrade required by kernel.</strong> If the host is on a kernel that no longer supports your current branch (e.g. you upgraded the host kernel to 6.17 while still on driver 570.x), the message says so explicitly and recommends the kernel's minimum compatible branch — same matrix the installer uses to filter the version menu."
|
|
||||||
],
|
],
|
||||||
"antiTitle": "Anti-cascade by design",
|
"antiTitle": "Anti-cascade by design",
|
||||||
"antiBody": "One notification per distinct upstream version, never on every 24h scan. The branch-upgrade message in particular only fires once you actually need to switch — until then the same-branch tracker stays muted.",
|
"antiBody": "One notification per distinct upstream version, never on every 24h scan. If no newer release exists in the installed branch, the tracker stays quiet.",
|
||||||
"applyTitle": "Applying the update",
|
"applyTitle": "Applying the update",
|
||||||
"applyBody": "The Monitor doesn't auto-apply driver updates — reinstalling the NVIDIA driver always needs a reboot. Open the same installer entry described above, pick <strong>Reinstall / update</strong>, and the new version is downloaded, the DKMS module rebuilt against the running kernel, and the reboot prompted at the end."
|
"applyBody": "The Monitor doesn't auto-apply driver updates — reinstalling the NVIDIA driver always needs a reboot. Open the same installer entry described above, pick <strong>Reinstall / update</strong>, and the new version is downloaded, the DKMS module rebuilt against the running kernel, and the reboot prompted at the end."
|
||||||
},
|
},
|
||||||
@@ -161,7 +131,7 @@
|
|||||||
"troubleshoot": {
|
"troubleshoot": {
|
||||||
"heading": "Troubleshooting",
|
"heading": "Troubleshooting",
|
||||||
"smiFailTitle": "`nvidia-smi` says 'NVIDIA-SMI has failed'",
|
"smiFailTitle": "`nvidia-smi` says 'NVIDIA-SMI has failed'",
|
||||||
"smiFailBody": "Almost always a <strong>nouveau</strong> still loaded or a <strong>kernel header mismatch</strong>. After reboot, run <code>lsmod | grep nouveau</code> — if it returns anything, the blacklist didn't take effect (check <code>/etc/modprobe.d/nouveau-blacklist.conf</code> exists and rebuild initramfs with <code>update-initramfs -u -k all</code>, then reboot). If nouveau is gone, check <code>dmesg | grep -i nvidia</code> — DKMS build errors usually mean your kernel headers don't match the running kernel; reinstall them with <code>apt install --reinstall pve-headers-$(uname -r)</code>.",
|
"smiFailBody": "Almost always a <strong>nouveau</strong> still loaded or a <strong>kernel header mismatch</strong>. After reboot, run <code>lsmod | grep nouveau</code> — if it returns anything, check <code>/etc/modprobe.d/proxmenux-nouveau-blacklist.conf</code>, rebuild initramfs with <code>update-initramfs -u -k all</code>, and reboot. If nouveau is gone, check <code>dmesg | grep -i nvidia</code> — DKMS build errors usually mean the headers do not match the running kernel.",
|
||||||
"lxcMissTitle": "LXC container can't see the GPU after host update",
|
"lxcMissTitle": "LXC container can't see the GPU after host update",
|
||||||
"lxcMissBody": "The container's userspace libraries are stuck at the previous driver version. Either re-run the NVIDIA installer and accept the LXC propagation prompt, or install the same driver version manually inside the container with <code>--no-kernel-modules</code>.",
|
"lxcMissBody": "The container's userspace libraries are stuck at the previous driver version. Either re-run the NVIDIA installer and accept the LXC propagation prompt, or install the same driver version manually inside the container with <code>--no-kernel-modules</code>.",
|
||||||
"logTitle": "Check the install log",
|
"logTitle": "Check the install log",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"prereqs": {
|
"prereqs": {
|
||||||
"title": "Before you start",
|
"title": "Before you start",
|
||||||
"assigned": "<strong>A GPU already assigned</strong> — either in a VM via VFIO or attached to at least one LXC. If you haven't assigned it yet, start from Add GPU to VM / LXC instead.",
|
"assigned": "<strong>A GPU already assigned</strong> — either in a VM via VFIO or attached to at least one LXC. If you haven't assigned it yet, start from Add GPU to VM / LXC instead.",
|
||||||
"iommu": "<strong>IOMMU enabled on the host</strong> — only strictly required when switching <em>to</em> VM mode, but worth having on either way. The script warns if the kernel param is missing.",
|
"iommu": "<strong>IOMMU enabled on the host</strong> — only strictly required when switching <em>to</em> VM mode, but worth having on either way. If the kernel param is missing the script auto-adds <code>intel_iommu=on iommu=pt</code> or <code>amd_iommu=on</code> to the boot command line (via <code>proxmox-boot-tool refresh</code> on systemd-boot or <code>update-grub</code> on GRUB) and includes it in the reboot prompt at the end.",
|
||||||
"iommuCheck": "dmesg | grep -i 'IOMMU enabled' | head -1",
|
"iommuCheck": "dmesg | grep -i 'IOMMU enabled' | head -1",
|
||||||
"reboot": "<strong>Be OK with a reboot.</strong> Switching GPU bindings at the kernel level means the host regenerates initramfs and you reboot to apply. The script prompts at the end.",
|
"reboot": "<strong>Be OK with a reboot.</strong> Switching GPU bindings at the kernel level means the host regenerates initramfs and you reboot to apply. The script prompts at the end.",
|
||||||
"knowList": "<strong>Know which VMs / LXCs are using the GPU.</strong> The script will find them and ask what to do with each, but it's faster if you already know the list."
|
"knowList": "<strong>Know which VMs / LXCs are using the GPU.</strong> The script will find them and ask what to do with each, but it's faster if you already know the list."
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
},
|
},
|
||||||
"apply": {
|
"apply": {
|
||||||
"title": "Apply host + workload changes",
|
"title": "Apply host + workload changes",
|
||||||
"body": "Once you confirm, the script writes the host-side changes — <code>vfio.conf</code>, blacklist, modules, and (for NVIDIA) the per-BDF udev rule at <code>/etc/udev/rules.d/10-proxmenux-vfio-bind.rules</code> plus the BDF state at <code>/etc/proxmenux/vfio-bind.bdfs</code>. It also applies the chosen conflict policy to each affected VM/LXC. If the host config actually changed, it runs <code>update-initramfs -u -k all</code> — otherwise it skips that step."
|
"body": "Once you confirm, the script writes the host-side changes — <code>vfio.conf</code>, blacklist, modules, and (for NVIDIA) the per-BDF udev rule at <code>/etc/udev/rules.d/10-proxmenux-vfio-bind.rules</code> plus the BDF state at <code>/etc/proxmenux/vfio-bind.bdfs</code>. It also applies the chosen conflict policy to each affected VM/LXC. If the host config actually changed, it runs <code>update-initramfs -u -k all</code> followed by <code>proxmox-boot-tool refresh</code>; otherwise both are skipped."
|
||||||
},
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"title": "Reboot",
|
"title": "Reboot",
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"description": "Telegram, Discord, Email, Gotify and Apprise (multi-channel) — with deduplication, cooldown, burst aggregation, quiet hours and a complete history.",
|
"description": "Telegram, Discord, Email, Gotify, Pushover and Apprise (multi-channel) — with deduplication, cooldown, burst aggregation, quiet hours and a complete history.",
|
||||||
"icon": "Bell",
|
"icon": "Bell",
|
||||||
"href": "/docs/monitor/notifications"
|
"href": "/docs/monitor/notifications"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -254,8 +254,8 @@
|
|||||||
"items": [
|
"items": [
|
||||||
"<strong>Watchers</strong> push events: <code>JournalWatcher</code> tails the system journal, <code>TaskWatcher</code> polls the Proxmox task list, <code>ProxmoxHookWatcher</code> reacts to backup / replication / snapshot hooks, and <code>PollingCollector</code> handles slow data sources.",
|
"<strong>Watchers</strong> push events: <code>JournalWatcher</code> tails the system journal, <code>TaskWatcher</code> polls the Proxmox task list, <code>ProxmoxHookWatcher</code> reacts to backup / replication / snapshot hooks, and <code>PollingCollector</code> handles slow data sources.",
|
||||||
"<strong>Templates</strong> turn an event into a (title, body) pair. The same template can run through the configured AI provider (OpenAI / Anthropic / Gemini / Groq / Ollama / OpenRouter) to produce a plain-language rewrite; both versions are stored in <code>notification_history</code>.",
|
"<strong>Templates</strong> turn an event into a (title, body) pair. The same template can run through the configured AI provider (OpenAI / Anthropic / Gemini / Groq / Ollama / OpenRouter) to produce a plain-language rewrite; both versions are stored in <code>notification_history</code>.",
|
||||||
"<strong>Channels</strong> deliver messages: Telegram, Discord, Email, Gotify and Apprise (multi-channel). Each is implemented in <code>notification_channels.py</code> behind the same <code>create_channel()</code> / <code>send()</code> interface, so adding a new channel is a single class.",
|
"<strong>Channels</strong> deliver messages: Telegram, Discord, Email, Gotify, Pushover and Apprise (multi-channel). Each is implemented in <code>notification_channels.py</code> behind the same <code>create_channel()</code> / <code>send()</code> interface, so adding a new channel is a single class.",
|
||||||
"<strong>Encryption.</strong> Sensitive settings (<code>telegram.token</code>, <code>discord.webhook_url</code>, <code>ai_api_key_*</code>, <code>email.password</code>) are XOR-encrypted with the key in <code>.notification_key</code> before being written to the DB. Plaintext never touches disk."
|
"<strong>Encryption.</strong> Sensitive settings (<code>telegram.bot_token</code>, <code>discord.webhook_url</code>, <code>pushover.user_key</code>, <code>pushover.api_token</code>, <code>ai_api_key_*</code>, <code>email.password</code>) are encrypted with the key in <code>.notification_key</code> before being written to the DB and are masked in the interface."
|
||||||
],
|
],
|
||||||
"linksFooter": "Per-event toggles, channel overrides and AI configuration are surfaced in <notifLink>Settings → Notifications</notifLink> and <aiLink>Settings → AI Assistant</aiLink>."
|
"linksFooter": "Per-event toggles, channel overrides and AI configuration are surfaced in <notifLink>Settings → Notifications</notifLink> and <aiLink>Settings → AI Assistant</aiLink>."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"mechanisms": {
|
"mechanisms": {
|
||||||
"heading": "How an update method is selected",
|
"heading": "How an update method is selected",
|
||||||
"lead": "The integrated Proxmox VE Helper-Scripts path follows the official <helper>update-apps mechanism</helper>. Other install types use the matching package, Docker or custom path.",
|
"lead": "The integrated Proxmox VE Helper-Scripts path follows the official <helper>update-apps mechanism</helper>. Other install types use the matching package, Docker or custom path.",
|
||||||
|
"officialReference": "Official reference: the <helper>Proxmox VE Helper-Scripts update-apps documentation</helper> covers interactive and unattended modes, backups, dry runs, temporary build resources, logs and exit codes.",
|
||||||
"colSource": "Source",
|
"colSource": "Source",
|
||||||
"colAction": "Displayed action",
|
"colAction": "Displayed action",
|
||||||
"colNotes": "What runs",
|
"colNotes": "What runs",
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
"body1": "Inside the dashboard, the <strong>Health Monitor</strong> runs continuously in the background and produces a structured stream of events: high CPU temperature, disk SMART warnings, ZFS pool degradation, OOM kills, VM/CT failures, security incidents, and so on. Each event has a category, a severity (INFO / WARNING / CRITICAL) and a stable <code>error_key</code> so duplicates collapse instead of flooding the screen.",
|
"body1": "Inside the dashboard, the <strong>Health Monitor</strong> runs continuously in the background and produces a structured stream of events: high CPU temperature, disk SMART warnings, ZFS pool degradation, OOM kills, VM/CT failures, security incidents, and so on. Each event has a category, a severity (INFO / WARNING / CRITICAL) and a stable <code>error_key</code> so duplicates collapse instead of flooding the screen.",
|
||||||
"feedsIntro": "Events feed three things at the same time:",
|
"feedsIntro": "Events feed three things at the same time:",
|
||||||
"feedsHealth": "The <strong>Health Monitor view</strong> in the dashboard (active + dismissed lists).",
|
"feedsHealth": "The <strong>Health Monitor view</strong> in the dashboard (active + dismissed lists).",
|
||||||
"feedsChannels": "The <strong>notification engine</strong> — Telegram, Discord, Email, Gotify and Apprise (multi-channel). Each channel is configured independently and per-event categories can be silenced.",
|
"feedsChannels": "The <strong>notification engine</strong> — Telegram, Discord, Email, Gotify, Pushover and Apprise (multi-channel). Each channel is configured independently and per-event categories can be silenced.",
|
||||||
"feedsAI": "The optional <strong>AI assistant</strong> — when enabled, the configured provider (OpenAI, Anthropic, Gemini, Groq, Ollama or OpenRouter) explains incoming events in plain language and, if enabled in the AI settings, proposes next steps.",
|
"feedsAI": "The optional <strong>AI assistant</strong> — when enabled, the configured provider (OpenAI, Anthropic, Gemini, Groq, Ollama or OpenRouter) explains incoming events in plain language and, if enabled in the AI settings, proposes next steps.",
|
||||||
"suppressionTitle": "Suppression instead of mute-all",
|
"suppressionTitle": "Suppression instead of mute-all",
|
||||||
"suppressionBody": "Each category has its own <em>Suppression Duration</em>: once you dismiss an alert, the same alert is silenced for that window (default 24 hours, configurable per category up to permanent). Real escalations — e.g. CPU temperature crossing the critical threshold — always re-trigger regardless of suppression."
|
"suppressionBody": "Each category has its own <em>Suppression Duration</em>: once you dismiss an alert, the same alert is silenced for that window (default 24 hours, configurable per category up to permanent). Real escalations — e.g. CPU temperature crossing the critical threshold — always re-trigger regardless of suppression."
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Proxmox Notifications — Telegram, Discord, Email, Gotify, Apprise | ProxMenux Monitor",
|
"title": "Proxmox Notifications — Telegram, Discord, Email, Gotify, Pushover, Apprise | ProxMenux Monitor",
|
||||||
"description": "Send Proxmox VE notifications to Telegram, Discord, Email, Gotify and ~80 extra services via Apprise. ProxMenux Monitor turns events from the Health Monitor, the journal watcher and the Proxmox VE webhook into rich messages with deduplication, cooldown, burst aggregation, an optional AI rewrite and a complete history.",
|
"description": "Send Proxmox VE notifications to Telegram, Discord, Email, Gotify, Pushover and ~80 extra services via Apprise. ProxMenux Monitor turns events from the Health Monitor, the journal watcher and the Proxmox VE webhook into rich messages with deduplication, cooldown, burst aggregation, an optional AI rewrite and a complete history.",
|
||||||
"ogTitle": "Proxmox Notifications — Telegram, Discord, Email, Gotify, Apprise",
|
"ogTitle": "Proxmox Notifications — Telegram, Discord, Email, Gotify, Pushover, Apprise",
|
||||||
"ogDescription": "Send Proxmox VE alerts to Telegram, Discord, Email, Gotify and ~80 extra services via Apprise — with deduplication, cooldown, burst aggregation and an optional AI rewrite.",
|
"ogDescription": "Send Proxmox VE alerts to Telegram, Discord, Email, Gotify, Pushover and ~80 extra services via Apprise — with deduplication, cooldown, burst aggregation and an optional AI rewrite.",
|
||||||
"twitterTitle": "Proxmox Notifications | ProxMenux Monitor",
|
"twitterTitle": "Proxmox Notifications | ProxMenux Monitor",
|
||||||
"twitterDescription": "Send Proxmox VE alerts to Telegram, Discord, Email, Gotify and ~80 extra services via Apprise."
|
"twitterDescription": "Send Proxmox VE alerts to Telegram, Discord, Email, Gotify, Pushover and ~80 extra services via Apprise."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"description": "The fan-out engine that takes events from every collector inside the Monitor and delivers them to Telegram, Discord, Email, Gotify and ~80 extra services via Apprise — with deduplication, cooldown, burst aggregation, per-event and per-channel toggles, an optional AI rewriter, and a queryable history.",
|
"description": "The fan-out engine that takes events from every collector inside the Monitor and delivers them to Telegram, Discord, Email, Gotify, Pushover and ~80 extra services via Apprise — with deduplication, cooldown, burst aggregation, per-event and per-channel toggles, an optional AI rewriter, and a queryable history.",
|
||||||
"section": "ProxMenux Monitor"
|
"section": "ProxMenux Monitor"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"aiLabel": "AI rewrite (opt.)",
|
"aiLabel": "AI rewrite (opt.)",
|
||||||
"aiDetail": "OpenAI / Anthropic\nGemini / Groq\nOpenRouter / Ollama\n(off by default)",
|
"aiDetail": "OpenAI / Anthropic\nGemini / Groq\nOpenRouter / Ollama\n(off by default)",
|
||||||
"channelsLabel": "Channels",
|
"channelsLabel": "Channels",
|
||||||
"channelsDetail": "Telegram\nDiscord\nEmail (SMTP)\nGotify\nApprise (~80 services)"
|
"channelsDetail": "Telegram\nDiscord\nEmail (SMTP)\nGotify\nPushover\nApprise (~80 services)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabling": {
|
"enabling": {
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
"Registers a Proxmox VE webhook target in <code>/etc/pve/notifications.cfg</code> pointing at <code>POST http://127.0.0.1:8008/api/notifications/webhook</code>. From this moment on, anything Proxmox VE emits on its own (HA, replication, vzdump from the GUI) flows into the same pipeline as the Monitor's own events. See <pvelink>PVE webhook integration</pvelink> below for the full mechanics.",
|
"Registers a Proxmox VE webhook target in <code>/etc/pve/notifications.cfg</code> pointing at <code>POST http://127.0.0.1:8008/api/notifications/webhook</code>. From this moment on, anything Proxmox VE emits on its own (HA, replication, vzdump from the GUI) flows into the same pipeline as the Monitor's own events. See <pvelink>PVE webhook integration</pvelink> below for the full mechanics.",
|
||||||
"Starts the dispatch background thread. The thread polls the event queue and walks every event through the pipeline diagrammed above."
|
"Starts the dispatch background thread. The thread polls the event queue and walks every event through the pipeline diagrammed above."
|
||||||
],
|
],
|
||||||
"activeAlt": "Notifications card after enabling — Active badge, channel tabs (Telegram, Gotify, Discord, Email), Display Name field and Advanced AI Enhancement collapsible section",
|
"activeAlt": "Notifications card after enabling — Active badge, channel tabs, Display Name field and Advanced AI Enhancement collapsible section",
|
||||||
"activeCaption": "Active state — channel tabs at the top (Telegram / Gotify / Discord / Email), the Display Name field, the per-channel category list, and the collapsible <em>Advanced: AI Enhancement</em> section."
|
"activeCaption": "Active state — channel tabs at the top (Telegram / Gotify / Discord / Email / Pushover / Apprise), the Display Name field, the per-channel category list, and the collapsible <em>Advanced: AI Enhancement</em> section."
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": {
|
||||||
"heading": "Event sources",
|
"heading": "Event sources",
|
||||||
@@ -89,9 +89,9 @@
|
|||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"heading": "Channel walkthroughs",
|
"heading": "Channel walkthroughs",
|
||||||
"intro": "Five channels are currently supported: Telegram, Discord, Gotify, Email (SMTP) and Apprise. The first four are native — each one has its own tab inside the Notifications panel with a <em>+ setup guide</em> link opening an in-app modal. Apprise is a generic hub that adds ~80 additional services (ntfy, Matrix, Pushover, Slack, Teams, Pushbullet, AWS SNS, Mattermost…) through a single URL field. They are all documented step by step below.",
|
"intro": "Six channels are currently supported: Telegram, Discord, Gotify, Email (SMTP), Pushover and Apprise. The first five are native integrations with their own configuration fields. Apprise is a generic hub that adds around 80 additional services (ntfy, Matrix, Slack, Teams, Pushbullet, AWS SNS, Mattermost…) through a single URL field. They are all documented step by step below.",
|
||||||
"credsTitle": "Where credentials live",
|
"credsTitle": "Where credentials live",
|
||||||
"credsBody": "Tokens, webhook URLs and SMTP passwords are stored locally in the Monitor's SQLite database under <code>/usr/local/share/proxmenux/</code>. They never leave the host except to reach their respective services. A backup of that directory is enough to recover the configured channels."
|
"credsBody": "Tokens, keys, webhook URLs and SMTP passwords are stored locally in the Monitor's SQLite database under <code>/usr/local/share/proxmenux/</code>. Sensitive values are protected and masked in the interface. They never leave the host except to reach their respective services. A backup of that directory is enough to recover the configured channels."
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"heading": "Telegram",
|
"heading": "Telegram",
|
||||||
@@ -176,12 +176,28 @@
|
|||||||
"relayTitle": "Self-hosted SMTP relay",
|
"relayTitle": "Self-hosted SMTP relay",
|
||||||
"relayBody": "If you run your own SMTP relay (Postfix, msmtp, etc.) on the LAN, point the Monitor at it and skip the app-password dance entirely. The relay handles auth upstream and the Monitor sends in cleartext on a trusted network."
|
"relayBody": "If you run your own SMTP relay (Postfix, msmtp, etc.) on the LAN, point the Monitor at it and skip the app-password dance entirely. The relay handles auth upstream and the Monitor sends in cleartext on a trusted network."
|
||||||
},
|
},
|
||||||
|
"pushover": {
|
||||||
|
"heading": "Pushover",
|
||||||
|
"intro": "Pushover is a mobile push service with official apps for iOS, Android and desktop browsers. The dedicated ProxMenux channel talks directly to the <a>Pushover API</a>, so no Apprise URL is required.",
|
||||||
|
"stepsTitle": "Setup",
|
||||||
|
"steps": [
|
||||||
|
"Create a <a>Pushover account</a>, install the official app on the devices that should receive alerts and sign in.",
|
||||||
|
"Copy the <em>User Key</em> shown on your Pushover dashboard. A group key can be used instead when several people or devices must receive the same alert.",
|
||||||
|
"Open <a>Create an Application/API Token</a>, create an application named <em>ProxMenux</em> and copy its 30-character API token.",
|
||||||
|
"In <em>Settings → Notifications → Pushover</em>, paste the user or group key and the application API token. The device and sound fields are optional.",
|
||||||
|
"Save the settings and press <em>Send test</em>. The Pushover app should receive the message immediately."
|
||||||
|
],
|
||||||
|
"priorityTitle": "Priority mapping",
|
||||||
|
"priorityBody": "Normal ProxMenux messages use Pushover priority 0. When <strong>High priority for critical alerts</strong> is enabled, CRITICAL events use priority 1 so they stand out and bypass the user's Pushover quiet hours. ProxMenux does not use emergency priority 2, which would require repeated notifications and an acknowledgement callback.",
|
||||||
|
"secretTitle": "Protect both values",
|
||||||
|
"secretBody": "The user or group key and the application API token both authorize message delivery. ProxMenux stores them as protected notification secrets and masks them in the interface; do not publish either value in screenshots or support logs."
|
||||||
|
},
|
||||||
"apprise": {
|
"apprise": {
|
||||||
"heading": "Apprise (generic hub for ~80 services)",
|
"heading": "Apprise (generic hub for ~80 services)",
|
||||||
"intro": "Apprise is an open-source notification library that speaks the protocol of around 80 different services through a single URL format. Adding it as one more channel inside the Monitor means you can deliver alerts to services that don't have a dedicated tab — ntfy, Matrix, Pushover, Slack, Microsoft Teams, Mattermost, Pushbullet, AWS SNS, Pushsafer, Rocket.Chat, Signal API and many others — without ProxMenux having to implement each integration separately.",
|
"intro": "Apprise is an open-source notification library that speaks the protocol of around 80 different services through a single URL format. Adding it as one more channel inside the Monitor means you can deliver alerts to services that don't have a dedicated tab — ntfy, Matrix, Slack, Microsoft Teams, Mattermost, Pushbullet, AWS SNS, Pushsafer, Rocket.Chat, Signal API and many others — without ProxMenux having to implement each integration separately. Pushover can also be reached through Apprise, although its dedicated tab is simpler for a single Pushover destination.",
|
||||||
"listIntro": "The full list of supported services and the exact URL format for each one lives in the official Apprise wiki:",
|
"listIntro": "The full list of supported services and the exact URL format for each one is available in the official Apprise documentation:",
|
||||||
"listItems": [
|
"listItems": [
|
||||||
"<a>github.com/caronc/apprise/wiki</a> — full index of supported services.",
|
"<a>Apprise service documentation</a> — full index of supported services.",
|
||||||
"<a>URL basics</a> — how Apprise URLs are structured."
|
"<a>URL basics</a> — how Apprise URLs are structured."
|
||||||
],
|
],
|
||||||
"stepsTitle": "Steps",
|
"stepsTitle": "Steps",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Automated Post-Install Script | ProxMenux Documentation",
|
"title": "Automated Post-Install Script | ProxMenux Documentation",
|
||||||
"description": "The ProxMenux Automated post-install script applies a curated set of 13 safe, hardware-aware optimizations to a fresh Proxmox VE host with zero prompts. Every change is registered for later reversal via Uninstall Optimizations.",
|
"description": "The ProxMenux Automated post-install script applies a curated set of 14 safe, hardware-aware optimizations to a fresh Proxmox VE host with zero prompts. Reversible configuration changes are registered for later restoration.",
|
||||||
"ogTitle": "Automated Post-Install Script | ProxMenux Documentation",
|
"ogTitle": "Automated Post-Install Script | ProxMenux Documentation",
|
||||||
"ogDescription": "13 curated optimizations applied to a fresh Proxmox VE host with zero prompts. Hardware-aware (SSD/NVMe auto-detect) and fully reversible."
|
"ogDescription": "14 curated optimizations applied to a fresh Proxmox VE host with zero prompts. Hardware-aware, with reversible configuration changes tracked."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Automated Post-Install Script",
|
"title": "Automated Post-Install Script",
|
||||||
"description": "One click, zero prompts — ProxMenux applies a curated set of 13 safe optimizations that almost every Proxmox host benefits from. Every change is registered in the tools JSON so you can undo any of them later from Uninstall Optimizations.",
|
"description": "One click, zero prompts — ProxMenux applies a curated set of 14 safe optimizations that almost every Proxmox host benefits from. Reversible configuration changes are registered for Uninstall Optimizations; package upgrades are not described as reversible.",
|
||||||
"section": "Post-Install · Automated"
|
"section": "Post-Install · Automated"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Memory tuning",
|
"tool": "Memory tuning",
|
||||||
"what": "Sets vm.swappiness=10, balanced dirty ratios, vm.overcommit_memory=1, vm.max_map_count=262144 and compaction proactiveness when supported.",
|
"what": "Sets vm.swappiness=10, balanced dirty ratios, vm.max_map_count=262144 and compaction proactiveness when supported. The kernel's memory-overcommit policy is left at the Proxmox default.",
|
||||||
"category": "System",
|
"category": "System",
|
||||||
"categorySlug": "system"
|
"categorySlug": "system"
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Persistent interface names",
|
"tool": "Persistent interface names",
|
||||||
"what": "Writes one /etc/systemd/network/10-proxmenux-<iface>.link per physical NIC (each starting with a 'Managed by ProxMenux' header) that pins the MAC to the current name, so eth0 / enp… names stay stable across reboots and new NIC additions.",
|
"what": "Writes one <code>/etc/systemd/network/10-proxmenux-<iface>.link</code> per physical NIC (each starting with a 'Managed by ProxMenux' header) that pins the MAC to the current name, so <code>eth0</code> / <code>enp…</code> names stay stable across reboots and new NIC additions.",
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"categorySlug": "network"
|
"categorySlug": "network"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "What this category covers",
|
"title": "What this category covers",
|
||||||
"body": "Four foundational options you typically want on any fresh Proxmox host: switch to the free community repositories and run a full system upgrade, auto-configure the timezone and NTP sync, strip APT language downloads to save bandwidth and disk, and pick from a list of 25 common system utilities."
|
"body": "Five foundational options you typically want on any fresh Proxmox host: switch to the free community repositories, run a full system upgrade, auto-configure the timezone and NTP sync, strip APT language downloads to save bandwidth and disk, and pick from a list of 25 common system utilities."
|
||||||
},
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"heading": "Update and upgrade system",
|
"heading": "Update and upgrade system",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"shortTitle": "In short",
|
"shortTitle": "In short",
|
||||||
"shortBody": "The option runs the exact <code>apt update && apt full-upgrade -y</code> Proxmox recommends, wraps it with the repo hygiene and post-upgrade cleanup that the official guide also tells you to do, and prompts for the reboot at the end. See <link>Proxmox System Update</link> — the same updater is also available as a standalone utility in the main menu, with the full process diagram.",
|
"shortBody": "The option runs the exact <code>apt update && apt full-upgrade -y</code> Proxmox recommends, wraps it with the repo hygiene and post-upgrade cleanup that the official guide also tells you to do, and prompts for the reboot at the end. See <link>Proxmox System Update</link> — the same updater is also available as a standalone utility in the main menu, with the full process diagram.",
|
||||||
"subTitle": "Don't apply to a subscribed host",
|
"subTitle": "Don't apply to a subscribed host",
|
||||||
"subBody": "If you actually have a Proxmox subscription and want to keep using the enterprise repositories, skip this option. Re-running it would disable the enterprise repo and route you to the community channel. You can restore enterprise repos from the Uninstall menu if you change your mind later.",
|
"subBody": "If you actually have a Proxmox subscription and want to keep using the enterprise repositories, skip this option. Running it disables the enterprise repository and routes the host to the community channel. The package upgrade and repository rewrite are not presented as reversible in Uninstall Optimizations; restore your repository configuration deliberately if you need to change channels later.",
|
||||||
"safetyTitle": "Post-update safety check",
|
"safetyTitle": "Post-update safety check",
|
||||||
"safetyBody": "After the upgrade, the script checks for disks with stale PV (Physical Volume) metadata — an edge case that can happen when a VM with disk passthrough scribbles LVM headers onto a raw disk. If anything suspicious is found you'll see a warning suggesting <code>pvs</code> to inspect. No action is taken automatically."
|
"safetyBody": "After the upgrade, the script checks for disks with stale PV (Physical Volume) metadata — an edge case that can happen when a VM with disk passthrough scribbles LVM headers onto a raw disk. If anything suspicious is found you'll see a warning suggesting <code>pvs</code> to inspect. No action is taken automatically."
|
||||||
},
|
},
|
||||||
@@ -209,8 +209,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actionTitle": "A few of them in action",
|
"actionTitle": "A few of them in action",
|
||||||
"noBulkTitle": "No bulk uninstall for utilities",
|
"noBulkTitle": "Only ProxMenux-installed utilities are removed",
|
||||||
"noBulkBody": "The Uninstall Optimizations menu does <strong>not</strong> track which utilities you installed — only whether the \"apt languages\", \"time sync\" and \"apt upgrade\" options were applied. To remove a specific utility later, uninstall it by hand:"
|
"noBulkBody": "ProxMenux records only the selected utility packages that were not already installed before this action. Uninstall Optimizations can purge those packages later, while utilities that were already present on the host are left untouched. The general APT system upgrade is intentionally not tracked as reversible because upgraded packages have no safe atomic rollback."
|
||||||
},
|
},
|
||||||
"related": {
|
"related": {
|
||||||
"heading": "Related",
|
"heading": "Related",
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Customizable Post-Install Script | ProxMenux Documentation",
|
"title": "Customizable Post-Install Script | ProxMenux Documentation",
|
||||||
"description": "Cherry-pick exactly which optimizations to apply to a Proxmox VE host with ProxMenux. 10 categories, ~30 individual tools, checklist UI. Includes everything the Automated script does, plus opt-in features (IOMMU, Fastfetch, Figurine, Ceph, HA, AMD fixes…).",
|
"description": "Cherry-pick exactly which optimizations to apply to a Proxmox VE host with ProxMenux. 10 categories, ~35 individual tools, checklist UI. Includes everything the Automated script does, plus opt-in features (IOMMU, Fastfetch, Figurine, Ceph, HA, AMD fixes…).",
|
||||||
"ogTitle": "Customizable Post-Install Script | ProxMenux Documentation",
|
"ogTitle": "Customizable Post-Install Script | ProxMenux Documentation",
|
||||||
"ogDescription": "10 categories, ~30 individual optimizations. Pick exactly what you want on a Proxmox VE host. Fully reversible."
|
"ogDescription": "10 categories, ~35 individual optimizations. Pick exactly what you want on a Proxmox VE host. Reversible changes are tracked."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Customizable Post-Install Script",
|
"title": "Customizable Post-Install Script",
|
||||||
"description": "Cherry-pick exactly which optimizations to apply to a Proxmox VE host. ProxMenux groups ~30 individual tools into 10 categories, each with its own checklist dialog. Same engine as Automated, but with full control over what gets applied.",
|
"description": "Cherry-pick exactly which optimizations to apply to a Proxmox VE host. ProxMenux groups ~35 individual tools into 10 categories, each with its own checklist dialog. Same engine as Automated, but with full control over what gets applied.",
|
||||||
"section": "Post-Install · Customizable"
|
"section": "Post-Install · Customizable"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "When to pick Customizable",
|
"title": "When to pick Customizable",
|
||||||
"body": "Choose this path when you already know which tweaks you want on the host — or which you definitely do not want. The script presents a checklist per category so you can pre-select, deselect or mix-and-match optimizations. Every item can be applied again later (it is idempotent) or reverted from <link>Uninstall Optimizations</link>."
|
"body": "Choose this path when you already know which tweaks you want on the host — or which you definitely do not want. The script presents a checklist per category so you can pre-select, deselect or mix-and-match optimizations. Items can be applied again later, and reversible configuration changes are tracked for <link>Uninstall Optimizations</link>. Package upgrades are not presented as reversible."
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"heading": "How it compares to Automated",
|
"heading": "How it compares to Automated",
|
||||||
"body": "Customizable is a superset of the <link>Automated script</link>. It covers the same 13 baseline optimizations plus a long list of opt-in ones that Automated intentionally skips — things that are useful only on specific hardware (AMD fixes), specific hosting (OVH RTM), or specific workloads (IOMMU/VFIO, Ceph repo, High Availability, Fastfetch, Figurine, ZFS ARC tuning, pigz, ZFS auto-snapshot, vzdump speed limits, Open vSwitch, TCP BBR…)."
|
"body": "Customizable is a superset of the <link>Automated script</link>. It covers the same 14 baseline optimizations plus a long list of opt-in ones that Automated intentionally skips — things that are useful only on specific hardware (AMD fixes), specific hosting (OVH RTM), or specific workloads (IOMMU/VFIO, Ceph repo, High Availability, Fastfetch, Figurine, ZFS ARC tuning, pigz, ZFS auto-snapshot, vzdump speed limits, Open vSwitch, TCP BBR…)."
|
||||||
},
|
},
|
||||||
"categoriesSection": {
|
"categoriesSection": {
|
||||||
"heading": "The 10 categories",
|
"heading": "The 10 categories",
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Network",
|
"name": "Network",
|
||||||
"description": "Harden and tune the host's network stack. Forces APT over IPv4, applies sysctl hardening + TCP buffer tuning, offers Open vSwitch and BBR, and pins persistent interface names by MAC."
|
"description": "Harden and tune the host's network stack. Forces APT over IPv4, applies sysctl hardening + TCP buffer tuning, offers Open vSwitch, TCP BBR + TCP Fast Open, and pins persistent interface names by MAC."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Storage",
|
"name": "Storage",
|
||||||
"description": "Set up Proxmox's common storage subsystems: ZFS ARC sizing, ZFS auto-snapshot, and vzdump speed limits to avoid saturating the disk during backups."
|
"description": "Set up Proxmox's common storage subsystems: ZFS ARC sizing, ZFS auto-snapshot, ZFS autotrim for SSD/NVMe pools, and vzdump speed limits to avoid saturating the disk during backups."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Security",
|
"name": "Security",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Optional",
|
"name": "Optional",
|
||||||
"description": "Niche pieces not every host needs: AMD CPU fixes, Fastfetch banner, Figurine 3D hostname, Ceph repository, High Availability services and Log2RAM to reduce SSD wear."
|
"description": "Niche pieces not every host needs: AMD CPU fixes, Fastfetch banner, Figurine 3D hostname, PVE Appliance Manager index refresh, Ceph repository, High Availability services and Log2RAM to reduce SSD wear."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mixTip": {
|
"mixTip": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Post-Install: Customization",
|
"title": "Post-Install: Customization",
|
||||||
"description": "Cosmetic and quality-of-life tweaks for the Proxmox host. None of them change functional behaviour — they just make the shell nicer to use and hide the subscription nag in the web UI. All three are tracked and reversible from the Uninstall menu.",
|
"description": "Cosmetic and quality-of-life tweaks for the Proxmox host. They make the shell nicer to use and hide the subscription nag in the web UI. Bashrc, MOTD and the subscription banner are tracked and reversible from the Uninstall menu.",
|
||||||
"section": "Settings post-install Proxmox"
|
"section": "Settings post-install Proxmox"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"heading": "Set up custom MOTD banner",
|
"heading": "Set up custom MOTD banner",
|
||||||
"intro": "Prepends <em>\"This system is optimised by: ProxMenux\"</em> to <code>/etc/motd</code>, the message shown after a successful SSH login (above the shell prompt, before any <code>update-motd</code> scripts run). Harmless and purely informational — useful as a quick visual confirmation that ProxMenux has been applied on this host.",
|
"intro": "Prepends <em>\"This system is optimised by: ProxMenux\"</em> to <code>/etc/motd</code>, the message shown after a successful SSH login (above the shell prompt, before any <code>update-motd</code> scripts run). Harmless and purely informational — useful as a quick visual confirmation that ProxMenux has been applied on this host.",
|
||||||
"writesTitle": "What ProxMenux writes",
|
"writesTitle": "What ProxMenux writes",
|
||||||
"writesOutro": "Original <code>/etc/motd</code> is backed up to <code>/etc/motd.bak</code> on first apply. The operation is idempotent: if the marker line is already present, nothing is added."
|
"writesOutro": "On first apply, ProxMenux records whether <code>/etc/motd</code> existed and stores its original contents under <code>/usr/local/share/proxmenux</code>. The operation is idempotent: if the marker line is already present, nothing is added. Older installations with an existing <code>/etc/motd.bak</code> are migrated to the same reversible state."
|
||||||
},
|
},
|
||||||
"banner": {
|
"banner": {
|
||||||
"heading": "Remove subscription banner",
|
"heading": "Remove subscription banner",
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
"verify": {
|
"verify": {
|
||||||
"heading": "Verification",
|
"heading": "Verification",
|
||||||
"intro": "After applying all three:",
|
"intro": "After applying all three:",
|
||||||
"reversibleTitle": "All three are reversible",
|
"reversibleTitle": "All three customization changes are tracked",
|
||||||
"reversibleBody": "<link>Uninstall Optimizations</link> restores <code>/root/.bashrc</code> and <code>/etc/motd</code> from their <code>.bak</code> backups, and either restores the patched UI files from the backup directory or reinstalls <code>pve-manager</code>, <code>proxmox-widget-toolkit</code>, <code>libjs-extjs</code> and <code>libpve-http-server-perl</code> with <code>--force-confnew</code> to bring the web UI back to vanilla."
|
"reversibleBody": "<link>Uninstall Optimizations</link> restores <code>/root/.bashrc</code>, returns MOTD to its exact pre-ProxMenux contents (or removes the file if it did not previously exist), and restores the patched UI files from backup or reinstalls the affected Proxmox packages when necessary."
|
||||||
},
|
},
|
||||||
"related": {
|
"related": {
|
||||||
"heading": "Related",
|
"heading": "Related",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Proxmox VE Post-Install Script — Automated and Customizable | ProxMenux",
|
"title": "Proxmox VE Post-Install Script — Automated and Customizable | ProxMenux",
|
||||||
"description": "Overview of the ProxMenux Post-Install scripts for Proxmox VE. Run the Automated script for sane defaults with zero prompts, the Customizable script to pick exactly what you want across 10 categories (system, virtualization, network, storage, security, performance, optional), or fully reverse any change with the Uninstall Optimizations option.",
|
"description": "Overview of the ProxMenux Post-Install scripts for Proxmox VE. Run the Automated script for sane defaults with zero prompts, the Customizable script to pick exactly what you want across 10 categories, or restore supported reversible changes with Uninstall Optimizations.",
|
||||||
"ogTitle": "Proxmox VE Post-Install Script — Automated and Customizable",
|
"ogTitle": "Proxmox VE Post-Install Script — Automated and Customizable",
|
||||||
"ogDescription": "Apply common Proxmox VE post-install optimizations across 10 categories — automated or à la carte, with reversible options.",
|
"ogDescription": "Apply common Proxmox VE post-install optimizations across 10 categories — automated or à la carte, with reversible options.",
|
||||||
"twitterTitle": "Proxmox VE Post-Install Script | ProxMenux",
|
"twitterTitle": "Proxmox VE Post-Install Script | ProxMenux",
|
||||||
@@ -9,26 +9,26 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Post-Install Scripts",
|
"title": "Post-Install Scripts",
|
||||||
"description": "Configure a fresh Proxmox VE host with ProxMenux's post-install optimizations. Three paths: run everything automatically, cherry-pick what you want, or reverse any change. All changes are tracked.",
|
"description": "Configure a fresh Proxmox VE host with ProxMenux's post-install optimizations. Apply the baseline automatically, choose individual options, update installed functions or restore supported reversible changes. Package upgrades are not presented as reversible.",
|
||||||
"section": "Settings post-install Proxmox"
|
"section": "Settings post-install Proxmox"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "What this menu is for",
|
"title": "What this menu is for",
|
||||||
"body": "Right after installing Proxmox VE, there are dozens of small changes that make the host faster and easier to maintain — free repositories, sane journald limits, sensible TCP buffers, SSD-friendly log storage, bashrc niceties, and more. ProxMenux automates all of them, tracks what it changed, and lets you revert."
|
"body": "Right after installing Proxmox VE, there are dozens of small changes that make the host faster and easier to maintain — free repositories, sane journald limits, sensible TCP buffers, SSD-friendly log storage, bashrc niceties, and more. ProxMenux automates them and tracks the supported reversible configuration changes."
|
||||||
},
|
},
|
||||||
"openingMenu": {
|
"openingMenu": {
|
||||||
"heading": "Opening the menu",
|
"heading": "Opening the menu",
|
||||||
"body": "From ProxMenux's main menu, select <strong>Settings post-install Proxmox</strong>. You will see this:",
|
"body": "From ProxMenux's main menu, select <strong>Settings post-install Proxmox</strong>. You will see this:",
|
||||||
"imageAlt": "Post-Installation Scripts menu with 3 ProxMenux options (Automated / Customizable / Uninstall) followed by the Community Scripts section"
|
"imageAlt": "Post-Installation Scripts menu — Automated, Customizable, the conditional Apply Available Updates (only when updates are pending), and Uninstall, followed by the Community Scripts section"
|
||||||
},
|
},
|
||||||
"threeWays": {
|
"threeWays": {
|
||||||
"heading": "Three ways to apply optimizations",
|
"heading": "Four ways to apply optimizations",
|
||||||
"body": "The three ProxMenux entries share the same underlying code and the same registry of installed tools — they just give you different levels of control. Pick the one that matches how much you want to decide."
|
"body": "The four ProxMenux entries share the same underlying code and the same registry of installed tools — they just give you different levels of control. The <em>Apply Available Updates</em> entry only shows when at least one installed optimization has a newer version on disk than what is registered; on a freshly applied host it stays hidden."
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"title": "Automated",
|
"title": "Automated",
|
||||||
"description": "A curated set of 13 safe, always-useful optimizations applied in sequence with zero prompts. Good default for most users.",
|
"description": "A curated set of 14 safe, always-useful optimizations applied in sequence with zero prompts. Good default for most users.",
|
||||||
"bullets": [
|
"bullets": [
|
||||||
"Free repos + system upgrade",
|
"Free repos + system upgrade",
|
||||||
"Memory, kernel, network tuning",
|
"Memory, kernel, network tuning",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Customizable",
|
"title": "Customizable",
|
||||||
"description": "~30 individual optimizations across 10 categories. You pick exactly which ones to apply. Same engine as Automated, but with full control.",
|
"description": "~35 individual optimizations across 10 categories. You pick exactly which ones to apply. Same engine as Automated, but with full control.",
|
||||||
"bullets": [
|
"bullets": [
|
||||||
"Checklist UI per category",
|
"Checklist UI per category",
|
||||||
"Includes everything Automated does, plus opt-in items (IOMMU, Fastfetch, Figurine, Ceph, HA, AMD fixes…)",
|
"Includes everything Automated does, plus opt-in items (IOMMU, Fastfetch, Figurine, Ceph, HA, AMD fixes…)",
|
||||||
@@ -57,10 +57,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Uninstall Optimizations",
|
"title": "Uninstall Optimizations",
|
||||||
"description": "Every change made by either path is tracked in a JSON registry, and every optimization has a reverse function. Pick what to revert, and the host goes back.",
|
"description": "Supported reversible changes are tracked in a JSON registry and paired with a restoration function. Actions without a safe rollback, such as a full package upgrade, are intentionally excluded.",
|
||||||
"bullets": [
|
"bullets": [
|
||||||
"Detects previously applied optimizations automatically",
|
"Detects previously applied optimizations automatically",
|
||||||
"Reversal restores original configs from backup files",
|
"Reversal picks the right path for each item — restores from a .bak backup where one was made, deletes the sysctl.d snippet where nothing needed backing up, or reinstalls the vanilla package with --force-confnew (e.g. subscription banner)",
|
||||||
"Reboot prompt if needed (VFIO, persistent names, etc.)"
|
"Reboot prompt if needed (VFIO, persistent names, etc.)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,11 @@
|
|||||||
"remoteTitle": "Remote script piped to bash",
|
"remoteTitle": "Remote script piped to bash",
|
||||||
"remoteBody": "The installation runs <code>wget -qO - https://…apply.sh | bash</code>. If the OVH mirror is ever compromised, the script executes as root on your host. Before enabling this option, decide whether you trust OVH's mirror chain more than the monitoring you gain. For most home-lab or non-OVH users this option should simply stay off.",
|
"remoteBody": "The installation runs <code>wget -qO - https://…apply.sh | bash</code>. If the OVH mirror is ever compromised, the script executes as root on your host. Before enabling this option, decide whether you trust OVH's mirror chain more than the monitoring you gain. For most home-lab or non-OVH users this option should simply stay off.",
|
||||||
"noOpTitle": "Only enable if the host is actually at OVH",
|
"noOpTitle": "Only enable if the host is actually at OVH",
|
||||||
"noOpBody": "The option is a no-op on non-OVH servers, so ticking it on a home-lab Proxmox doesn't break anything. But there is a cosmetic bug today: even on non-OVH servers the script prints <em>\"Server belongs to OVH\"</em> at the end, which can be misleading. See the troubleshooting note below.",
|
"noOpBody": "The option is a no-op on non-OVH servers, so ticking it on a home-lab Proxmox doesn't break anything. On a non-OVH host the script prints <em>\"Not an OVH server, skipping RTM installation\"</em> and exits cleanly; no packages are installed.",
|
||||||
"runsTitle": "What ProxMenux runs",
|
"runsTitle": "What ProxMenux runs",
|
||||||
"verifyTitle": "Verification",
|
"verifyTitle": "Verification",
|
||||||
"verifyBody": "On a real OVH host, after a reboot you should see the <a>RTM dashboard</a> in your OVH Manager populated with live data for the host. On the Proxmox side, the RTM collector is a systemd service — check it directly:",
|
"verifyBody": "On a real OVH host, after a reboot you should see the <a>RTM dashboard</a> in your OVH Manager populated with live data for the host. On the Proxmox side, the RTM collector is a systemd service — check it directly:",
|
||||||
"troubleTitle": "Troubleshooting",
|
"troubleTitle": "Troubleshooting",
|
||||||
"spuriousTitle": "\"Server belongs to OVH\" but I'm not on OVH",
|
|
||||||
"spuriousBody": "This is a known cosmetic quirk in the current script: the success message fires outside the OVH-detected conditional, so it prints on every run. If the RTM install did <em>not</em> actually happen (check <code>systemctl status ovh-rtm</code> — it will not exist), the message is spurious and can be ignored. Nothing was installed on your host.",
|
|
||||||
"revertTitle": "Not reversible from the Uninstall menu",
|
"revertTitle": "Not reversible from the Uninstall menu",
|
||||||
"revertBody": "There is no dedicated uninstall entry for RTM. On a real OVH host, remove the packages manually with <code>apt purge ovh-*</code> and delete any puppet manifests under <code>/etc/puppet/</code> that RTM installed. On a non-OVH host, nothing was ever installed, so there's nothing to revert."
|
"revertBody": "There is no dedicated uninstall entry for RTM. On a real OVH host, remove the packages manually with <code>apt purge ovh-*</code> and delete any puppet manifests under <code>/etc/puppet/</code> that RTM installed. On a non-OVH host, nothing was ever installed, so there's nothing to revert."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"area": "Routing safety",
|
"area": "Routing safety",
|
||||||
"settings": "<code>accept_redirects=0</code>, <code>accept_source_route=0</code>, <code>secure_redirects=0</code>, <code>send_redirects=0</code>"
|
"settings": "<code>accept_redirects=0</code>, <code>accept_source_route=0</code>, <code>secure_redirects=0</code>, <code>send_redirects=0</code>, <code>log_martians=0</code>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"area": "Reverse path filter",
|
"area": "Reverse path filter",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
],
|
],
|
||||||
"sourceOutro": "It also adds <code>source /etc/network/interfaces.d/*</code> to <code>/etc/network/interfaces</code> if not already present — standard practice so you can drop modular interface snippets without editing the main file.",
|
"sourceOutro": "It also adds <code>source /etc/network/interfaces.d/*</code> to <code>/etc/network/interfaces</code> if not already present — standard practice so you can drop modular interface snippets without editing the main file.",
|
||||||
"fwbrTitle": "Automatic tuning of virtual firewall bridges",
|
"fwbrTitle": "Automatic tuning of virtual firewall bridges",
|
||||||
"fwbrBody": "Alongside the sysctl profile, ProxMenux installs a helper at <code>/usr/local/sbin/proxmenux-fwbr-tune</code> that applies <code>rp_filter=0</code> and <code>log_martians=0</code> to the <code>fwbr*</code> / <code>fwln*</code> / <code>fwpr*</code> / <code>tap*</code> interfaces Proxmox creates around VMs and containers. The helper is invoked by the <code>proxmenux-fwbr-tune.service</code> one-shot unit at boot, and by the <code>/etc/udev/rules.d/99-proxmenux-fwbr-tune.rules</code> rule on every <code>net add</code> event matching those prefixes — covering interfaces that Proxmox recreates on VM start/stop, reboot and live migration.",
|
"fwbrBody": "Alongside the sysctl profile, ProxMenux installs a helper at <code>/usr/local/sbin/proxmenux-fwbr-tune</code> that applies <code>rp_filter=0</code> and <code>log_martians=0</code> to the <code>fwbr*</code> / <code>fwln*</code> / <code>fwpr*</code> / <code>tap*</code> interfaces Proxmox creates around VMs and containers. The helper is invoked by the <code>proxmenux-fwbr-tune.service</code> one-shot unit at boot, and by the <code>/etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules</code> rule on every <code>net add</code> event matching those prefixes — covering interfaces that Proxmox recreates on VM start/stop, reboot and live migration. The helper also runs immediately after install to sweep interfaces already present.",
|
||||||
"rpFilterTitle": "Why rp_filter=2 (loose) instead of 1 (strict)",
|
"rpFilterTitle": "Why rp_filter=2 (loose) instead of 1 (strict)",
|
||||||
"rpFilterBody": "Strict reverse-path filtering drops packets whose source would be routed out a <em>different</em> interface. That's the right default on a client machine, but breaks badly on a Proxmox host where VM traffic often arrives on a bridge and leaves on an uplink with asymmetric routes. <code>rp_filter=2</code> (loose) only drops packets with truly unroutable sources. It's a pragmatic trade-off — slight reduction in local-IP-spoof detection in exchange for not breaking your VM network."
|
"rpFilterBody": "Strict reverse-path filtering drops packets whose source would be routed out a <em>different</em> interface. That's the right default on a client machine, but breaks badly on a Proxmox host where VM traffic often arrives on a bridge and leaves on an uplink with asymmetric routes. <code>rp_filter=2</code> (loose) only drops packets with truly unroutable sources. It's a pragmatic trade-off — slight reduction in local-IP-spoof detection in exchange for not breaking your VM network."
|
||||||
},
|
},
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
"intro": "Installs <code>openvswitch-switch</code> + <code>openvswitch-common</code>. These packages add OVS as a bridge implementation alternative to the standard Linux bridges that Proxmox uses by default. The install alone doesn't change any networking — existing <code>vmbrX</code> bridges keep working. OVS becomes available in the Proxmox UI when you <em>create</em> a new bridge and pick it from the type dropdown.",
|
"intro": "Installs <code>openvswitch-switch</code> + <code>openvswitch-common</code>. These packages add OVS as a bridge implementation alternative to the standard Linux bridges that Proxmox uses by default. The install alone doesn't change any networking — existing <code>vmbrX</code> bridges keep working. OVS becomes available in the Proxmox UI when you <em>create</em> a new bridge and pick it from the type dropdown.",
|
||||||
"tipTitle": "When OVS makes sense",
|
"tipTitle": "When OVS makes sense",
|
||||||
"tipBody": "Consider OVS if you need <strong>VLAN trunking with non-contiguous VLAN IDs</strong>, <strong>LACP with LLDP on specific modes</strong>, <strong>fine-grained flow programming</strong> (OpenFlow), or interoperation with SDN controllers. For a home lab with a couple of VLANs and a single LACP uplink, standard Linux bridges + <code>vmbrX.VID</code> are simpler and perfectly fine.",
|
"tipBody": "Consider OVS if you need <strong>VLAN trunking with non-contiguous VLAN IDs</strong>, <strong>LACP with LLDP on specific modes</strong>, <strong>fine-grained flow programming</strong> (OpenFlow), or interoperation with SDN controllers. For a home lab with a couple of VLANs and a single LACP uplink, standard Linux bridges + <code>vmbrX.VID</code> are simpler and perfectly fine.",
|
||||||
"revertTitle": "Not reversible from the Uninstall menu",
|
"revertTitle": "Reversible from the Uninstall menu",
|
||||||
"revertBody": "Installing OVS is not tracked in Uninstall Optimizations. If you decide you don't want it, remove it manually — but only after migrating any bridges back to Linux bridges first:"
|
"revertBody": "OVS is tracked. <link>Uninstall Optimizations</link> runs <code>apt purge</code> on <code>openvswitch-switch</code> and <code>openvswitch-common</code>. Migrate any OVS bridges back to Linux bridges <em>before</em> uninstalling, otherwise the VMs on those bridges lose networking on next boot. Manual equivalent:"
|
||||||
},
|
},
|
||||||
"bbr": {
|
"bbr": {
|
||||||
"heading": "Enable TCP BBR + TCP Fast Open",
|
"heading": "Enable TCP BBR + TCP Fast Open",
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
"verifyTitle": "Verification",
|
"verifyTitle": "Verification",
|
||||||
"impactTitle": "Impact is workload-dependent",
|
"impactTitle": "Impact is workload-dependent",
|
||||||
"impactBody": "BBR shines on high-latency or lossy links (cross-continent replication, VPN tunnels, mobile clients). On a LAN between two machines on the same switch, the difference is often within noise. TFO helps short, repeated HTTP connections the most.",
|
"impactBody": "BBR shines on high-latency or lossy links (cross-continent replication, VPN tunnels, mobile clients). On a LAN between two machines on the same switch, the difference is often within noise. TFO helps short, repeated HTTP connections the most.",
|
||||||
"revertTitle": "Not reversible from the Uninstall menu",
|
"revertTitle": "Reversible from the Uninstall menu",
|
||||||
"revertBody": "BBR/TFO aren't tracked. To revert, remove the two sysctl files and reload:"
|
"revertBody": "BBR/TFO are tracked. <link>Uninstall Optimizations</link> removes the two sysctl files (<code>/etc/sysctl.d/99-tcp-bbr.conf</code> and <code>99-tcp-fastopen.conf</code>) and reloads sysctl so the kernel returns to <code>cubic</code> and <code>tcp_fastopen=1</code>. Manual equivalent:"
|
||||||
},
|
},
|
||||||
"names": {
|
"names": {
|
||||||
"heading": "Interface Names (persistent)",
|
"heading": "Interface Names (persistent)",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"title": "Optional Settings",
|
"title": "Optional Settings",
|
||||||
"intro": "The <strong>Optional Settings</strong> category provides additional features and optimizations that you can choose to apply to your Proxmox VE installation. These settings are not essential but can enhance your system's capabilities in specific scenarios.",
|
"intro": "The <strong>Optional Settings</strong> category provides additional features and optimizations that you can choose to apply to your Proxmox VE installation. These settings are not essential but can enhance your system's capabilities in specific scenarios.",
|
||||||
"available": "Available Optional Features",
|
"available": "Available Optional Features",
|
||||||
|
"stepLabel": "Step",
|
||||||
"ceph": {
|
"ceph": {
|
||||||
"title": "Add Latest Ceph Support",
|
"title": "Add Latest Ceph Support",
|
||||||
"intro": "This option installs the latest Ceph storage system support for Proxmox VE. Ceph is a distributed storage system that provides high performance, reliability, and scalability.",
|
"intro": "This option installs the latest Ceph storage system support for Proxmox VE. Ceph is a distributed storage system that provides high performance, reliability, and scalability.",
|
||||||
@@ -28,9 +29,8 @@
|
|||||||
"doesIntro": "What it does:",
|
"doesIntro": "What it does:",
|
||||||
"doesItems": [
|
"doesItems": [
|
||||||
"Detects if an AMD EPYC or Ryzen CPU is present",
|
"Detects if an AMD EPYC or Ryzen CPU is present",
|
||||||
"Applies kernel parameter 'idle=nomwait' to prevent random crashes",
|
"Applies kernel parameter 'idle=nomwait' to prevent random crashes (via /etc/kernel/cmdline on systemd-boot hosts, or /etc/default/grub on GRUB hosts — with a .bak of the original)",
|
||||||
"Configures KVM to ignore certain MSRs (Model Specific Registers) for better Windows guest compatibility",
|
"Configures KVM to ignore certain MSRs (Model Specific Registers) for better Windows guest compatibility"
|
||||||
"Installs the latest Proxmox VE kernel"
|
|
||||||
],
|
],
|
||||||
"howUse": "How to use: These fixes are applied automatically and require a system reboot to take effect.",
|
"howUse": "How to use: These fixes are applied automatically and require a system reboot to take effect.",
|
||||||
"automates": "This adjustment automates the following commands:"
|
"automates": "This adjustment automates the following commands:"
|
||||||
@@ -47,21 +47,16 @@
|
|||||||
"howUse": "How to use: After enabling these services, you can configure HA groups and resources in the Proxmox VE web interface.",
|
"howUse": "How to use: After enabling these services, you can configure HA groups and resources in the Proxmox VE web interface.",
|
||||||
"automates": "This adjustment automates the following commands:"
|
"automates": "This adjustment automates the following commands:"
|
||||||
},
|
},
|
||||||
"testing": {
|
"pveam": {
|
||||||
"title": "Enable Proxmox Testing Repository",
|
"title": "Update Proxmox VE Appliance Manager",
|
||||||
"intro": "This option enables the Proxmox testing repository, allowing access to the latest, potentially unstable versions of Proxmox VE packages.",
|
"intro": "Refreshes the local index of container templates that <code>pveam</code> exposes in the Proxmox UI, so the list of available appliances is up to date the next time you create an LXC.",
|
||||||
"doesIntro": "What it does:",
|
"doesIntro": "What it does:",
|
||||||
"doesItems": [
|
"doesItems": [
|
||||||
"Adds the Proxmox testing repository to the system's package sources",
|
"Runs <code>pveam update</code> against the Proxmox mirrors to fetch the current appliance catalogue",
|
||||||
"Creates a new file in /etc/apt/sources.list.d/ for the testing repository",
|
"Populates the appliance list shown by the web UI when creating a container"
|
||||||
"Updates the package lists to include packages from the new repository"
|
|
||||||
],
|
],
|
||||||
"howUse": "How to use: After enabling this repository, you can update and upgrade your system to get the latest testing versions of Proxmox VE packages. Use with caution as these versions may be unstable.",
|
"howUse": "How to use: run it once when the appliance list in the UI feels stale, or after switching mirrors. It does not download the templates themselves — only the catalogue index.",
|
||||||
"manualIntro": "To manually add the Proxmox testing repository, you can use these commands:",
|
"automates": "This adjustment automates the following command:"
|
||||||
"noteLabel": "Note:",
|
|
||||||
"noteBody": "$(lsb_release -cs) automatically detects your Proxmox VE version codename (e.g., bullseye).",
|
|
||||||
"warnLabel": "Warning:",
|
|
||||||
"warnBody": "Enabling the testing repository may lead to system instability. It's recommended for testing environments only."
|
|
||||||
},
|
},
|
||||||
"fastfetch": {
|
"fastfetch": {
|
||||||
"title": "Install and Configure Fastfetch",
|
"title": "Install and Configure Fastfetch",
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
],
|
],
|
||||||
"replacesTitle": "This replaces a system binary",
|
"replacesTitle": "This replaces a system binary",
|
||||||
"replacesBody": "Replacing <code>/bin/gzip</code> with a wrapper is unusual. It is safe (the wrapper produces gzip-compatible output), but worth knowing: scripts that hardcode paths, run inside restrictive chroots, or verify binary hashes may behave differently. The original binary is preserved as <code>/bin/gzip.original</code> so you can always swap it back.",
|
"replacesBody": "Replacing <code>/bin/gzip</code> with a wrapper is unusual. It is safe (the wrapper produces gzip-compatible output), but worth knowing: scripts that hardcode paths, run inside restrictive chroots, or verify binary hashes may behave differently. The original binary is preserved as <code>/bin/gzip.original</code> so you can always swap it back.",
|
||||||
"revertTitle": "Not reversible from the Uninstall menu",
|
"revertTitle": "Reversible from the Uninstall menu",
|
||||||
"revertBody": "This optimization is applied by Customizable, but <strong>does not currently have a matching entry in the Uninstall Optimizations menu</strong>. To revert it by hand, restore the original gzip and clear the wrapper:",
|
"revertBody": "This optimization is tracked. <link>Uninstall Optimizations</link> restores <code>/bin/gzip.original</code> back into place, removes the <code>pigzwrapper</code>, reverts the two lines added to <code>/etc/vzdump.conf</code>, and runs <code>apt purge pigz</code>. Manual equivalent:",
|
||||||
"verifyTitle": "Verification",
|
"verifyTitle": "Verification",
|
||||||
"verifyBody": "After applying, <code>gzip --version</code> should mention pigz. A quick benchmark also shows the speed difference on a multi-core host:",
|
"verifyBody": "After applying, <code>gzip --version</code> should mention pigz. A quick benchmark also shows the speed difference on a multi-core host:",
|
||||||
"whenTitle": "When this matters most",
|
"whenTitle": "When this matters most",
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
"nfsTitle": "Don't disable this if you use NFS",
|
"nfsTitle": "Don't disable this if you use NFS",
|
||||||
"nfsBody": "NFS server <strong>and</strong> NFS client rely on <code>rpcbind</code> to negotiate the ports used by <code>mountd</code>, <code>statd</code>, <code>lockd</code>, etc. If your Proxmox host either <em>exports</em> NFS shares to other machines or <em>mounts</em> NFS shares from a NAS, do not apply this option. Mounts will fail with <code>mount.nfs: rpc.statd is not running</code> or similar.",
|
"nfsBody": "NFS server <strong>and</strong> NFS client rely on <code>rpcbind</code> to negotiate the ports used by <code>mountd</code>, <code>statd</code>, <code>lockd</code>, etc. If your Proxmox host either <em>exports</em> NFS shares to other machines or <em>mounts</em> NFS shares from a NAS, do not apply this option. Mounts will fail with <code>mount.nfs: rpc.statd is not running</code> or similar.",
|
||||||
"runsTitle": "What ProxMenux runs",
|
"runsTitle": "What ProxMenux runs",
|
||||||
"runsOutro": "The package stays installed (so you or another tool can re-enable it later). The service unit is disabled so the service does not come back on reboot.",
|
"runsOutro": "The package stays installed. ProxMenux records the original enabled/active state of both rpcbind.service and rpcbind.socket, then disables and stops both units so socket activation cannot bring the service back.",
|
||||||
"verifyTitle": "Verification",
|
"verifyTitle": "Verification",
|
||||||
"verifyBody": "After applying, confirm <code>rpcbind</code> is off and nothing is listening on port 111:",
|
"verifyBody": "After applying, confirm <code>rpcbind</code> is off and nothing is listening on port 111:",
|
||||||
"reversibleTitle": "Reversible from the Uninstall menu",
|
"reversibleTitle": "Restores the original service state",
|
||||||
"reversibleBody": "This change is tracked. Open <link>Uninstall Optimizations</link> and pick <em>RPC Disable</em> to restore it. Nothing is purged from the system — just re-enable the service and it starts again."
|
"reversibleBody": "This change is registered in <code>installed_tools.json</code>. <link>Uninstall Optimizations</link> restores each rpcbind unit to the enabled/disabled and active/inactive state captured before ProxMenux changed it; it does not assume that rpcbind was enabled on every host."
|
||||||
},
|
},
|
||||||
"related": {
|
"related": {
|
||||||
"heading": "Related",
|
"heading": "Related",
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "What this category covers",
|
"title": "What this category covers",
|
||||||
"body": "Three storage-related optimizations: tune the <strong>ZFS ARC</strong> cache size to a sensible fraction of host RAM, install and schedule <strong>ZFS auto-snapshots</strong>, and remove throttles from <strong>vzdump</strong> so backups run at full speed. All three are independent — pick the ones that match your setup. A fourth storage-adjacent optimization, <link>Log2RAM</link>, reduces SSD/NVMe wear by moving <code>/var/log</code> to a ramdisk — it lives on the Optional page because the ProxMenux Customizable menu groups it there."
|
"body": "Four storage-related optimizations: tune the <strong>ZFS ARC</strong> cache size to a sensible fraction of host RAM, install and schedule <strong>ZFS auto-snapshots</strong>, enable <strong>ZFS autotrim</strong> on SSD/NVMe pools, and remove throttles from <strong>vzdump</strong> so backups run at full speed. All four are independent — pick the ones that match your setup. A fifth storage-adjacent optimization, <link>Log2RAM</link>, reduces SSD/NVMe wear by moving <code>/var/log</code> to a ramdisk — it lives on the Optional page because the ProxMenux Customizable menu groups it there."
|
||||||
},
|
},
|
||||||
"notTrackedTitle": "None of these are in the Uninstall menu",
|
"trackedTitle": "All four are tracked in the Uninstall menu",
|
||||||
"notTrackedBody": "Unlike most post-install optimizations, the three Storage options are <strong>not currently tracked</strong> in the Uninstall Optimizations flow. If you apply them and later want to revert, you'll have to do it by hand. The manual rollback commands are shown below each section.",
|
"trackedBody": "Each of these options registers a tool in <code>installed_tools.json</code>, so they appear in <link>Uninstall Optimizations</link>. Reverting <code>zfs_arc</code> removes <code>/etc/modprobe.d/99-zfsarc.conf</code> and rebuilds initramfs; <code>zfs_auto_snapshot</code> reverses the cron schedule and offers to purge the package; <code>zfs_autotrim</code> sets <code>autotrim=off</code> on the pools it enabled; <code>vzdump_speed</code> restores <code>/etc/vzdump.conf</code> from the <code>.bak</code> the install created.",
|
||||||
"arc": {
|
"arc": {
|
||||||
"heading": "Optimize ZFS ARC size",
|
"heading": "Optimize ZFS ARC size",
|
||||||
"intro": "The <strong>Adaptive Replacement Cache (ARC)</strong> is ZFS's in-memory read cache. Without explicit tuning, ZFS happily grabs up to half the host RAM for itself, which is excessive on a Proxmox host that also needs memory for VMs and LXCs. This option caps ARC to a sane fraction of total RAM based on the size of the machine.",
|
"intro": "The <strong>Adaptive Replacement Cache (ARC)</strong> is ZFS's in-memory read cache. Without explicit tuning, ZFS happily grabs up to half the host RAM for itself, which is excessive on a Proxmox host that also needs memory for VMs and LXCs. This option caps ARC to a sane fraction of total RAM based on the size of the machine.",
|
||||||
@@ -21,19 +21,27 @@
|
|||||||
"headerMax": "ARC cap",
|
"headerMax": "ARC cap",
|
||||||
"rows": [
|
"rows": [
|
||||||
{
|
{
|
||||||
"ram": "≤ 16 GB",
|
"ram": "Formula",
|
||||||
"max": "512 MiB"
|
"max": "RAM / 10, capped at 16 GiB, with a 64 MiB floor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ram": "17 – 32 GB",
|
"ram": "8 GB host",
|
||||||
"max": "1 GiB"
|
"max": "≈ 819 MiB (RAM/10)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ram": "> 32 GB",
|
"ram": "16 GB host",
|
||||||
"max": "RAM / 8 (floor 512 MiB)"
|
"max": "≈ 1.6 GiB (RAM/10)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ram": "64 GB host",
|
||||||
|
"max": "≈ 6.4 GiB (RAM/10)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ram": "≥ 160 GB host",
|
||||||
|
"max": "16 GiB (cap)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"after": "On a 64 GB host, that means an 8 GB cap for ARC. The file <code>/etc/modprobe.d/99-zfsarc.conf</code> contains a single directive — <code>options zfs zfs_arc_max=…</code>. Every other ZFS module parameter (<code>zfs_arc_min</code>, L2ARC prefetch/write throttle, TXG timeout) is left at its OpenZFS default. After writing the file, ProxMenux runs <code>update-initramfs -u -k all</code> and, when applicable, <code>proxmox-boot-tool refresh</code>, so the cap also lands in the initramfs used by ZFS-on-root setups.",
|
"after": "The file <code>/etc/modprobe.d/99-zfsarc.conf</code> contains a single directive — <code>options zfs zfs_arc_max=…</code>. Every other ZFS module parameter (<code>zfs_arc_min</code>, L2ARC prefetch/write throttle, TXG timeout) is left at its OpenZFS default. Before writing, a reconcile step scans any other <code>*.conf</code> in <code>/etc/modprobe.d/</code> that sets <code>zfs_arc_min</code> / <code>zfs_arc_max</code>, backs them up to <code>/usr/local/share/proxmenux/backups/zfs_arc/</code> with a manifest, and strips the conflicting lines so only ProxMenux's file is active. After writing the file, ProxMenux runs <code>update-initramfs -u -k all</code> and, when applicable, <code>proxmox-boot-tool refresh</code>, so the cap also lands in the initramfs used by ZFS-on-root setups.",
|
||||||
"rebootTitle": "Requires a reboot to take effect",
|
"rebootTitle": "Requires a reboot to take effect",
|
||||||
"rebootBody": "ARC settings are read when the <code>zfs</code> kernel module loads. To make the cap take effect on ZFS-on-root hosts, ProxMenux regenerates the initramfs with <code>update-initramfs -u -k all</code> and, when applicable, refreshes the boot loader with <code>proxmox-boot-tool refresh</code>. A reboot is still required to pick up the new module parameter; the \"reboot required\" flag is set automatically.",
|
"rebootBody": "ARC settings are read when the <code>zfs</code> kernel module loads. To make the cap take effect on ZFS-on-root hosts, ProxMenux regenerates the initramfs with <code>update-initramfs -u -k all</code> and, when applicable, refreshes the boot loader with <code>proxmox-boot-tool refresh</code>. A reboot is still required to pick up the new module parameter; the \"reboot required\" flag is set automatically.",
|
||||||
"safeTitle": "Safe on non-ZFS hosts",
|
"safeTitle": "Safe on non-ZFS hosts",
|
||||||
@@ -115,8 +123,8 @@
|
|||||||
"heading": "Increase vzdump backup speed",
|
"heading": "Increase vzdump backup speed",
|
||||||
"intro": "By default, Proxmox vzdump throttles backups to protect running VMs/CTs from IO starvation. On many setups that throttle is more conservative than needed. This option removes the bandwidth cap and lowers the I/O priority so vzdump can saturate the storage path during backup windows.",
|
"intro": "By default, Proxmox vzdump throttles backups to protect running VMs/CTs from IO starvation. On many setups that throttle is more conservative than needed. This option removes the bandwidth cap and lowers the I/O priority so vzdump can saturate the storage path during backup windows.",
|
||||||
"changedTitle": "What gets changed in /etc/vzdump.conf",
|
"changedTitle": "What gets changed in /etc/vzdump.conf",
|
||||||
"noBackupTitle": "No backup of vzdump.conf",
|
"backupTitle": "First run creates a .bak of vzdump.conf",
|
||||||
"noBackupBody": "The script <strong>edits <code>/etc/vzdump.conf</code> in place</strong> without creating a <code>.bak</code> first. If you had custom values there (bwlimit, ionice, compress, pigz, tmpdir, exclude-path, etc.), the changes to <em>those two lines</em> are made with <code>sed</code> — surrounding config is preserved — but there's no \"undo\" snapshot. Make a manual backup if your config is non-trivial: <code>cp /etc/vzdump.conf /etc/vzdump.conf.pre-proxmenux</code>.",
|
"backupBody": "The first time this option runs, ProxMenux copies <code>/etc/vzdump.conf</code> to <code>/etc/vzdump.conf.bak</code> before touching it. Subsequent runs re-use that backup and won't overwrite it, so a hand-edited config from before the first apply stays recoverable. The changes to <code>bwlimit</code> and <code>ionice</code> are then made with <code>sed</code>, and any other options in the file (compress, pigz, tmpdir, exclude-path, etc.) are preserved.",
|
||||||
"skipTitle": "When to skip this",
|
"skipTitle": "When to skip this",
|
||||||
"skipBody": "On a host with slow local storage and VMs that are latency-sensitive, removing the bandwidth cap can cause noticeable slowdowns during backups. If you've previously set a specific <code>bwlimit</code> for that reason, keep it — skip this option.",
|
"skipBody": "On a host with slow local storage and VMs that are latency-sensitive, removing the bandwidth cap can cause noticeable slowdowns during backups. If you've previously set a specific <code>bwlimit</code> for that reason, keep it — skip this option.",
|
||||||
"verifyTitle": "Verification and manual rollback"
|
"verifyTitle": "Verification and manual rollback"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
"intro": "Installs <code>kexec-tools</code> and wires it up so you can reboot the host straight into a new kernel <em>without going through BIOS/UEFI firmware</em>. On big servers where POST takes 45 – 90 seconds, this turns a reboot from a coffee break into a few seconds of downtime.",
|
"intro": "Installs <code>kexec-tools</code> and wires it up so you can reboot the host straight into a new kernel <em>without going through BIOS/UEFI firmware</em>. On big servers where POST takes 45 – 90 seconds, this turns a reboot from a coffee break into a few seconds of downtime.",
|
||||||
"installsTitle": "What ProxMenux installs",
|
"installsTitle": "What ProxMenux installs",
|
||||||
"installsItems": [
|
"installsItems": [
|
||||||
"Package <code>kexec-tools</code> (with debconf pre-answered so apt doesn't prompt during install).",
|
"Package <code>kexec-tools</code> (debconf pre-answered with <code>kexec-tools/load_kexec boolean false</code> so apt doesn't prompt and the auto-load on shutdown stays off).",
|
||||||
"Systemd unit <code>/etc/systemd/system/kexec-pve.service</code> — loads the Proxmox kernel and initrd into memory at boot, reusing the current cmdline.",
|
"Systemd unit <code>/etc/systemd/system/kexec-pve.service</code> — loads the Proxmox kernel and initrd into memory at boot, reusing the current cmdline.",
|
||||||
"An alias in <code>/root/.bash_profile</code>: <code>reboot-quick</code> → <code>systemctl kexec</code>."
|
"An alias in <code>/root/.bash_profile</code>: <code>reboot-quick</code> → <code>systemctl kexec</code>."
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Uninstall Optimizations | ProxMenux Documentation",
|
"title": "Uninstall Optimizations | ProxMenux Documentation",
|
||||||
"description": "Reverse any post-install optimization applied by ProxMenux. Every change is tracked in a JSON registry, and every tool has a dedicated uninstaller that restores the original configuration."
|
"description": "Restore reversible post-install configuration changes applied by ProxMenux. Registered tools use dedicated uninstallers that preserve the pre-existing host state where possible."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Uninstall Optimizations",
|
"title": "Uninstall Optimizations",
|
||||||
"description": "Reverse any change made by the Automated or Customizable post-install scripts. ProxMenux keeps a registry of every optimization it applied and has a dedicated reversal function for each one — pick which to revert, and the host goes back.",
|
"description": "Restore reversible changes made by the Automated or Customizable post-install scripts. ProxMenux registers each supported optimization with its dedicated restoration function; package upgrades are intentionally excluded.",
|
||||||
"section": "Settings post-install Proxmox"
|
"section": "Settings post-install Proxmox"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Why this exists",
|
"title": "Why this exists",
|
||||||
"body": "Every tweak the post-install scripts apply is <strong>tracked</strong> in a JSON registry at <code>/usr/local/share/proxmenux/installed_tools.json</code>. That registry is what powers the uninstall flow — it shows you the list of optimizations currently applied, and a reversal function that restores the original state for each one (from backup files where possible, or by reinstalling the affected packages)."
|
"body": "Every supported reversible tweak is <strong>tracked</strong> in <code>/usr/local/share/proxmenux/installed_tools.json</code>. That registry powers the uninstall flow: it lists the active optimizations and dispatches the matching restoration function. Actions without a safe rollback, such as a full package upgrade, are not added."
|
||||||
},
|
},
|
||||||
"openMenu": {
|
"openMenu": {
|
||||||
"heading": "How to open it",
|
"heading": "How to open it",
|
||||||
@@ -40,21 +40,21 @@
|
|||||||
"body2": "Each reversal logs its progress. Items that require a reboot (VFIO, persistent interface names) set a flag that triggers the reboot prompt at the end."
|
"body2": "Each reversal logs its progress. Items that require a reboot (VFIO, persistent interface names) set a flag that triggers the reboot prompt at the end."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Reboot if needed",
|
"title": "Reboot prompt at the end",
|
||||||
"body1": "If any reversed item modified kernel parameters, kernel modules, or network naming, you'll be offered a reboot. Otherwise the changes are live immediately."
|
"body1": "After the reversal finishes the menu shows a reboot prompt. Items that changed kernel parameters, kernel modules or network naming (VFIO, persistent interface names) do need the reboot to take effect; other items do not, and the prompt is a safety default rather than a per-item check."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"reversible": {
|
"reversible": {
|
||||||
"heading": "What is reversible",
|
"heading": "What is reversible",
|
||||||
"intro": "Every optimization the post-install scripts apply has a matching uninstaller. Grouped here by area:",
|
"intro": "Registered reversible optimizations and their matching uninstallers are grouped here by area:",
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"title": "Repositories & APT",
|
"title": "Repositories & APT",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"tool": "Subscription Banner Removal",
|
"tool": "Subscription Banner Removal",
|
||||||
"restores": "Reinstalls pve-manager, proxmox-widget-toolkit, libjs-extjs and libpve-http-server-perl with force-confnew to restore the original UI files. Also clears cached .js / .gz copies."
|
"restores": "First tries to restore the UI files from ProxMenux's own backups (/usr/local/share/proxmenux/backups/proxmoxlib.js.backup.* and, when the mobile UI is patched, index.html.tpl.backup.*). Only if a backup is missing or corrupt, falls back to reinstalling pve-manager, proxmox-widget-toolkit, libjs-extjs and libpve-http-server-perl with force-confnew. Also clears cached .js / .gz copies."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "APT Language Skip",
|
"tool": "APT Language Skip",
|
||||||
@@ -63,6 +63,10 @@
|
|||||||
{
|
{
|
||||||
"tool": "APT IPv4 Force",
|
"tool": "APT IPv4 Force",
|
||||||
"restores": "Removes /etc/apt/apt.conf.d/99-force-ipv4."
|
"restores": "Removes /etc/apt/apt.conf.d/99-force-ipv4."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "System Utilities",
|
||||||
|
"restores": "Purges only the selected utility packages that ProxMenux recorded as newly installed. Packages already present before the action are never added to this list and are left untouched."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "System Limits Increase",
|
"tool": "System Limits Increase",
|
||||||
"restores": "Removes /etc/sysctl.d/99-maxwatches.conf, 99-maxkeys.conf, 99-swap.conf, 99-fs.conf and /etc/security/limits.d/99-limits.conf. Reverts PAM limits and systemd DefaultLimitNOFILE."
|
"restores": "Removes /etc/sysctl.d/99-maxwatches.conf, 99-maxkeys.conf, 99-swap.conf, 99-fs.conf and /etc/security/limits.d/99-limits.conf. Reverts PAM limits and systemd DefaultLimitNOFILE, and strips the ulimit -n 256000 line from /root/.profile."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -88,7 +92,15 @@
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"tool": "Network Optimizations",
|
"tool": "Network Optimizations",
|
||||||
"restores": "Removes /etc/sysctl.d/99-network.conf together with the proxmenux-fwbr-tune.service unit, the /usr/local/sbin/proxmenux-fwbr-tune helper and the /etc/udev/rules.d/99-proxmenux-fwbr-tune.rules udev rule. Reloads sysctl, systemd and the udev ruleset."
|
"restores": "Removes /etc/sysctl.d/99-network.conf, 97-proxmenux-fwbr.conf and 98-proxmenux-rpf.conf together with the proxmenux-fwbr-tune.service unit, the /usr/local/sbin/proxmenux-fwbr-tune helper and the /etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules udev rule. Also strips the source /etc/network/interfaces.d/* line from /etc/network/interfaces. Reloads sysctl, systemd and the udev ruleset."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "Open vSwitch",
|
||||||
|
"restores": "Runs apt purge on openvswitch-switch and openvswitch-common. Migrate any OVS bridges back to Linux bridges before running this uninstall — otherwise the VMs on those bridges lose networking on next boot."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "TCP BBR + TCP Fast Open",
|
||||||
|
"restores": "Removes /etc/sysctl.d/99-tcp-bbr.conf and 99-tcp-fastopen.conf and reloads sysctl so the kernel returns to the cubic congestion control and to tcp_fastopen=1."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Persistent Interface Names",
|
"tool": "Persistent Interface Names",
|
||||||
@@ -124,9 +136,13 @@
|
|||||||
"tool": "Bashrc Customization",
|
"tool": "Bashrc Customization",
|
||||||
"restores": "Restores /root/.bashrc from the .bak backup. If no backup exists, removes the PMX_CORE_BASHRC block by markers."
|
"restores": "Restores /root/.bashrc from the .bak backup. If no backup exists, removes the PMX_CORE_BASHRC block by markers."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tool": "Custom MOTD Banner",
|
||||||
|
"restores": "Restores the exact original /etc/motd content kept under /usr/local/share/proxmenux, removes the file when it did not exist before, or safely removes the legacy marker when migrating an older installation."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tool": "Fastfetch",
|
"tool": "Fastfetch",
|
||||||
"restores": "Removes the binary, config directory, update-motd hook and the bashrc block. Purges the apt package if installed."
|
"restores": "Removes the binary, config directory, update-motd hook and the fenced BEGIN FASTFETCH / END FASTFETCH block from /root/.bashrc, ~/.profile, /etc/profile and /etc/profile.d/fastfetch.sh. Purges the apt package if installed."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Figurine",
|
"tool": "Figurine",
|
||||||
@@ -144,6 +160,27 @@
|
|||||||
{
|
{
|
||||||
"tool": "AMD CPU fixes (Ryzen/EPYC)",
|
"tool": "AMD CPU fixes (Ryzen/EPYC)",
|
||||||
"restores": "Removes idle=nomwait from kernel cmdline (ZFS) or GRUB, and the ignore_msrs / report_ignored_msrs options from /etc/modprobe.d/kvm.conf."
|
"restores": "Removes idle=nomwait from kernel cmdline (ZFS) or GRUB, and the ignore_msrs / report_ignored_msrs options from /etc/modprobe.d/kvm.conf."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "QEMU Guest Agent (templates)",
|
||||||
|
"restores": "Reads /usr/local/share/proxmenux/guest_agent.pkg (recorded at install time) and apt purges whichever package was installed (qemu-guest-agent for standard hosts, spice-vdagent when Spice mode is used)."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Storage",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"tool": "ZFS ARC sizing",
|
||||||
|
"restores": "Removes /etc/modprobe.d/99-zfsarc.conf, restores any conflicting external *.conf that was staged aside in /usr/local/share/proxmenux/backups/zfs_arc/, rebuilds initramfs and runs proxmox-boot-tool refresh on systemd-boot hosts."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "ZFS auto-snapshot",
|
||||||
|
"restores": "Removes the cron entries the script wrote and offers to apt purge zfs-auto-snapshot. Existing snapshot datasets on the pools are left intact — remove them separately if you want them gone."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "vzdump speed limits",
|
||||||
|
"restores": "Restores /etc/vzdump.conf from the .bak the install created, bringing back bwlimit and ionice to their pre-ProxMenux values."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -160,7 +197,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "kexec (fast reboots)",
|
"tool": "kexec (fast reboots)",
|
||||||
"restores": "Disables kexec-pve.service, removes the unit file and the reboot-quick alias, purges kexec-tools."
|
"restores": "Disables kexec-pve.service, removes the unit file and the reboot-quick alias from /root/.bash_profile, purges kexec-tools."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "RPC / rpcbind Disable",
|
||||||
|
"restores": "Restores rpcbind.service and rpcbind.socket independently to the enabled/disabled and active/inactive states recorded before ProxMenux changed them."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "pigz (parallel gzip)",
|
||||||
|
"restores": "Puts /bin/gzip.original back in place, removes the /bin/pigzwrapper, reverts the pigz and bwlimit lines in /etc/vzdump.conf and apt purges pigz."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "High Availability services",
|
||||||
|
"restores": "Stops and disables pve-ha-lrm, pve-ha-crm and corosync. Existing HA groups and resource definitions are not deleted — remove them from the web UI if you no longer need them."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "Ceph repository",
|
||||||
|
"restores": "Purges the Ceph packages installed by this option, removes the deb822 /etc/apt/sources.list.d/ceph.sources on PVE 9 (or the legacy Ceph list on PVE 8), and refreshes the APT cache."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": "OVH RTM (monitoring)",
|
||||||
|
"restores": "apt purges any ovh-* packages the RTM installer added and deletes the puppet manifests it dropped under /etc/puppet/. On non-OVH hosts nothing was ever installed, so nothing is removed."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,14 @@
|
|||||||
{
|
{
|
||||||
"title": "No reboot unless the function says so",
|
"title": "No reboot unless the function says so",
|
||||||
"body": "Most updates take effect immediately. Updates that touch kernel modules, persistent interface names, or VFIO show the same reboot prompt as a fresh install would."
|
"body": "Most updates take effect immediately. Updates that touch kernel modules, persistent interface names, or VFIO show the same reboot prompt as a fresh install would."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Registry refresh sent to the Monitor",
|
||||||
|
"body": "Once the batch finishes, the menu POSTs to <code>http://127.0.0.1:8008/api/updates/post-install/scan</code> to rebuild <code>/usr/local/share/proxmenux/updates_available.json</code>. That is what makes the Monitor's Optimizations card and the shell menu entry disappear immediately after the update, without waiting for the next scheduled scan."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"jqTitle": "jq is required",
|
||||||
|
"jqBody": "The Path A checklist relies on <code>jq</code> to parse the pending-updates JSON. If <code>jq</code> is missing, the flow exits silently — you would see the menu entry with a count but nothing would happen after picking rows. On any modern Proxmox install <code>jq</code> is present; if in doubt run <code>apt install -y jq</code>."
|
||||||
},
|
},
|
||||||
"differs": {
|
"differs": {
|
||||||
"heading": "How it differs from the other paths",
|
"heading": "How it differs from the other paths",
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"switchToHttps": {
|
"switchToHttps": {
|
||||||
"heading": "Switch the Monitor to HTTPS",
|
"heading": "Switch the Monitor to HTTPS",
|
||||||
"bodyRich": "Once <code>/etc/pve/local/pveproxy-ssl.pem</code> is signed by Let's Encrypt, the Monitor side is one click: open <strong>Settings → Security → HTTPS / SSL</strong>, confirm the issuer shown in the detected-certificate panel reads <em>Let's Encrypt</em> (and not the local Proxmox CA), and click <strong>Use Proxmox Certificate</strong>. The Monitor service restarts and the next browser load is HTTPS on port 8008 — no certificate warning, since the chain is publicly trusted."
|
"bodyRich": "Once <code>/etc/pve/local/pveproxy-ssl.pem</code> is signed by Let's Encrypt, the Monitor side is one click: open <strong>Settings → Security → HTTPS / SSL</strong>, confirm the issuer shown in the detected-certificate panel reads <em>Let's Encrypt</em> (and not the local Proxmox CA), and click <strong>Use Proxmox Certificate</strong>. The Monitor service restarts and the next browser load is HTTPS on port 8008 — no certificate warning, since the chain is publicly trusted. Later Proxmox ACME renewals are validated and selected during the next new TLS connection; there is no recurring certificate poller or renewal-time service restart. <strong>Update certificate</strong> remains available in Security as an explicit diagnostic and recovery action."
|
||||||
},
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
"heading": "Custom certificate — when to use it",
|
"heading": "Custom certificate — when to use it",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Script de creación de VM Synology",
|
"title": "Script de creación de VM Synology",
|
||||||
|
"stepLabel": "Paso",
|
||||||
"intro": {
|
"intro": {
|
||||||
"heading": "Introducción",
|
"heading": "Introducción",
|
||||||
"intro": "ProxMenux ofrece un script automatizado que crea y configura una máquina virtual (VM) para instalar Synology DSM (DiskStation Manager) en Proxmox VE. Este script simplifica el proceso descargando y añadiendo uno de los loaders disponibles al arranque de la VM, dándote la opción de elegir entre cuatro alternativas distintas:",
|
"intro": "ProxMenux ofrece un script automatizado que crea y configura una máquina virtual (VM) para instalar Synology DSM (DiskStation Manager) en Proxmox VE. Este script simplifica el proceso descargando y añadiendo uno de los loaders disponibles al arranque de la VM, dándote la opción de elegir entre cuatro alternativas distintas:",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"heading": "Recorriendo el flujo",
|
"heading": "Recorriendo el flujo",
|
||||||
"detect": {
|
"detect": {
|
||||||
"title": "Detectar GPUs y comprobar IOMMU",
|
"title": "Detectar GPUs y comprobar IOMMU",
|
||||||
"body": "El script lista cada GPU que encuentra. Si IOMMU no está ya habilitado en la cmdline del kernel en ejecución, recibirás un prompt sí/no para añadir <code>intel_iommu=on</code> (o <code>amd_iommu=on</code>) + <code>iommu=pt</code> al archivo de arranque correcto — <code>/etc/kernel/cmdline</code> en ZFS (systemd-boot) o <code>/etc/default/grub</code> en LVM/ext4. Si aceptas y la cmdline del kernel cambia, el script marca que el prompt de reinicio al final será obligatorio.",
|
"body": "El script lista cada GPU que encuentra. Si IOMMU no está ya habilitado en la cmdline del kernel en ejecución, recibirás un prompt sí/no para añadir <code>intel_iommu=on</code> (o <code>amd_iommu=on</code>) + <code>iommu=pt</code> al archivo de arranque correcto. La selección depende del bootloader: <code>/etc/kernel/cmdline</code> cuando el host arranca con systemd-boot (se detecta por la presencia de <code>root=ZFS=</code> en ese fichero, lo habitual en instalaciones de Proxmox VE sobre ZFS-on-root), o <code>/etc/default/grub</code> en el resto. Si aceptas y la cmdline cambia, el script marca que hará falta un reinicio al final.",
|
||||||
"tipTitle": "¿Ya ejecutaste post-instalación?",
|
"tipTitle": "¿Ya ejecutaste post-instalación?",
|
||||||
"tipBody": "Si habilitaste anteriormente <postLink>soporte VFIO IOMMU</postLink> desde los scripts post-instalación, IOMMU ya está activo y este paso pasa silenciosamente. Bien.",
|
"tipBody": "Si habilitaste anteriormente <postLink>soporte VFIO IOMMU</postLink> desde los scripts post-instalación, IOMMU ya está activo y este paso pasa silenciosamente. Bien.",
|
||||||
"imageAlt": "Lista de GPUs detectadas con fabricante y dirección PCI"
|
"imageAlt": "Lista de GPUs detectadas con fabricante y dirección PCI"
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
"intro": "El script escanea cada config de VM y cada config de LXC en el host buscando la GPU que elegiste. Tres resultados posibles:",
|
"intro": "El script escanea cada config de VM y cada config de LXC en el host buscando la GPU que elegiste. Tres resultados posibles:",
|
||||||
"items": [
|
"items": [
|
||||||
"<strong>La GPU está libre.</strong> Nada que hacer, continúa.",
|
"<strong>La GPU está libre.</strong> Nada que hacer, continúa.",
|
||||||
"<strong>La GPU está en otra VM.</strong> Se te ofrece quitarla de esa otra VM antes de asignarla aquí. Si rechazas, el script aborta — dos VMs no pueden compartir una asignación VFIO exclusiva.",
|
"<strong>La GPU está en otra VM.</strong> Si la VM origen está en ejecución, el script aborta — dos VMs no pueden compartir una asignación VFIO exclusiva y la VM origen ha de pararse antes. Si la VM origen está parada, aparece un menú con dos opciones: <em>Mantener la GPU en la config de la VM origen pero desactivar Arranque al inicio</em>, o <em>Eliminar las líneas de la GPU de la config de la VM origen y mantener Arranque al inicio</em>. También existe un camino rápido: si la GPU ya está vinculada a <code>vfio-pci</code> y es un simple traspaso VM→VM, no se reconfigura el host y no hace falta reinicio.",
|
||||||
"<strong>La GPU está en un LXC (modo compartido).</strong> Se te ofrece quitar la configuración de passthrough del LXC (líneas <code>lxc.cgroup2.devices.allow</code> + <code>lxc.mount.entry</code>). El LXC dejará de ver la GPU, pero la VM la verá — esta es la mecánica de \"switch mode\" que le da a esta entrada de menú su etiqueta secundaria."
|
"<strong>La GPU está en un LXC (modo compartido).</strong> Aparece un menú con dos opciones: <em>Mantener la GPU en la config del LXC pero desactivar Arranque al inicio</em>, o <em>Eliminar las líneas de la GPU de la config del LXC (<code>lxc.cgroup2.devices.allow</code> / <code>lxc.mount.entry</code>) y mantener Arranque al inicio</em>. En ambos casos, el LXC dejará de ver la GPU tras el switch, y la VM la verá — esta es la mecánica de \"switch mode\" que le da a esta entrada de menú su etiqueta secundaria."
|
||||||
],
|
],
|
||||||
"imageAlt": "Diálogo que ofrece quitar la GPU de un LXC antes de asignarla a la VM",
|
"imageAlt": "Diálogo que ofrece quitar la GPU de un LXC antes de asignarla a la VM",
|
||||||
"smartTitle": "Los hermanos de audio también se limpian con inteligencia",
|
"smartTitle": "Los hermanos de audio también se limpian con inteligencia",
|
||||||
@@ -100,10 +100,10 @@
|
|||||||
"<code>/etc/modules</code> — añade <code>vfio</code>, <code>vfio_iommu_type1</code>, <code>vfio_pci</code> (más <code>vfio_virqfd</code> en kernels < 6.2).",
|
"<code>/etc/modules</code> — añade <code>vfio</code>, <code>vfio_iommu_type1</code>, <code>vfio_pci</code> (más <code>vfio_virqfd</code> en kernels < 6.2).",
|
||||||
"<code>/etc/modprobe.d/vfio.conf</code> — para AMD / Intel, define <code>options vfio-pci ids=<vendor:device,...> disable_vga=1</code> para que VFIO reclame la GPU pronto en el arranque. Para NVIDIA el archivo solo añade <code>softdep nvidia pre: vfio-pci</code> (más <code>_drm</code>/<code>_modeset</code>/<code>_uvm</code>) — el binding real es por BDF vía la regla udev de abajo. En AMD, también añade líneas <code>softdep</code> forzando que <code>vfio-pci</code> cargue antes de <code>radeon</code> / <code>amdgpu</code>.",
|
"<code>/etc/modprobe.d/vfio.conf</code> — para AMD / Intel, define <code>options vfio-pci ids=<vendor:device,...> disable_vga=1</code> para que VFIO reclame la GPU pronto en el arranque. Para NVIDIA el archivo solo añade <code>softdep nvidia pre: vfio-pci</code> (más <code>_drm</code>/<code>_modeset</code>/<code>_uvm</code>) — el binding real es por BDF vía la regla udev de abajo. En AMD, también añade líneas <code>softdep</code> forzando que <code>vfio-pci</code> cargue antes de <code>radeon</code> / <code>amdgpu</code>.",
|
||||||
"<code>/etc/modprobe.d/iommu_unsafe_interrupts.conf</code> y <code>kvm.conf</code> — workarounds sensatos que la mayoría de VMs Windows / macOS necesitan (<code>allow_unsafe_interrupts=1</code>, <code>ignore_msrs=1</code>).",
|
"<code>/etc/modprobe.d/iommu_unsafe_interrupts.conf</code> y <code>kvm.conf</code> — workarounds sensatos que la mayoría de VMs Windows / macOS necesitan (<code>allow_unsafe_interrupts=1</code>, <code>ignore_msrs=1</code>).",
|
||||||
"<code>/etc/modprobe.d/blacklist.conf</code> — pone en blacklist los drivers open-source compañeros (<code>nouveau</code>, <code>amdgpu</code>, <code>radeon</code>, <code>i915</code>) que si no agarrarían la GPU antes que VFIO. El módulo propietario <code>nvidia</code> <strong>nunca se pone en blacklist</strong> — sigue disponible para cualquier OTRA GPU NVIDIA que mantengas en el host.",
|
"<code>/etc/modprobe.d/blacklist.conf</code> — solo pone en blacklist los drivers open-source del vendor seleccionado (<code>nouveau</code>/<code>lbm-nouveau</code> para NVIDIA; <code>radeon</code>+<code>amdgpu</code> para AMD; <code>i915</code> para Intel), así los drivers de otros vendors en el mismo host siguen cargados. El módulo propietario <code>nvidia</code> <strong>solo</strong> se pone en blacklist (mediante un fichero aparte, <code>/etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf</code>) una vez que <em>todas</em> las GPUs NVIDIA del host han pasado a VFIO — hasta entonces sigue cargado para que cualquier NVIDIA que mantengas en el host siga funcionando.",
|
||||||
"<code>/etc/udev/rules.d/10-proxmenux-vfio-bind.rules</code> + <code>/etc/proxmenux/vfio-bind.bdfs</code> — <strong>solo NVIDIA</strong>. Estado de binding por BDF. La regla udev aplica <code>ATTR'{'driver_override'}'=\"vfio-pci\"</code> en el evento PCI ADD para cada Bus:Device.Function rastreado, así que solo las GPUs que has pasado explícitamente van a VFIO. Esto es lo que hace que NVIDIA multi-GPU funcione — tus otras tarjetas NVIDIA mantienen su driver <code>nvidia</code> y siguen siendo usables en el host.",
|
"<code>/etc/udev/rules.d/10-proxmenux-vfio-bind.rules</code> + <code>/etc/proxmenux/vfio-bind.bdfs</code> — <strong>solo NVIDIA</strong>. Estado de binding por BDF. La regla udev aplica <code>ATTR'{'driver_override'}'=\"vfio-pci\"</code> en el evento PCI ADD para cada Bus:Device.Function rastreado, así que solo las GPUs que has pasado explícitamente van a VFIO. Esto es lo que hace que NVIDIA multi-GPU funcione — tus otras tarjetas NVIDIA mantienen su driver <code>nvidia</code> y siguen siendo usables en el host.",
|
||||||
"<strong>Solo AMD.</strong> Vuelca la ROM de la GPU desde sysfs (<code>/sys/bus/pci/.../rom</code>) o la tabla ACPI VFCT a <code>/usr/share/kvm/vbios_<card>.bin</code>. La VM la referencia vía <code>romfile=</code> para que las tarjetas que mal-reportan su propia VBIOS aún inicialicen correctamente.",
|
"<strong>Solo AMD.</strong> Vuelca la ROM de la GPU desde sysfs (<code>/sys/bus/pci/.../rom</code>) o la tabla ACPI VFCT a <code>/usr/share/kvm/vbios_<card>.bin</code>. La VM la referencia vía <code>romfile=</code> para que las tarjetas que mal-reportan su propia VBIOS aún inicialicen correctamente.",
|
||||||
"<strong>Solo NVIDIA.</strong> Para y deshabilita los servicios NVIDIA del host que podrían sondear / bloquear la GPU en el arranque (<code>nvidia-persistenced</code>, <code>nvidia-powerd</code>, <code>nvidia-fabricmanager</code>). El propio módulo <code>nvidia</code> se deja cargado para que otras GPUs NVIDIA del host sigan funcionando con <code>nvidia-smi</code>.",
|
"<strong>Solo NVIDIA.</strong> Los servicios NVIDIA del host (<code>nvidia-persistenced</code>, <code>nvidia-powerd</code>, <code>nvidia-fabricmanager</code>) solo se detienen y deshabilitan cuando todas las GPU NVIDIA están asignadas a VFIO. En un host mixto permanecen activos, junto con el módulo <code>nvidia</code>, para las GPU que continúan en modo nativo.",
|
||||||
"<code>update-initramfs -u -k all</code> — solo se ejecuta si algo de lo de arriba ha cambiado realmente."
|
"<code>update-initramfs -u -k all</code> — solo se ejecuta si algo de lo de arriba ha cambiado realmente."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Instalar drivers NVIDIA en el host | ProxMenux Documentation",
|
"title": "Instalar drivers NVIDIA en el host | ProxMenux Documentation",
|
||||||
"description": "Instala y configura los drivers propietarios NVIDIA en un host Proxmox VE usando ProxMenux. Cubre compatibilidad de kernel, setup VFIO, servicio de persistencia, parche NVENC opcional y propagación automática a LXCs."
|
"description": "Instala y configura los drivers propietarios NVIDIA en un host Proxmox VE usando ProxMenux. Cubre filtrado por GPU, validación DKMS, servicio de persistencia, parche NVENC opcional y propagación automática a LXC."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Instalar drivers NVIDIA en el host",
|
"title": "Instalar drivers NVIDIA en el host",
|
||||||
"description": "Instala el driver propietario NVIDIA en un host Proxmox VE usando ProxMenux. El instalador gestiona la compatibilidad de kernel, el blacklisting de nouveau, la configuración VFIO, el servicio de persistencia y puede propagar el driver a cualquier contenedor LXC que ya tenga passthrough NVIDIA configurado.",
|
"description": "Instala el driver propietario NVIDIA en un host Proxmox VE usando ProxMenux. El instalador filtra las ramas mantenidas por el PCI ID de la GPU, valida la versión elegida mediante DKMS, gestiona nouveau, instala el servicio de persistencia y puede propagar el driver a contenedores LXC con passthrough NVIDIA.",
|
||||||
"section": "Hardware: GPUs y Coral-TPU"
|
"section": "Hardware: GPUs y Coral-TPU"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Qué hace esto",
|
"title": "Qué hace esto",
|
||||||
"body": "ProxMenux automatiza todo el ciclo de vida del driver NVIDIA en el host: detecta tu GPU, elige una versión de driver compatible con tu kernel en ejecución, pone <code>nouveau</code> en blacklist, descarga y ejecuta el instalador oficial <code>.run</code> de NVIDIA con DKMS, instala el servicio <code>nvidia-persistenced</code> y las reglas udev, y se ofrece a aplicar el parche NVENC opcional. Si ya tienes contenedores LXC con passthrough NVIDIA, puede actualizar las librerías userspace dentro de ellos para que su versión coincida con la del host."
|
"body": "ProxMenux automatiza todo el ciclo de vida del driver NVIDIA en el host: detecta tu GPU, ofrece ramas mantenidas por NVIDIA que incluyen su PCI Device ID, pone <code>nouveau</code> en blacklist, descarga y ejecuta el instalador oficial <code>.run</code> con DKMS, instala <code>nvidia-persistenced</code> y las reglas udev, y ofrece aplicar el parche NVENC opcional. La compilación DKMS es la validación final frente al kernel en ejecución. Si ya tienes contenedores LXC con passthrough NVIDIA, puede actualizar sus librerías de espacio de usuario para que coincidan con el host."
|
||||||
},
|
},
|
||||||
"who": {
|
"who": {
|
||||||
"heading": "¿Para quién es esto?",
|
"heading": "¿Para quién es esto?",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"gpuCheck": "lspci | grep -i nvidia",
|
"gpuCheck": "lspci | grep -i nvidia",
|
||||||
"notVm": "La GPU <strong>no está asignada actualmente a una VM vía passthrough VFIO</strong>. Si lo está, el script se negará a instalar el driver del host para evitar romper la config de passthrough.",
|
"notVm": "La GPU <strong>no está asignada actualmente a una VM vía passthrough VFIO</strong>. Si lo está, el script se negará a instalar el driver del host para evitar romper la config de passthrough.",
|
||||||
"internet": "Acceso a internet en el host. El instalador descarga el driver desde <code>download.nvidia.com</code> y, opcionalmente, clona <code>nvidia-persistenced</code> y <code>nvidia-patch</code> desde GitHub.",
|
"internet": "Acceso a internet en el host. El instalador descarga el driver desde <code>download.nvidia.com</code> y, opcionalmente, clona <code>nvidia-persistenced</code> y <code>nvidia-patch</code> desde GitHub.",
|
||||||
"space": "Unos <strong>2 GB de espacio libre</strong> en <code>/opt/nvidia</code> (workdir) más la RAM usada durante la instalación. Se necesita reiniciar al final."
|
"space": "Algo de espacio libre en <code>/opt/nvidia</code> para el instalador <code>.run</code> más la RAM usada durante el build. Al propagar a LXCs con distros no-Arch, cada contenedor necesita al menos 1.5 GB libres; ProxMenux eleva temporalmente la RAM del contenedor a 2 GB y la restaura al terminar. Se necesita reiniciar al final de la instalación del host."
|
||||||
},
|
},
|
||||||
"vmWarn": {
|
"vmWarn": {
|
||||||
"title": "¿GPU asignada a una VM? Para aquí",
|
"title": "¿GPU asignada a una VM? Para aquí",
|
||||||
@@ -46,40 +46,11 @@
|
|||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"title": "Elegir la versión del driver",
|
"title": "Elegir la versión del driver",
|
||||||
"body1": "ProxMenux obtiene la lista de drivers disponibles de NVIDIA y <strong>filtra las versiones que no son compatibles con tu kernel en ejecución</strong>. La opción <em>Latest available</em> es casi siempre la elección correcta.",
|
"body1": "ProxMenux obtiene la lista de drivers disponibles de NVIDIA y acota el selector a las versiones que <strong>listan el PCI Device ID de tu GPU en la tabla de chips soportados</strong> de la rama correspondiente en <code>nvidia.com</code>. Una serie de heurísticas descartan además las builds developer / beta del CDN que aparecerían al principio. La primera entrada se etiqueta como <em><versión> — Recommended</em>: prefiere la cabeza de la rama del driver ya instalado en el host (bugfix in place), en su defecto la cabeza de la Production Branch actual, y en último caso el número más alto entre los filtrados.",
|
||||||
"body2": "La matriz de compatibilidad que usa el script:",
|
"body2": "Si el driver actualmente instalado fue parcheado con keylase (NVENC), el selector se acota automáticamente a versiones aún cubiertas por la tabla de parches, para que aplicar <em>Reinstalar / actualizar</em> sin perder el parche sea un click.",
|
||||||
"headerKernel": "Kernel",
|
"whyTitle": "Cómo se valida la compatibilidad con el kernel",
|
||||||
"headerPve": "Versión típica de PVE",
|
"whyBody": "La lista se filtra por el mantenimiento de la rama NVIDIA y el soporte del PCI ID de la GPU, no mediante una matriz fija de kernel y driver. Después de elegir, DKMS compila el módulo contra el kernel en ejecución. Si la compilación falla, la instalación no se considera válida; elige otra rama mantenida si NVIDIA todavía no ha adaptado esa versión a tu kernel.",
|
||||||
"headerMin": "Driver NVIDIA mínimo",
|
"imageAlt": "Selector de versiones con ramas NVIDIA compatibles con la GPU y la opción recomendada en primer lugar"
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"kernel": "6.17+",
|
|
||||||
"pve": "Proxmox VE 9.x",
|
|
||||||
"minCode": "580.82.07",
|
|
||||||
"minTail": " o más nuevo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kernel": "6.8 – 6.16",
|
|
||||||
"pve": "Proxmox VE 8.2+",
|
|
||||||
"minCode": "550.x",
|
|
||||||
"minTail": " o más nuevo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kernel": "6.2 – 6.7",
|
|
||||||
"pve": "Proxmox VE 8.0 – 8.1",
|
|
||||||
"minCode": "535.x",
|
|
||||||
"minTail": " o más nuevo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kernel": "5.15+",
|
|
||||||
"pve": "Proxmox VE 7.x (legacy)",
|
|
||||||
"minCode": "470.x",
|
|
||||||
"minTail": " o más nuevo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"whyTitle": "Por qué importa el kernel",
|
|
||||||
"whyBody": "El kernel 6.17 introdujo cambios en la API interna que rompen los drivers NVIDIA más viejos. Si instalas un driver por debajo del mínimo de tu kernel, DKMS no podrá construir el módulo y la GPU no estará disponible después de reiniciar. ProxMenux filtra la lista para que no puedas elegir una versión incompatible por accidente.",
|
|
||||||
"imageAlt": "Selector de versión del driver con las versiones compatibles con el kernel, Latest available arriba"
|
|
||||||
},
|
},
|
||||||
"uninstall": {
|
"uninstall": {
|
||||||
"title": "Desinstalación limpia (solo si reinstalas)",
|
"title": "Desinstalación limpia (solo si reinstalas)",
|
||||||
@@ -90,8 +61,8 @@
|
|||||||
"body": "Tras una única confirmación, el script:",
|
"body": "Tras una única confirmación, el script:",
|
||||||
"items": [
|
"items": [
|
||||||
"Instala <code>pve-headers-$(uname -r)</code> (o <code>proxmox-headers-$(uname -r)</code>), <code>build-essential</code> y <code>dkms</code>.",
|
"Instala <code>pve-headers-$(uname -r)</code> (o <code>proxmox-headers-$(uname -r)</code>), <code>build-essential</code> y <code>dkms</code>.",
|
||||||
"Crea <code>/etc/modprobe.d/nouveau-blacklist.conf</code> poniendo <code>nouveau</code> en blacklist e intenta descargarlo inmediatamente.",
|
"Crea el archivo propiedad de ProxMenux <code>/etc/modprobe.d/proxmenux-nouveau-blacklist.conf</code> con <code>blacklist nouveau</code> y <code>options nouveau modeset=0</code>, registra si añadió la línea complementaria a <code>blacklist.conf</code> e intenta descargar el módulo inmediatamente.",
|
||||||
"Escribe <code>/etc/modules-load.d/nvidia-vfio.conf</code> con <code>vfio</code>, <code>vfio_pci</code>, <code>nvidia</code>, <code>nvidia_uvm</code> y módulos relacionados."
|
"Escribe <code>/etc/modules-load.d/nvidia-vfio.conf</code> con <code>nvidia</code> y <code>nvidia_uvm</code> para que los módulos se carguen pronto en el arranque."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
@@ -112,7 +83,7 @@
|
|||||||
"propagate": {
|
"propagate": {
|
||||||
"title": "Opcional: propagar el driver a los contenedores LXC",
|
"title": "Opcional: propagar el driver a los contenedores LXC",
|
||||||
"body1": "Si la pantalla de resumen listó contenedores con passthrough NVIDIA, ProxMenux ahora se ofrece a actualizar las librerías userspace dentro de cada uno para que coincidan con el host. El módulo de kernel del host y el userspace del contenedor <strong>deben ser exactamente la misma versión</strong> — si no <code>nvidia-smi</code> dentro del contenedor fallará con un error \"version mismatch\".",
|
"body1": "Si la pantalla de resumen listó contenedores con passthrough NVIDIA, ProxMenux ahora se ofrece a actualizar las librerías userspace dentro de cada uno para que coincidan con el host. El módulo de kernel del host y el userspace del contenedor <strong>deben ser exactamente la misma versión</strong> — si no <code>nvidia-smi</code> dentro del contenedor fallará con un error \"version mismatch\".",
|
||||||
"body2": "La actualización es consciente de la distro: <code>apk</code> para Alpine, <code>pacman</code> para Arch y el mismo instalador <code>.run</code> (con <code>--no-kernel-modules --no-dkms --no-install-compat32-libs</code>) para Debian/Ubuntu y otras distros. Eleva temporalmente la RAM del contenedor a 2 GB si es menor, ejecuta la instalación y luego restaura la RAM original.",
|
"body2": "La actualización es consciente de la distro. Para Debian / Ubuntu y otras distros glibc, el mismo instalador <code>.run</code> (con <code>--no-kernel-modules --no-dkms --no-install-compat32-libs</code>) se copia al contenedor y se ejecuta; la RAM del contenedor se eleva temporalmente a 2 GB si es menor y se restaura al terminar. Para <strong>Arch, Manjaro y EndeavourOS</strong> la actualización es un <code>pacman -Syu nvidia-utils</code> pineado a la rama del driver del host. <strong>Alpine</strong> usa un camino distinto — se extrae el <code>.run</code> en el host, solo las librerías userspace se empaquetan como tarball y se envían con <code>pct push</code>, luego se instalan los shims <code>gcompat</code> + <code>binutils</code> vía <code>apk</code> y se recrean los symlinks SONAME con <code>readelf</code> para que las librerías glibc-linked carguen correctamente sobre musl.",
|
||||||
"imageAlt": "Prompt listando los LXCs con passthrough NVIDIA y la versión actual del driver, con Sí/No para actualizarlos todos"
|
"imageAlt": "Prompt listando los LXCs con passthrough NVIDIA y la versión actual del driver, con Sí/No para actualizarlos todos"
|
||||||
},
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
@@ -122,33 +93,32 @@
|
|||||||
},
|
},
|
||||||
"reinstallUninstall": {
|
"reinstallUninstall": {
|
||||||
"heading": "Reinstalar o desinstalar",
|
"heading": "Reinstalar o desinstalar",
|
||||||
"intro": "Cuando el instalador detecta que ya hay un driver NVIDIA cargado (<code>nvidia-smi</code> devuelve una versión), no reinstala silenciosamente encima. En lugar de eso muestra un menú de acciones para que elijas qué hacer.",
|
"intro": "Cuando el instalador detecta que el módulo de kernel <code>nvidia</code> está cargado actualmente y <code>nvidia-smi</code> devuelve una versión, no reinstala silenciosamente encima. En lugar de eso muestra un menú de acciones para que elijas qué hacer. (Los binarios presentes en disco pero el módulo no cargado no cuentan como instalados — el módulo tiene que estar activo.)",
|
||||||
"imageAlt": "Menú de acciones NVIDIA ofrecido cuando ya hay un driver instalado — dos opciones: Reinstalar / actualizar driver, o Desinstalar el driver NVIDIA completamente",
|
"imageAlt": "Menú de acciones NVIDIA ofrecido cuando ya hay un driver instalado — dos opciones: Reinstalar / actualizar driver, o Desinstalar el driver NVIDIA completamente",
|
||||||
"imageCaption": "El menú de acciones solo aparece cuando hay un driver NVIDIA activo actualmente en el host.",
|
"imageCaption": "El menú de acciones solo aparece cuando hay un driver NVIDIA activo actualmente en el host.",
|
||||||
"reinstallHeading": "Reinstalar / actualizar",
|
"reinstallHeading": "Reinstalar / actualizar",
|
||||||
"reinstallBody": "Continúa con el flujo normal de instalación pero, antes de descargar nada, ejecuta una eliminación limpia del driver actual (apt purge + entradas DKMS quitadas + módulos residuales descargados). Esta es la ruta segura para aplicar una versión más nueva del driver, cambiar de rama cuando el kernel lo exige o recuperarte de un estado medio roto. Los prompts de propagación LXC y parche NVENC se vuelven a ejecutar al final.",
|
"reinstallBody": "Continúa con el flujo normal de instalación pero, antes de descargar nada, ejecuta una eliminación limpia del driver actual (apt purge + entradas DKMS eliminadas + módulos residuales descargados). Es la ruta segura para aplicar una versión más nueva de la misma rama, elegir otra rama mantenida cuando sea necesario o recuperarse de un estado incompleto. Al final vuelven a mostrarse las opciones de propagación LXC y del parche NVENC.",
|
||||||
"uninstallHeading": "Desinstalar — qué se elimina",
|
"uninstallHeading": "Desinstalar — qué se elimina",
|
||||||
"uninstallIntro": "Confirma primero con un diálogo sí/no. Luego ejecuta un rollback completo e idempotente:",
|
"uninstallIntro": "Confirma primero con un diálogo sí/no. Luego ejecuta un rollback completo e idempotente:",
|
||||||
"uninstallItems": [
|
"uninstallItems": [
|
||||||
"Para y deshabilita <code>nvidia-persistenced</code>, descarga los módulos de kernel (<code>nvidia_uvm</code>, <code>nvidia_drm</code>, <code>nvidia_modeset</code>, <code>nvidia</code>) — cualquier contenedor LXC con passthrough NVIDIA será cortado limpiamente.",
|
"Ejecuta primero <code>nvidia-uninstall --silent</code> (el reverso del instalador <code>.run</code>), luego para y deshabilita <code>nvidia-persistenced</code> y <code>nvidia-powerd</code>, y descarga los módulos de kernel (<code>nvidia_uvm</code>, <code>nvidia_drm</code>, <code>nvidia_modeset</code>, <code>nvidia</code>) — cualquier contenedor LXC con passthrough NVIDIA será cortado limpiamente.",
|
||||||
"Ejecuta <code>apt purge</code> sobre cada paquete NVIDIA, quita el árbol fuente DKMS y la caché del instalador .run de <code>/opt/nvidia</code>.",
|
"Ejecuta <code>apt purge</code> sobre <code>nvidia-*</code>, <code>libnvidia-*</code>, <code>cuda-*</code> y <code>libcudnn*</code>, quita el árbol fuente DKMS y la caché del instalador .run de <code>/opt/nvidia</code>.",
|
||||||
"Revierte el blacklist de nouveau (<code>/etc/modprobe.d/nouveau-blacklist.conf</code>) y la config de modules-load (<code>/etc/modules-load.d/nvidia-vfio.conf</code>) para que nouveau pueda volver si quieres gráficos genéricos otra vez.",
|
"Quita la configuración de carga de módulos (<code>/etc/modules-load.d/nvidia-vfio.conf</code>) y las entradas de blacklist de nouveau creadas por ProxMenux. También migra y elimina los archivos antiguos de ProxMenux con las dos líneas conocidas; conserva los archivos modificados o ajenos del administrador.",
|
||||||
"Quita las reglas udev (<code>/etc/udev/rules.d/70-nvidia.rules</code>) y el archivo de estado del parche NVENC (si el parche keylase se aplicó antes).",
|
"Quita las reglas udev (<code>/etc/udev/rules.d/70-nvidia.rules</code>) y limpia el estado del parche NVENC (un campo del registro de instalaciones gestionadas de ProxMenux, marcado como <em>removed</em> — no hay fichero aparte que borrar).",
|
||||||
"Reconstruye <code>initramfs</code> para todos los kernels y pide reiniciar para finalizar (el desblacklisting de nouveau solo surte efecto tras reiniciar)."
|
"Reconstruye <code>initramfs</code> para todos los kernels, ejecuta <code>proxmox-boot-tool refresh</code> en hosts con systemd-boot, y pide reiniciar para finalizar."
|
||||||
],
|
],
|
||||||
"lxcWarnTitle": "Contenedores LXC con passthrough NVIDIA",
|
"lxcWarnTitle": "Contenedores LXC con passthrough NVIDIA",
|
||||||
"lxcWarnBody": "Quitar el driver del host invalida las rutas de dispositivo y las librerías CUDA mapeadas a cualquier LXC con passthrough NVIDIA. Planifica la operación en una ventana de mantenimiento si Frigate / Plex / Jellyfin / Ollama (o cualquier otra cosa) depende de ello."
|
"lxcWarnBody": "Quitar el driver del host invalida las rutas de dispositivo y las librerías CUDA mapeadas a cualquier LXC con passthrough NVIDIA. Planifica la operación en una ventana de mantenimiento si Frigate / Plex / Jellyfin / Ollama (o cualquier otra cosa) depende de ello."
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"heading": "Notificaciones de actualización",
|
"heading": "Notificaciones de actualización",
|
||||||
"body": "El driver NVIDIA instalado se rastrea en el registro de instalaciones gestionadas de ProxMenux. En el arranque y cada 24h el Monitor comprueba el listado upstream en <code>download.nvidia.com/XFree86/Linux-x86_64/</code> contra la versión que reporta <code>nvidia-smi</code>, y dispara una notificación cuando hay una nueva versión compatible disponible.",
|
"body": "El driver NVIDIA instalado se rastrea en el registro de instalaciones gestionadas de ProxMenux. En el arranque y cada 24 horas, el Monitor compara el listado de <code>download.nvidia.com/XFree86/Linux-x86_64/</code> con la versión que devuelve <code>nvidia-smi</code> y solo avisa cuando existe una versión de mantenimiento más nueva en la rama instalada.",
|
||||||
"kindsHeading": "Dos tipos de mensaje de actualización",
|
"kindsHeading": "Mensaje de actualización",
|
||||||
"kindsItems": [
|
"kindsItems": [
|
||||||
"<strong>Parche de la misma rama.</strong> Una release de mantenimiento más nueva en tu rama actual de driver (p. ej. instalado 580.65.06 → disponible 580.105.08). Bug fixes y parches de seguridad sin cambiar de rama.",
|
"<strong>Mantenimiento de la misma rama.</strong> Una versión más reciente dentro de la rama instalada (por ejemplo, 580.65.06 instalada → 580.105.08 disponible). El Monitor no deduce compatibilidad entre ramas y kernels."
|
||||||
"<strong>Subida de rama requerida por el kernel.</strong> Si el host está en un kernel que ya no soporta tu rama actual (p. ej. subiste el kernel del host a 6.17 mientras seguías en el driver 570.x), el mensaje lo dice explícitamente y recomienda la rama mínima compatible con el kernel — la misma matriz que usa el instalador para filtrar el menú de versión."
|
|
||||||
],
|
],
|
||||||
"antiTitle": "Anti-cascada por diseño",
|
"antiTitle": "Anti-cascada por diseño",
|
||||||
"antiBody": "Una notificación por versión upstream distinta, nunca en cada escaneo de 24h. El mensaje de subida de rama en particular solo se dispara cuando realmente necesitas cambiar — hasta entonces el tracker de la misma rama se queda silenciado.",
|
"antiBody": "Una notificación por cada versión nueva distinta, nunca en cada comprobación de 24 horas. Si no hay una versión más reciente en la rama instalada, el seguimiento permanece silencioso.",
|
||||||
"applyTitle": "Aplicar la actualización",
|
"applyTitle": "Aplicar la actualización",
|
||||||
"applyBody": "El Monitor no autoaplica actualizaciones de driver — reinstalar el driver NVIDIA siempre necesita un reinicio. Abre la misma entrada del instalador descrita arriba, elige <strong>Reinstall / update</strong> y la nueva versión se descarga, el módulo DKMS se reconstruye contra el kernel en ejecución y se pide el reinicio al final."
|
"applyBody": "El Monitor no autoaplica actualizaciones de driver — reinstalar el driver NVIDIA siempre necesita un reinicio. Abre la misma entrada del instalador descrita arriba, elige <strong>Reinstall / update</strong> y la nueva versión se descarga, el módulo DKMS se reconstruye contra el kernel en ejecución y se pide el reinicio al final."
|
||||||
},
|
},
|
||||||
@@ -161,7 +131,7 @@
|
|||||||
"troubleshoot": {
|
"troubleshoot": {
|
||||||
"heading": "Solución de problemas",
|
"heading": "Solución de problemas",
|
||||||
"smiFailTitle": "`nvidia-smi` dice 'NVIDIA-SMI has failed'",
|
"smiFailTitle": "`nvidia-smi` dice 'NVIDIA-SMI has failed'",
|
||||||
"smiFailBody": "Casi siempre es <strong>nouveau</strong> aún cargado o un <strong>mismatch de headers del kernel</strong>. Tras reiniciar, ejecuta <code>lsmod | grep nouveau</code> — si devuelve algo, el blacklist no surtió efecto (comprueba que <code>/etc/modprobe.d/nouveau-blacklist.conf</code> existe y reconstruye initramfs con <code>update-initramfs -u -k all</code>, luego reinicia). Si nouveau no está, comprueba <code>dmesg | grep -i nvidia</code> — los errores de build DKMS suelen significar que tus headers de kernel no coinciden con el kernel en ejecución; reinstálalos con <code>apt install --reinstall pve-headers-$(uname -r)</code>.",
|
"smiFailBody": "Casi siempre se debe a que <strong>nouveau</strong> sigue cargado o a que las <strong>cabeceras no coinciden con el kernel</strong>. Tras reiniciar, ejecuta <code>lsmod | grep nouveau</code>. Si devuelve algo, comprueba <code>/etc/modprobe.d/proxmenux-nouveau-blacklist.conf</code>, reconstruye initramfs con <code>update-initramfs -u -k all</code> y reinicia. Si nouveau no aparece, revisa <code>dmesg | grep -i nvidia</code>; los errores de DKMS suelen indicar que faltan las cabeceras del kernel en ejecución.",
|
||||||
"lxcMissTitle": "El contenedor LXC no ve la GPU tras actualizar el host",
|
"lxcMissTitle": "El contenedor LXC no ve la GPU tras actualizar el host",
|
||||||
"lxcMissBody": "Las librerías userspace del contenedor están atascadas en la versión anterior del driver. O vuelves a ejecutar el instalador NVIDIA y aceptas el prompt de propagación LXC, o instalas la misma versión del driver manualmente dentro del contenedor con <code>--no-kernel-modules</code>.",
|
"lxcMissBody": "Las librerías userspace del contenedor están atascadas en la versión anterior del driver. O vuelves a ejecutar el instalador NVIDIA y aceptas el prompt de propagación LXC, o instalas la misma versión del driver manualmente dentro del contenedor con <code>--no-kernel-modules</code>.",
|
||||||
"logTitle": "Revisa el log de instalación",
|
"logTitle": "Revisa el log de instalación",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"prereqs": {
|
"prereqs": {
|
||||||
"title": "Antes de empezar",
|
"title": "Antes de empezar",
|
||||||
"assigned": "<strong>Una GPU ya asignada</strong> — o bien en una VM vía VFIO o adjuntada al menos a un LXC. Si aún no la has asignado, empieza desde Añadir GPU a VM / LXC en su lugar.",
|
"assigned": "<strong>Una GPU ya asignada</strong> — o bien en una VM vía VFIO o adjuntada al menos a un LXC. Si aún no la has asignado, empieza desde Añadir GPU a VM / LXC en su lugar.",
|
||||||
"iommu": "<strong>IOMMU habilitado en el host</strong> — solo estrictamente necesario al cambiar <em>a</em> modo VM, pero vale la pena tenerlo en cualquier caso. El script avisa si falta el parámetro del kernel.",
|
"iommu": "<strong>IOMMU habilitado en el host</strong> — solo estrictamente necesario al cambiar <em>a</em> modo VM, pero vale la pena tenerlo en cualquier caso. Si falta el parámetro del kernel, el script lo añade automáticamente al command line de arranque (<code>intel_iommu=on iommu=pt</code> o <code>amd_iommu=on</code>, vía <code>proxmox-boot-tool refresh</code> en systemd-boot o <code>update-grub</code> en GRUB) y lo incluye en el aviso de reinicio final.",
|
||||||
"iommuCheck": "dmesg | grep -i 'IOMMU enabled' | head -1",
|
"iommuCheck": "dmesg | grep -i 'IOMMU enabled' | head -1",
|
||||||
"reboot": "<strong>Asume un reinicio.</strong> Cambiar bindings de GPU a nivel de kernel significa que el host regenera initramfs y reinicias para aplicar. El script lo pide al final.",
|
"reboot": "<strong>Asume un reinicio.</strong> Cambiar bindings de GPU a nivel de kernel significa que el host regenera initramfs y reinicias para aplicar. El script lo pide al final.",
|
||||||
"knowList": "<strong>Saber qué VMs / LXCs están usando la GPU.</strong> El script las encontrará y preguntará qué hacer con cada una, pero es más rápido si ya conoces la lista."
|
"knowList": "<strong>Saber qué VMs / LXCs están usando la GPU.</strong> El script las encontrará y preguntará qué hacer con cada una, pero es más rápido si ya conoces la lista."
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Notificaciones",
|
"title": "Notificaciones",
|
||||||
"description": "Telegram, Discord, Email, Gotify y Apprise (multicanal) — con deduplicación, cooldown, agregación de ráfagas, horas silenciosas y un historial completo.",
|
"description": "Telegram, Discord, Email, Gotify, Pushover y Apprise (multicanal), con deduplicación, cooldown, agregación de ráfagas, horas silenciosas e historial completo.",
|
||||||
"icon": "Bell",
|
"icon": "Bell",
|
||||||
"href": "/docs/monitor/notifications"
|
"href": "/docs/monitor/notifications"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -254,8 +254,8 @@
|
|||||||
"items": [
|
"items": [
|
||||||
"<strong>Los watchers</strong> empujan eventos: <code>JournalWatcher</code> sigue el journal del sistema, <code>TaskWatcher</code> hace polling de la lista de tareas Proxmox, <code>ProxmoxHookWatcher</code> reacciona a hooks de backup / replicación / snapshot y <code>PollingCollector</code> gestiona fuentes de datos lentas.",
|
"<strong>Los watchers</strong> empujan eventos: <code>JournalWatcher</code> sigue el journal del sistema, <code>TaskWatcher</code> hace polling de la lista de tareas Proxmox, <code>ProxmoxHookWatcher</code> reacciona a hooks de backup / replicación / snapshot y <code>PollingCollector</code> gestiona fuentes de datos lentas.",
|
||||||
"<strong>Las templates</strong> convierten un evento en un par (título, cuerpo). La misma template puede pasar por el proveedor de IA configurado (OpenAI / Anthropic / Gemini / Groq / Ollama / OpenRouter) para producir una reescritura en lenguaje natural; ambas versiones se guardan en <code>notification_history</code>.",
|
"<strong>Las templates</strong> convierten un evento en un par (título, cuerpo). La misma template puede pasar por el proveedor de IA configurado (OpenAI / Anthropic / Gemini / Groq / Ollama / OpenRouter) para producir una reescritura en lenguaje natural; ambas versiones se guardan en <code>notification_history</code>.",
|
||||||
"<strong>Los canales</strong> entregan los mensajes: Telegram, Discord, Email, Gotify y Apprise (multicanal). Cada uno está implementado en <code>notification_channels.py</code> detrás de la misma interfaz <code>create_channel()</code> / <code>send()</code>, así que añadir un canal nuevo es una sola clase.",
|
"<strong>Los canales</strong> entregan los mensajes: Telegram, Discord, Email, Gotify, Pushover y Apprise (multicanal). Cada uno está implementado en <code>notification_channels.py</code> mediante la misma interfaz <code>create_channel()</code> / <code>send()</code>.",
|
||||||
"<strong>Cifrado.</strong> Los ajustes sensibles (<code>telegram.token</code>, <code>discord.webhook_url</code>, <code>ai_api_key_*</code>, <code>email.password</code>) se cifran con XOR usando la clave en <code>.notification_key</code> antes de escribirse en la DB. El texto plano nunca toca disco."
|
"<strong>Cifrado.</strong> Los ajustes sensibles (<code>telegram.bot_token</code>, <code>discord.webhook_url</code>, <code>pushover.user_key</code>, <code>pushover.api_token</code>, <code>ai_api_key_*</code>, <code>email.password</code>) se cifran con la clave de <code>.notification_key</code> antes de escribirse en la base de datos y aparecen ocultos en la interfaz."
|
||||||
],
|
],
|
||||||
"linksFooter": "Los toggles por evento, los overrides por canal y la configuración de IA se exponen en <notifLink>Settings → Notifications</notifLink> y <aiLink>Settings → AI Assistant</aiLink>."
|
"linksFooter": "Los toggles por evento, los overrides por canal y la configuración de IA se exponen en <notifLink>Settings → Notifications</notifLink> y <aiLink>Settings → AI Assistant</aiLink>."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"mechanisms": {
|
"mechanisms": {
|
||||||
"heading": "Cómo se selecciona el método",
|
"heading": "Cómo se selecciona el método",
|
||||||
"lead": "La vía integrada de Proxmox VE Helper-Scripts sigue el mecanismo oficial <helper>update-apps</helper>. El resto de instalaciones usa el método de paquetes, Docker o el comando personalizado correspondiente.",
|
"lead": "La vía integrada de Proxmox VE Helper-Scripts sigue el mecanismo oficial <helper>update-apps</helper>. El resto de instalaciones usa el método de paquetes, Docker o el comando personalizado correspondiente.",
|
||||||
|
"officialReference": "Referencia oficial: la <helper>documentación de update-apps de Proxmox VE Helper-Scripts</helper> explica los modos interactivo y desatendido, las copias de seguridad, la simulación, los recursos temporales de compilación, los registros y los códigos de salida.",
|
||||||
"colSource": "Origen",
|
"colSource": "Origen",
|
||||||
"colAction": "Acción mostrada",
|
"colAction": "Acción mostrada",
|
||||||
"colNotes": "Qué se ejecuta",
|
"colNotes": "Qué se ejecuta",
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
"body1": "Dentro del panel, el <strong>Health Monitor</strong> se ejecuta continuamente en segundo plano y produce un flujo estructurado de eventos: alta temperatura de CPU, avisos SMART de discos, degradación de pools ZFS, OOM kills, fallos de VM/CT, incidentes de seguridad, etc. Cada evento tiene una categoría, una severidad (INFO / WARNING / CRITICAL) y un <code>error_key</code> estable para que los duplicados se colapsen en vez de inundar la pantalla.",
|
"body1": "Dentro del panel, el <strong>Health Monitor</strong> se ejecuta continuamente en segundo plano y produce un flujo estructurado de eventos: alta temperatura de CPU, avisos SMART de discos, degradación de pools ZFS, OOM kills, fallos de VM/CT, incidentes de seguridad, etc. Cada evento tiene una categoría, una severidad (INFO / WARNING / CRITICAL) y un <code>error_key</code> estable para que los duplicados se colapsen en vez de inundar la pantalla.",
|
||||||
"feedsIntro": "Los eventos alimentan tres cosas al mismo tiempo:",
|
"feedsIntro": "Los eventos alimentan tres cosas al mismo tiempo:",
|
||||||
"feedsHealth": "La <strong>vista del Health Monitor</strong> en el panel (listas de activas + descartadas).",
|
"feedsHealth": "La <strong>vista del Health Monitor</strong> en el panel (listas de activas + descartadas).",
|
||||||
"feedsChannels": "El <strong>motor de notificaciones</strong> — Telegram, Discord, Email, Gotify y Apprise (multicanal). Cada canal se configura independientemente y se pueden silenciar categorías por evento.",
|
"feedsChannels": "El <strong>motor de notificaciones</strong>: Telegram, Discord, Email, Gotify, Pushover y Apprise (multicanal). Cada canal se configura de forma independiente y permite silenciar categorías por evento.",
|
||||||
"feedsAI": "El <strong>asistente IA</strong> opcional — cuando está activado, el proveedor configurado (OpenAI, Anthropic, Gemini, Groq, Ollama u OpenRouter) explica los eventos entrantes en lenguaje claro y propone próximos pasos si está activado en los ajustes de la IA.",
|
"feedsAI": "El <strong>asistente IA</strong> opcional — cuando está activado, el proveedor configurado (OpenAI, Anthropic, Gemini, Groq, Ollama u OpenRouter) explica los eventos entrantes en lenguaje claro y propone próximos pasos si está activado en los ajustes de la IA.",
|
||||||
"suppressionTitle": "Supresión en vez de silenciar todo",
|
"suppressionTitle": "Supresión en vez de silenciar todo",
|
||||||
"suppressionBody": "Cada categoría tiene su propia <em>duración de supresión</em>: una vez que descartas una alerta, la misma alerta se silencia durante esa ventana (24 horas por defecto, configurable por categoría hasta permanente). Las escalaciones reales — p. ej. la temperatura de CPU cruzando el umbral crítico — siempre se vuelven a disparar independientemente de la supresión."
|
"suppressionBody": "Cada categoría tiene su propia <em>duración de supresión</em>: una vez que descartas una alerta, la misma alerta se silencia durante esa ventana (24 horas por defecto, configurable por categoría hasta permanente). Las escalaciones reales — p. ej. la temperatura de CPU cruzando el umbral crítico — siempre se vuelven a disparar independientemente de la supresión."
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Notificaciones de Proxmox — Telegram, Discord, Email, Gotify, Apprise | ProxMenux Monitor",
|
"title": "Notificaciones de Proxmox — Telegram, Discord, Email, Gotify, Pushover, Apprise | ProxMenux Monitor",
|
||||||
"description": "Envía notificaciones de Proxmox VE a Telegram, Discord, Email, Gotify y ~80 servicios extra vía Apprise. ProxMenux Monitor convierte eventos del Monitor de salud, el journal watcher y el webhook de Proxmox VE en mensajes ricos con deduplicación, cooldown, agregación de ráfagas, una reescritura con IA opcional y un historial completo.",
|
"description": "Envía notificaciones de Proxmox VE a Telegram, Discord, Email, Gotify, Pushover y unos 80 servicios adicionales mediante Apprise. ProxMenux Monitor convierte los eventos del monitor de salud, el journal watcher y el webhook de Proxmox VE en mensajes enriquecidos con deduplicación, cooldown, agregación de ráfagas, una reescritura opcional con IA y un historial completo.",
|
||||||
"ogTitle": "Notificaciones de Proxmox — Telegram, Discord, Email, Gotify, Apprise",
|
"ogTitle": "Notificaciones de Proxmox — Telegram, Discord, Email, Gotify, Pushover, Apprise",
|
||||||
"ogDescription": "Envía alertas de Proxmox VE a Telegram, Discord, Email, Gotify y ~80 servicios extra vía Apprise — con deduplicación, cooldown, agregación de ráfagas y una reescritura con IA opcional.",
|
"ogDescription": "Envía alertas de Proxmox VE a Telegram, Discord, Email, Gotify, Pushover y unos 80 servicios adicionales mediante Apprise.",
|
||||||
"twitterTitle": "Notificaciones de Proxmox | ProxMenux Monitor",
|
"twitterTitle": "Notificaciones de Proxmox | ProxMenux Monitor",
|
||||||
"twitterDescription": "Envía alertas de Proxmox VE a Telegram, Discord, Email, Gotify y ~80 servicios extra vía Apprise."
|
"twitterDescription": "Envía alertas de Proxmox VE a Telegram, Discord, Email, Gotify, Pushover y unos 80 servicios adicionales mediante Apprise."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"description": "El motor de fan-out que toma eventos de cada colector dentro del Monitor y los entrega a Telegram, Discord, Email, Gotify y ~80 servicios extra vía Apprise — con deduplicación, cooldown, agregación de ráfagas, toggles por evento y por canal, un reescritor de IA opcional y un historial consultable.",
|
"description": "El motor de distribución que recibe eventos de todos los colectores del Monitor y los entrega a Telegram, Discord, Email, Gotify, Pushover y unos 80 servicios adicionales mediante Apprise, con deduplicación, cooldown, agregación de ráfagas, controles por evento y por canal, reescritura opcional con IA e historial consultable.",
|
||||||
"section": "ProxMenux Monitor"
|
"section": "ProxMenux Monitor"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"aiLabel": "Reescritura IA (opc.)",
|
"aiLabel": "Reescritura IA (opc.)",
|
||||||
"aiDetail": "OpenAI / Anthropic\nGemini / Groq\nOpenRouter / Ollama\n(off por defecto)",
|
"aiDetail": "OpenAI / Anthropic\nGemini / Groq\nOpenRouter / Ollama\n(off por defecto)",
|
||||||
"channelsLabel": "Canales",
|
"channelsLabel": "Canales",
|
||||||
"channelsDetail": "Telegram\nDiscord\nEmail (SMTP)\nGotify\nApprise (~80 servicios)"
|
"channelsDetail": "Telegram\nDiscord\nEmail (SMTP)\nGotify\nPushover\nApprise (~80 servicios)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabling": {
|
"enabling": {
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
"Registra un destino webhook de Proxmox VE en <code>/etc/pve/notifications.cfg</code> apuntando a <code>POST http://127.0.0.1:8008/api/notifications/webhook</code>. Desde este momento, todo lo que Proxmox VE emite por sí mismo (HA, replicación, vzdump desde la GUI) fluye a la misma pipeline que los eventos propios del Monitor. Mira <pvelink>Integración del webhook de PVE</pvelink> más abajo para la mecánica completa.",
|
"Registra un destino webhook de Proxmox VE en <code>/etc/pve/notifications.cfg</code> apuntando a <code>POST http://127.0.0.1:8008/api/notifications/webhook</code>. Desde este momento, todo lo que Proxmox VE emite por sí mismo (HA, replicación, vzdump desde la GUI) fluye a la misma pipeline que los eventos propios del Monitor. Mira <pvelink>Integración del webhook de PVE</pvelink> más abajo para la mecánica completa.",
|
||||||
"Arranca el hilo de fondo de despacho. El hilo hace polling de la cola de eventos y camina cada evento por la pipeline diagramada arriba."
|
"Arranca el hilo de fondo de despacho. El hilo hace polling de la cola de eventos y camina cada evento por la pipeline diagramada arriba."
|
||||||
],
|
],
|
||||||
"activeAlt": "Tarjeta Notifications tras activar — badge Active, pestañas de canal (Telegram, Gotify, Discord, Email), campo Display Name y sección colapsable Advanced AI Enhancement",
|
"activeAlt": "Tarjeta de notificaciones activada con pestañas de canales, nombre visible y opciones avanzadas de IA",
|
||||||
"activeCaption": "Estado Active — pestañas de canal arriba (Telegram / Gotify / Discord / Email), el campo Display Name, la lista de categorías por canal y la sección colapsable <em>Advanced: AI Enhancement</em>."
|
"activeCaption": "Estado activo: pestañas Telegram, Gotify, Discord, Email, Pushover y Apprise, nombre visible, categorías por canal y opciones avanzadas de IA."
|
||||||
},
|
},
|
||||||
"sources": {
|
"sources": {
|
||||||
"heading": "Fuentes de eventos",
|
"heading": "Fuentes de eventos",
|
||||||
@@ -89,9 +89,9 @@
|
|||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"heading": "Walkthroughs de canales",
|
"heading": "Walkthroughs de canales",
|
||||||
"intro": "Cinco canales están actualmente soportados: Telegram, Discord, Gotify, Email (SMTP) y Apprise. Los primeros cuatro son nativos — cada uno tiene su propia pestaña dentro del panel Notifications con un enlace <em>+ setup guide</em> que abre un modal in-app. Apprise es un hub genérico que añade ~80 servicios adicionales (ntfy, Matrix, Pushover, Slack, Teams, Pushbullet, AWS SNS, Mattermost…) a través de un único campo de URL. Están todos documentados paso a paso abajo.",
|
"intro": "Actualmente se admiten seis canales: Telegram, Discord, Gotify, Email (SMTP), Pushover y Apprise. Los cinco primeros son integraciones nativas con sus propios campos de configuración. Apprise funciona como un concentrador genérico que añade unos 80 servicios adicionales (ntfy, Matrix, Slack, Teams, Pushbullet, AWS SNS, Mattermost…) mediante una única URL. Todos están documentados paso a paso a continuación.",
|
||||||
"credsTitle": "Dónde viven las credenciales",
|
"credsTitle": "Dónde viven las credenciales",
|
||||||
"credsBody": "Los tokens, URLs de webhook y contraseñas SMTP se guardan localmente en la base de datos SQLite del Monitor bajo <code>/usr/local/share/proxmenux/</code>. Nunca salen del host excepto para llegar a sus respectivos servicios. Un backup de ese directorio basta para recuperar los canales configurados."
|
"credsBody": "Los tokens, las claves, las URL de webhook y las contraseñas SMTP se guardan localmente en la base de datos SQLite del Monitor dentro de <code>/usr/local/share/proxmenux/</code>. Los valores sensibles están protegidos y se ocultan en la interfaz. Solo salen del host para comunicarse con el servicio correspondiente. Una copia de ese directorio permite recuperar los canales configurados."
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"heading": "Telegram",
|
"heading": "Telegram",
|
||||||
@@ -176,12 +176,28 @@
|
|||||||
"relayTitle": "Relay SMTP autoalojado",
|
"relayTitle": "Relay SMTP autoalojado",
|
||||||
"relayBody": "Si corres tu propio relay SMTP (Postfix, msmtp, etc.) en la LAN, apunta el Monitor a él y saltea el baile de app-password por completo. El relay maneja la auth upstream y el Monitor envía en cleartext sobre una red de confianza."
|
"relayBody": "Si corres tu propio relay SMTP (Postfix, msmtp, etc.) en la LAN, apunta el Monitor a él y saltea el baile de app-password por completo. El relay maneja la auth upstream y el Monitor envía en cleartext sobre una red de confianza."
|
||||||
},
|
},
|
||||||
|
"pushover": {
|
||||||
|
"heading": "Pushover",
|
||||||
|
"intro": "Pushover es un servicio de notificaciones push con aplicaciones oficiales para iOS, Android y navegadores de escritorio. El canal específico de ProxMenux se comunica directamente con la <a>API de Pushover</a>, por lo que no necesita una URL de Apprise.",
|
||||||
|
"stepsTitle": "Configuración",
|
||||||
|
"steps": [
|
||||||
|
"Crea una <a>cuenta de Pushover</a>, instala la aplicación oficial en los dispositivos que recibirán las alertas e inicia sesión.",
|
||||||
|
"Copia la <em>User Key</em> que aparece en el panel de Pushover. También puedes usar una clave de grupo si varias personas o dispositivos deben recibir la misma alerta.",
|
||||||
|
"Abre <a>Create an Application/API Token</a>, crea una aplicación llamada <em>ProxMenux</em> y copia su token API de 30 caracteres.",
|
||||||
|
"En <em>Ajustes → Notificaciones → Pushover</em>, pega la clave de usuario o grupo y el token API de la aplicación. Los campos de dispositivo y sonido son opcionales.",
|
||||||
|
"Guarda los ajustes y pulsa <em>Enviar prueba</em>. La aplicación Pushover debería recibir el mensaje inmediatamente."
|
||||||
|
],
|
||||||
|
"priorityTitle": "Asignación de prioridad",
|
||||||
|
"priorityBody": "Las notificaciones normales de ProxMenux usan la prioridad 0 de Pushover. Si activas <strong>Prioridad alta para alertas críticas</strong>, los eventos CRÍTICOS usan la prioridad 1 para destacar e ignorar las horas de silencio configuradas por el usuario en Pushover. ProxMenux no usa la prioridad de emergencia 2, que exige notificaciones repetidas y una confirmación mediante callback.",
|
||||||
|
"secretTitle": "Protege ambos valores",
|
||||||
|
"secretBody": "Tanto la clave de usuario o grupo como el token API de la aplicación autorizan el envío de mensajes. ProxMenux los almacena como secretos protegidos y los oculta en la interfaz; no publiques ninguno de los dos en capturas ni registros de soporte."
|
||||||
|
},
|
||||||
"apprise": {
|
"apprise": {
|
||||||
"heading": "Apprise (hub genérico para ~80 servicios)",
|
"heading": "Apprise (hub genérico para ~80 servicios)",
|
||||||
"intro": "Apprise es una librería de notificaciones de código abierto que habla el protocolo de unos 80 servicios distintos a través de un único formato de URL. Añadirlo como un canal más dentro del Monitor significa que puedes entregar alertas a servicios que no tienen una pestaña dedicada — ntfy, Matrix, Pushover, Slack, Microsoft Teams, Mattermost, Pushbullet, AWS SNS, Pushsafer, Rocket.Chat, Signal API y muchos otros — sin que ProxMenux tenga que implementar cada integración por separado.",
|
"intro": "Apprise es una biblioteca de notificaciones de código abierto compatible con unos 80 servicios mediante un único formato de URL. Permite enviar alertas a servicios sin pestaña propia, como ntfy, Matrix, Slack, Microsoft Teams, Mattermost, Pushbullet, AWS SNS, Pushsafer, Rocket.Chat o Signal API. Pushover también puede utilizarse mediante Apprise, aunque su pestaña específica es más sencilla para un único destino Pushover.",
|
||||||
"listIntro": "La lista completa de servicios soportados y el formato exacto de URL para cada uno vive en la wiki oficial de Apprise:",
|
"listIntro": "La lista completa de servicios compatibles y el formato exacto de cada URL están disponibles en la documentación oficial de Apprise:",
|
||||||
"listItems": [
|
"listItems": [
|
||||||
"<a>github.com/caronc/apprise/wiki</a> — índice completo de servicios soportados.",
|
"<a>Documentación de servicios de Apprise</a> — índice completo de servicios compatibles.",
|
||||||
"<a>URL basics</a> — cómo se estructuran las URLs de Apprise."
|
"<a>URL basics</a> — cómo se estructuran las URLs de Apprise."
|
||||||
],
|
],
|
||||||
"stepsTitle": "Pasos",
|
"stepsTitle": "Pasos",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Script Automatizado post-instalación | ProxMenux Documentation",
|
"title": "Script Automatizado post-instalación | ProxMenux Documentation",
|
||||||
"description": "El script Automatizado post-instalación de ProxMenux aplica un conjunto curado de 13 optimizaciones seguras y conscientes del hardware a un host de Proxmox VE recién instalado, sin preguntas. Cada cambio queda registrado para revertirlo después vía Uninstall Optimizations.",
|
"description": "El script Automatizado post-instalación de ProxMenux aplica un conjunto seleccionado de 14 optimizaciones seguras y adaptadas al hardware a un host Proxmox VE recién instalado, sin preguntas. Los cambios de configuración reversibles quedan registrados para restaurarlos después.",
|
||||||
"ogTitle": "Script Automatizado post-instalación | ProxMenux Documentation",
|
"ogTitle": "Script Automatizado post-instalación | ProxMenux Documentation",
|
||||||
"ogDescription": "13 optimizaciones curadas aplicadas a un host de Proxmox VE recién instalado sin preguntas. Consciente del hardware (autodetección SSD/NVMe) y totalmente reversible."
|
"ogDescription": "14 optimizaciones aplicadas a un host Proxmox VE recién instalado sin preguntas. Adaptadas al hardware y con los cambios reversibles registrados."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Script Automatizado post-instalación",
|
"title": "Script Automatizado post-instalación",
|
||||||
"description": "Un clic, sin preguntas — ProxMenux aplica un conjunto curado de 13 optimizaciones seguras de las que se beneficia casi cualquier host Proxmox. Cada cambio queda registrado en el JSON de herramientas para que puedas deshacer cualquiera de ellos más tarde desde Uninstall Optimizations.",
|
"description": "Un clic, sin preguntas: ProxMenux aplica 14 optimizaciones seguras de las que se beneficia casi cualquier host Proxmox. Los cambios de configuración reversibles quedan registrados para Uninstall Optimizations; las actualizaciones de paquetes no se describen como reversibles.",
|
||||||
"section": "Post-Install · Automated"
|
"section": "Post-Install · Automated"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Tuning de memoria",
|
"tool": "Tuning de memoria",
|
||||||
"what": "Establece vm.swappiness=10, dirty ratios balanceados, vm.overcommit_memory=1, vm.max_map_count=262144 y compaction proactiveness cuando es soportado.",
|
"what": "Establece vm.swappiness=10, dirty ratios balanceados, vm.max_map_count=262144 y compaction proactiveness cuando es soportado. La política de memory-overcommit del kernel se deja en el valor por defecto de Proxmox.",
|
||||||
"category": "System",
|
"category": "System",
|
||||||
"categorySlug": "system"
|
"categorySlug": "system"
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Nombres de interfaz persistentes",
|
"tool": "Nombres de interfaz persistentes",
|
||||||
"what": "Escribe un /etc/systemd/network/10-proxmenux-<iface>.link por NIC física (cada uno comenzando con la cabecera 'Managed by ProxMenux') que fija el MAC al nombre actual, de forma que los nombres eth0 / enp… se mantengan estables tras reinicios y al añadir nuevas NICs.",
|
"what": "Escribe un <code>/etc/systemd/network/10-proxmenux-<iface>.link</code> por NIC física (cada uno comenzando con la cabecera 'Managed by ProxMenux') que fija el MAC al nombre actual, de forma que los nombres <code>eth0</code> / <code>enp…</code> se mantengan estables tras reinicios y al añadir nuevas NICs.",
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"categorySlug": "network"
|
"categorySlug": "network"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Qué cubre esta categoría",
|
"title": "Qué cubre esta categoría",
|
||||||
"body": "Cuatro opciones fundamentales que normalmente quieres en cualquier host Proxmox recién instalado: cambiar a los repositorios comunitarios sin suscripción y ejecutar un upgrade completo del sistema, autoconfigurar la zona horaria y la sincronización NTP, eliminar las descargas de idiomas de APT para ahorrar ancho de banda y disco, y elegir de una lista de 25 utilidades de sistema comunes."
|
"body": "Cinco opciones fundamentales que normalmente quieres en cualquier host Proxmox recién instalado: cambiar a los repositorios comunitarios sin suscripción, ejecutar un upgrade completo del sistema, autoconfigurar la zona horaria y la sincronización NTP, eliminar las descargas de idiomas de APT para ahorrar ancho de banda y disco, y elegir de una lista de 25 utilidades de sistema comunes."
|
||||||
},
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"heading": "Actualizar y hacer upgrade del sistema",
|
"heading": "Actualizar y hacer upgrade del sistema",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"shortTitle": "En resumen",
|
"shortTitle": "En resumen",
|
||||||
"shortBody": "La opción ejecuta el exacto <code>apt update && apt full-upgrade -y</code> que Proxmox recomienda, lo envuelve con la limpieza de repos y la limpieza post-upgrade que la guía oficial también te dice que hagas, y pregunta por el reinicio al final. Mira <link>Proxmox System Update</link> — el mismo updater también está disponible como utilidad independiente en el menú principal, con el diagrama completo del proceso.",
|
"shortBody": "La opción ejecuta el exacto <code>apt update && apt full-upgrade -y</code> que Proxmox recomienda, lo envuelve con la limpieza de repos y la limpieza post-upgrade que la guía oficial también te dice que hagas, y pregunta por el reinicio al final. Mira <link>Proxmox System Update</link> — el mismo updater también está disponible como utilidad independiente en el menú principal, con el diagrama completo del proceso.",
|
||||||
"subTitle": "No apliques esto a un host con suscripción",
|
"subTitle": "No apliques esto a un host con suscripción",
|
||||||
"subBody": "Si realmente tienes una suscripción de Proxmox y quieres seguir usando los repositorios enterprise, sáltate esta opción. Volver a ejecutarla desactivaría el repo enterprise y te llevaría al canal comunitario. Puedes restaurar los repos enterprise desde el menú Uninstall si cambias de opinión más tarde.",
|
"subBody": "Si tienes una suscripción de Proxmox y quieres seguir usando los repositorios enterprise, omite esta opción. Al ejecutarla se desactiva el repositorio enterprise y el host pasa al canal comunitario. La actualización de paquetes y la reescritura de repositorios no se presentan como reversibles en Uninstall Optimizations; restaura deliberadamente la configuración de repositorios si más adelante necesitas cambiar de canal.",
|
||||||
"safetyTitle": "Comprobación de seguridad post-update",
|
"safetyTitle": "Comprobación de seguridad post-update",
|
||||||
"safetyBody": "Tras el upgrade, el script comprueba si hay discos con metadatos PV (Physical Volume) obsoletos — un caso límite que puede pasar cuando una VM con passthrough de disco garabatea cabeceras LVM sobre un disco en bruto. Si encuentra algo sospechoso verás un aviso sugiriendo <code>pvs</code> para inspeccionarlo. No se toma ninguna acción automáticamente."
|
"safetyBody": "Tras el upgrade, el script comprueba si hay discos con metadatos PV (Physical Volume) obsoletos — un caso límite que puede pasar cuando una VM con passthrough de disco garabatea cabeceras LVM sobre un disco en bruto. Si encuentra algo sospechoso verás un aviso sugiriendo <code>pvs</code> para inspeccionarlo. No se toma ninguna acción automáticamente."
|
||||||
},
|
},
|
||||||
@@ -209,8 +209,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actionTitle": "Algunas de ellas en acción",
|
"actionTitle": "Algunas de ellas en acción",
|
||||||
"noBulkTitle": "No hay uninstall masivo para utilidades",
|
"noBulkTitle": "Solo se eliminan las utilidades instaladas por ProxMenux",
|
||||||
"noBulkBody": "El menú Uninstall Optimizations <strong>no</strong> registra qué utilidades has instalado — solo si se aplicaron las opciones \"apt languages\", \"time sync\" y \"apt upgrade\". Para eliminar una utilidad concreta más tarde, desinstálala a mano:"
|
"noBulkBody": "ProxMenux registra únicamente los paquetes seleccionados que no estaban instalados antes de esta acción. Uninstall Optimizations puede purgar después esos paquetes, mientras que las utilidades que ya existían en el host permanecen intactas. La actualización general mediante APT no se registra como reversible porque los paquetes actualizados no disponen de una restauración atómica segura."
|
||||||
},
|
},
|
||||||
"related": {
|
"related": {
|
||||||
"heading": "Relacionado",
|
"heading": "Relacionado",
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Script Personalizable post-instalación | ProxMenux Documentation",
|
"title": "Script Personalizable post-instalación | ProxMenux Documentation",
|
||||||
"description": "Elige exactamente qué optimizaciones aplicar a un host Proxmox VE con ProxMenux. 10 categorías, ~30 herramientas individuales, UI de checklist. Incluye todo lo que hace el script Automatizado, más funcionalidades opt-in (IOMMU, Fastfetch, Figurine, Ceph, HA, fixes de AMD…).",
|
"description": "Elige exactamente qué optimizaciones aplicar a un host Proxmox VE con ProxMenux. 10 categorías, ~35 herramientas individuales, UI de selección. Incluye todo lo que hace el script Automatizado, más funcionalidades opcionales (IOMMU, Fastfetch, Figurine, Ceph, HA, ajustes de AMD…).",
|
||||||
"ogTitle": "Script Personalizable post-instalación | ProxMenux Documentation",
|
"ogTitle": "Script Personalizable post-instalación | ProxMenux Documentation",
|
||||||
"ogDescription": "10 categorías, ~30 optimizaciones individuales. Elige exactamente qué quieres en un host Proxmox VE. Totalmente reversible."
|
"ogDescription": "10 categorías, ~35 optimizaciones individuales. Elige exactamente qué quieres en un host Proxmox VE. Los cambios reversibles quedan registrados."
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Script Personalizable post-instalación",
|
"title": "Script Personalizable post-instalación",
|
||||||
"description": "Elige exactamente qué optimizaciones aplicar a un host Proxmox VE. ProxMenux agrupa ~30 herramientas individuales en 10 categorías, cada una con su propio diálogo de checklist. Mismo motor que el Automatizado, pero con control total sobre qué se aplica.",
|
"description": "Elige exactamente qué optimizaciones aplicar a un host Proxmox VE. ProxMenux agrupa ~35 herramientas individuales en 10 categorías, cada una con su propio diálogo de selección. Mismo motor que el Automatizado, pero con control total sobre qué se aplica.",
|
||||||
"section": "Post-Install · Customizable"
|
"section": "Post-Install · Customizable"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Cuándo elegir Personalizable",
|
"title": "Cuándo elegir Personalizable",
|
||||||
"body": "Elige esta ruta cuando ya sabes qué tweaks quieres en el host — o cuáles definitivamente no quieres. El script presenta un checklist por categoría para que puedas preseleccionar, deseleccionar o mezclar optimizaciones. Cada item puede aplicarse otra vez más tarde (es idempotente) o revertirse desde <link>Uninstall Optimizations</link>."
|
"body": "Elige esta ruta cuando ya sabes qué ajustes quieres en el host — o cuáles definitivamente no quieres. El script presenta una selección por categoría para que puedas combinar optimizaciones. Las opciones se pueden volver a aplicar y los cambios de configuración reversibles quedan registrados para <link>Uninstall Optimizations</link>. Las actualizaciones de paquetes no se presentan como reversibles."
|
||||||
},
|
},
|
||||||
"compare": {
|
"compare": {
|
||||||
"heading": "Comparación con el Automatizado",
|
"heading": "Comparación con el Automatizado",
|
||||||
"body": "Personalizable es un superset del <link>script Automatizado</link>. Cubre las mismas 13 optimizaciones de baseline más una larga lista de opt-ins que el Automatizado se salta a propósito — cosas que solo son útiles en hardware específico (fixes de AMD), hosting específico (OVH RTM), o cargas específicas (IOMMU/VFIO, repo de Ceph, High Availability, Fastfetch, Figurine, tuning del ARC de ZFS, pigz, ZFS auto-snapshot, límites de velocidad de vzdump, Open vSwitch, TCP BBR…)."
|
"body": "Personalizable es un superset del <link>script Automatizado</link>. Cubre las mismas 14 optimizaciones de baseline más una larga lista de opt-ins que el Automatizado se salta a propósito — cosas que solo son útiles en hardware específico (fixes de AMD), hosting específico (OVH RTM), o cargas específicas (IOMMU/VFIO, repo de Ceph, High Availability, Fastfetch, Figurine, tuning del ARC de ZFS, pigz, ZFS auto-snapshot, límites de velocidad de vzdump, Open vSwitch, TCP BBR + TCP Fast Open, refresco del índice del PVE Appliance Manager…)."
|
||||||
},
|
},
|
||||||
"categoriesSection": {
|
"categoriesSection": {
|
||||||
"heading": "Las 10 categorías",
|
"heading": "Las 10 categorías",
|
||||||
"body": "El script Personalizable agrupa optimizaciones en 10 categorías. Cada categoría tiene su propio diálogo de checklist y su propia página de documentación — abre una de las tarjetas de abajo para ver el razonamiento, los valores por defecto y los pasos de verificación por opción."
|
"body": "El script Personalizable agrupa optimizaciones en 10 categorías. Cada categoría tiene su propio diálogo de selección y su propia página de documentación — abre una de las tarjetas para consultar el razonamiento, los valores predeterminados y los pasos de verificación."
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Network",
|
"name": "Network",
|
||||||
"description": "Endurece y afina la pila de red del host. Fuerza APT sobre IPv4, aplica sysctl con hardening y tuning de buffers TCP, ofrece Open vSwitch y BBR, y fija nombres persistentes de interfaces por MAC."
|
"description": "Endurece y afina la pila de red del host. Fuerza APT sobre IPv4, aplica sysctl con hardening y tuning de buffers TCP, ofrece Open vSwitch, TCP BBR + TCP Fast Open, y fija nombres persistentes de interfaces por MAC."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Storage",
|
"name": "Storage",
|
||||||
"description": "Configura los subsistemas de almacenamiento habituales de Proxmox: ARC de ZFS, auto-snapshots y límites de velocidad de vzdump para evitar saturar el disco durante backups."
|
"description": "Configura los subsistemas de almacenamiento habituales de Proxmox: ARC de ZFS, auto-snapshots, ZFS autotrim para pools SSD/NVMe y límites de velocidad de vzdump para evitar saturar el disco durante backups."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Security",
|
"name": "Security",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Optional",
|
"name": "Optional",
|
||||||
"description": "Piezas de nicho que no todo host necesita: fixes de CPU AMD, banner Fastfetch, hostname 3D con Figurine, repositorio de Ceph, servicios de Alta Disponibilidad y Log2RAM para reducir el desgaste del SSD."
|
"description": "Opciones que no necesita todo host: ajustes de CPU AMD, banner Fastfetch, nombre 3D con Figurine, actualización del índice de PVE Appliance Manager, repositorio Ceph, servicios de alta disponibilidad y Log2RAM para reducir el desgaste del SSD."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mixTip": {
|
"mixTip": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Post-instalación: Customización",
|
"title": "Post-instalación: Customización",
|
||||||
"description": "Tweaks cosméticos y de calidad de vida para el host Proxmox. Ninguno cambia el comportamiento funcional — solo hacen la shell más agradable de usar y ocultan el aviso de suscripción en la UI web. Los tres están registrados y son reversibles desde el menú Uninstall.",
|
"description": "Ajustes estéticos y de comodidad para el host Proxmox. Hacen más agradable el uso de la consola y ocultan el aviso de suscripción de la interfaz web. Bashrc, MOTD y el banner de suscripción quedan registrados y son reversibles desde el menú Uninstall.",
|
||||||
"section": "Settings post-install Proxmox"
|
"section": "Settings post-install Proxmox"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"heading": "Configurar banner MOTD personalizado",
|
"heading": "Configurar banner MOTD personalizado",
|
||||||
"intro": "Antepone <em>\"This system is optimised by: ProxMenux\"</em> a <code>/etc/motd</code>, el mensaje mostrado tras un login SSH exitoso (encima del prompt de la shell, antes de que se ejecute cualquier script de <code>update-motd</code>). Inofensivo y puramente informativo — útil como confirmación visual rápida de que ProxMenux se ha aplicado en este host.",
|
"intro": "Antepone <em>\"This system is optimised by: ProxMenux\"</em> a <code>/etc/motd</code>, el mensaje mostrado tras un login SSH exitoso (encima del prompt de la shell, antes de que se ejecute cualquier script de <code>update-motd</code>). Inofensivo y puramente informativo — útil como confirmación visual rápida de que ProxMenux se ha aplicado en este host.",
|
||||||
"writesTitle": "Qué escribe ProxMenux",
|
"writesTitle": "Qué escribe ProxMenux",
|
||||||
"writesOutro": "El <code>/etc/motd</code> original se respalda en <code>/etc/motd.bak</code> en la primera aplicación. La operación es idempotente: si la línea marcador ya está presente, no se añade nada."
|
"writesOutro": "En la primera aplicación, ProxMenux registra si <code>/etc/motd</code> existía y conserva su contenido original en <code>/usr/local/share/proxmenux</code>. La operación es idempotente: si la línea identificadora ya está presente, no se añade otra vez. Las instalaciones antiguas que tengan <code>/etc/motd.bak</code> se migran al mismo estado reversible."
|
||||||
},
|
},
|
||||||
"banner": {
|
"banner": {
|
||||||
"heading": "Eliminar banner de suscripción",
|
"heading": "Eliminar banner de suscripción",
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
"verify": {
|
"verify": {
|
||||||
"heading": "Verificación",
|
"heading": "Verificación",
|
||||||
"intro": "Tras aplicar los tres:",
|
"intro": "Tras aplicar los tres:",
|
||||||
"reversibleTitle": "Los tres son reversibles",
|
"reversibleTitle": "Los tres cambios de personalización quedan registrados",
|
||||||
"reversibleBody": "<link>Uninstall Optimizations</link> restaura <code>/root/.bashrc</code> y <code>/etc/motd</code> desde sus backups <code>.bak</code>, y o bien restaura los archivos parcheados de la UI desde el directorio de backups o reinstala <code>pve-manager</code>, <code>proxmox-widget-toolkit</code>, <code>libjs-extjs</code> y <code>libpve-http-server-perl</code> con <code>--force-confnew</code> para devolver la UI web al estado vanilla."
|
"reversibleBody": "<link>Uninstall Optimizations</link> restaura <code>/root/.bashrc</code>, devuelve MOTD exactamente al contenido anterior a ProxMenux (o elimina el archivo si antes no existía) y restaura los archivos parcheados de la interfaz desde sus copias o reinstala los paquetes de Proxmox afectados cuando sea necesario."
|
||||||
},
|
},
|
||||||
"related": {
|
"related": {
|
||||||
"heading": "Relacionado",
|
"heading": "Relacionado",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "Script post-instalación de Proxmox VE — Automatizado y personalizable | ProxMenux",
|
"title": "Script post-instalación de Proxmox VE — Automatizado y personalizable | ProxMenux",
|
||||||
"description": "Resumen de los scripts post-instalación de ProxMenux para Proxmox VE. Ejecuta el script Automatizado para valores por defecto sensatos sin preguntas, el script Personalizable para elegir exactamente qué quieres entre 10 categorías (sistema, virtualización, red, almacenamiento, seguridad, rendimiento, opcional), o revierte cualquier cambio por completo con la opción Uninstall Optimizations.",
|
"description": "Resumen de los scripts post-instalación de ProxMenux para Proxmox VE. Ejecuta el script Automatizado para aplicar valores recomendados sin preguntas, usa Personalizable para elegir entre 10 categorías o restaura los cambios reversibles compatibles mediante Uninstall Optimizations.",
|
||||||
"ogTitle": "Script post-instalación de Proxmox VE — Automatizado y personalizable",
|
"ogTitle": "Script post-instalación de Proxmox VE — Automatizado y personalizable",
|
||||||
"ogDescription": "Aplica optimizaciones comunes post-instalación de Proxmox VE en 10 categorías — automatizadas o a la carta, con opciones reversibles.",
|
"ogDescription": "Aplica optimizaciones comunes post-instalación de Proxmox VE en 10 categorías — automatizadas o a la carta, con opciones reversibles.",
|
||||||
"twitterTitle": "Script post-instalación de Proxmox VE | ProxMenux",
|
"twitterTitle": "Script post-instalación de Proxmox VE | ProxMenux",
|
||||||
@@ -9,26 +9,26 @@
|
|||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Scripts post-instalación",
|
"title": "Scripts post-instalación",
|
||||||
"description": "Configura un host de Proxmox VE recién instalado con las optimizaciones post-instalación de ProxMenux. Tres rutas: ejecutar todo automáticamente, elegir lo que quieres, o revertir cualquier cambio. Todos los cambios quedan registrados.",
|
"description": "Configura un host de Proxmox VE recién instalado con las optimizaciones post-instalación de ProxMenux. Aplica la configuración base automáticamente, elige opciones individuales, actualiza funciones instaladas o restaura cambios reversibles compatibles. Las actualizaciones de paquetes no se presentan como reversibles.",
|
||||||
"section": "Settings post-install Proxmox"
|
"section": "Settings post-install Proxmox"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"title": "Para qué sirve este menú",
|
"title": "Para qué sirve este menú",
|
||||||
"body": "Justo después de instalar Proxmox VE, hay decenas de pequeños cambios que hacen el host más rápido y más fácil de mantener — repositorios sin suscripción, límites sensatos para journald, buffers TCP razonables, almacenamiento de logs amigable con SSD, mejoras en bashrc y más. ProxMenux los automatiza todos, registra lo que cambió y te permite revertirlo."
|
"body": "Justo después de instalar Proxmox VE, hay decenas de pequeños cambios que hacen el host más rápido y más fácil de mantener — repositorios sin suscripción, límites sensatos para journald, buffers TCP razonables, almacenamiento de logs compatible con SSD, mejoras en bashrc y más. ProxMenux los automatiza y registra los cambios de configuración reversibles compatibles."
|
||||||
},
|
},
|
||||||
"openingMenu": {
|
"openingMenu": {
|
||||||
"heading": "Abrir el menú",
|
"heading": "Abrir el menú",
|
||||||
"body": "Desde el menú principal de ProxMenux, selecciona <strong>Settings post-install Proxmox</strong>. Verás esto:",
|
"body": "Desde el menú principal de ProxMenux, selecciona <strong>Settings post-install Proxmox</strong>. Verás esto:",
|
||||||
"imageAlt": "Menú de scripts post-instalación con 3 opciones de ProxMenux (Automatizado / Personalizable / Uninstall) seguidas de la sección Community Scripts"
|
"imageAlt": "Menú de scripts post-instalación — Automatizado, Personalizable, la entrada condicional Aplicar Actualizaciones Disponibles (solo cuando hay updates pendientes) y Uninstall, seguidos de la sección Community Scripts"
|
||||||
},
|
},
|
||||||
"threeWays": {
|
"threeWays": {
|
||||||
"heading": "Tres formas de aplicar optimizaciones",
|
"heading": "Cuatro formas de aplicar optimizaciones",
|
||||||
"body": "Las tres entradas de ProxMenux comparten el mismo código subyacente y el mismo registro de herramientas instaladas — solo te dan distintos niveles de control. Elige la que se ajusta a cuánto quieres decidir."
|
"body": "Las cuatro entradas de ProxMenux comparten el mismo código subyacente y el mismo registro de herramientas instaladas — solo te dan distintos niveles de control. La entrada <em>Aplicar Actualizaciones Disponibles</em> solo se muestra cuando al menos una optimización instalada tiene una versión más nueva en disco que la registrada; en un host recién configurado permanece oculta."
|
||||||
},
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"title": "Automatizado",
|
"title": "Automatizado",
|
||||||
"description": "Un conjunto curado de 13 optimizaciones seguras y siempre útiles aplicadas en secuencia sin preguntas. Buen valor por defecto para la mayoría de usuarios.",
|
"description": "Un conjunto curado de 14 optimizaciones seguras y siempre útiles aplicadas en secuencia sin preguntas. Buen valor por defecto para la mayoría de usuarios.",
|
||||||
"bullets": [
|
"bullets": [
|
||||||
"Repos sin suscripción + upgrade del sistema",
|
"Repos sin suscripción + upgrade del sistema",
|
||||||
"Tuning de memoria, kernel y red",
|
"Tuning de memoria, kernel y red",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Personalizable",
|
"title": "Personalizable",
|
||||||
"description": "~30 optimizaciones individuales en 10 categorías. Eliges exactamente cuáles aplicar. Mismo motor que el Automatizado, pero con control total.",
|
"description": "~35 optimizaciones individuales en 10 categorías. Eliges exactamente cuáles aplicar. Mismo motor que el Automatizado, pero con control total.",
|
||||||
"bullets": [
|
"bullets": [
|
||||||
"UI de checklist por categoría",
|
"UI de checklist por categoría",
|
||||||
"Incluye todo lo que hace el Automatizado, más opciones opt-in (IOMMU, Fastfetch, Figurine, Ceph, HA, fixes de AMD…)",
|
"Incluye todo lo que hace el Automatizado, más opciones opt-in (IOMMU, Fastfetch, Figurine, Ceph, HA, fixes de AMD…)",
|
||||||
@@ -57,10 +57,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Uninstall Optimizations",
|
"title": "Uninstall Optimizations",
|
||||||
"description": "Cada cambio hecho por cualquiera de las rutas queda registrado en un JSON, y cada optimización tiene una función inversa. Elige qué revertir y el host vuelve atrás.",
|
"description": "Los cambios reversibles compatibles quedan registrados en un JSON y asociados a una función de restauración. Las acciones sin reversión segura, como una actualización completa de paquetes, se excluyen deliberadamente.",
|
||||||
"bullets": [
|
"bullets": [
|
||||||
"Detecta automáticamente las optimizaciones aplicadas previamente",
|
"Detecta automáticamente las optimizaciones aplicadas previamente",
|
||||||
"La reversión restaura las configuraciones originales desde archivos de backup",
|
"La reversión elige el camino adecuado para cada elemento — restaura desde un backup .bak donde se hizo uno, elimina el snippet en sysctl.d donde no había nada que respaldar, o reinstala el paquete vanilla con --force-confnew (p. ej. banner de suscripción)",
|
||||||
"Pregunta antes de reiniciar si hace falta (VFIO, nombres persistentes, etc.)"
|
"Pregunta antes de reiniciar si hace falta (VFIO, nombres persistentes, etc.)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,11 @@
|
|||||||
"remoteTitle": "Script remoto pasado por tubería a bash",
|
"remoteTitle": "Script remoto pasado por tubería a bash",
|
||||||
"remoteBody": "La instalación ejecuta <code>wget -qO - https://…apply.sh | bash</code>. Si el mirror de OVH se ve comprometido alguna vez, el script se ejecuta como root en tu host. Antes de activar esta opción, decide si confías más en la cadena de mirrors de OVH que en la monitorización que ganas. Para la mayoría de usuarios de home-lab o no-OVH esta opción debería quedarse simplemente apagada.",
|
"remoteBody": "La instalación ejecuta <code>wget -qO - https://…apply.sh | bash</code>. Si el mirror de OVH se ve comprometido alguna vez, el script se ejecuta como root en tu host. Antes de activar esta opción, decide si confías más en la cadena de mirrors de OVH que en la monitorización que ganas. Para la mayoría de usuarios de home-lab o no-OVH esta opción debería quedarse simplemente apagada.",
|
||||||
"noOpTitle": "Actívalo solo si el host está realmente en OVH",
|
"noOpTitle": "Actívalo solo si el host está realmente en OVH",
|
||||||
"noOpBody": "La opción es un no-op en servidores no-OVH, así que marcarla en un Proxmox de home-lab no rompe nada. Pero hoy hay un bug cosmético: incluso en servidores no-OVH el script imprime <em>\"Server belongs to OVH\"</em> al final, lo que puede inducir a error. Mira la nota de solución de problemas más abajo.",
|
"noOpBody": "La opción es un no-op en servidores no-OVH, así que marcarla en un Proxmox de home-lab no rompe nada. En un host no-OVH el script imprime <em>\"Not an OVH server, skipping RTM installation\"</em> y termina limpiamente; no se instala ningún paquete.",
|
||||||
"runsTitle": "Qué ejecuta ProxMenux",
|
"runsTitle": "Qué ejecuta ProxMenux",
|
||||||
"verifyTitle": "Verificación",
|
"verifyTitle": "Verificación",
|
||||||
"verifyBody": "En un host OVH real, tras un reinicio deberías ver el <a>panel de RTM</a> en tu OVH Manager con datos en vivo del host. En el lado de Proxmox, el colector RTM es un servicio systemd — compruébalo directamente:",
|
"verifyBody": "En un host OVH real, tras un reinicio deberías ver el <a>panel de RTM</a> en tu OVH Manager con datos en vivo del host. En el lado de Proxmox, el colector RTM es un servicio systemd — compruébalo directamente:",
|
||||||
"troubleTitle": "Solución de problemas",
|
"troubleTitle": "Solución de problemas",
|
||||||
"spuriousTitle": "\"Server belongs to OVH\" pero no estoy en OVH",
|
|
||||||
"spuriousBody": "Es una peculiaridad cosmética conocida del script actual: el mensaje de éxito se dispara fuera del condicional de detección de OVH, así que se imprime en cada ejecución. Si la instalación de RTM <em>no</em> ocurrió realmente (comprueba <code>systemctl status ovh-rtm</code> — no existirá), el mensaje es espurio y se puede ignorar. No se instaló nada en tu host.",
|
|
||||||
"revertTitle": "No reversible desde el menú Uninstall",
|
"revertTitle": "No reversible desde el menú Uninstall",
|
||||||
"revertBody": "No hay una entrada de uninstall dedicada para RTM. En un host OVH real, elimina los paquetes a mano con <code>apt purge ovh-*</code> y borra cualquier manifest puppet bajo <code>/etc/puppet/</code> que RTM haya instalado. En un host no-OVH no se instaló nada, así que no hay nada que revertir."
|
"revertBody": "No hay una entrada de uninstall dedicada para RTM. En un host OVH real, elimina los paquetes a mano con <code>apt purge ovh-*</code> y borra cualquier manifest puppet bajo <code>/etc/puppet/</code> que RTM haya instalado. En un host no-OVH no se instaló nada, así que no hay nada que revertir."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"area": "Seguridad de routing",
|
"area": "Seguridad de routing",
|
||||||
"settings": "<code>accept_redirects=0</code>, <code>accept_source_route=0</code>, <code>secure_redirects=0</code>, <code>send_redirects=0</code>"
|
"settings": "<code>accept_redirects=0</code>, <code>accept_source_route=0</code>, <code>secure_redirects=0</code>, <code>send_redirects=0</code>, <code>log_martians=0</code>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"area": "Reverse path filter",
|
"area": "Reverse path filter",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
],
|
],
|
||||||
"sourceOutro": "También añade <code>source /etc/network/interfaces.d/*</code> a <code>/etc/network/interfaces</code> si no está ya presente — práctica estándar para que puedas dejar snippets modulares de interfaz sin editar el archivo principal.",
|
"sourceOutro": "También añade <code>source /etc/network/interfaces.d/*</code> a <code>/etc/network/interfaces</code> si no está ya presente — práctica estándar para que puedas dejar snippets modulares de interfaz sin editar el archivo principal.",
|
||||||
"fwbrTitle": "Ajuste automático de los bridges de firewall virtual",
|
"fwbrTitle": "Ajuste automático de los bridges de firewall virtual",
|
||||||
"fwbrBody": "Junto al perfil sysctl, ProxMenux instala un helper en <code>/usr/local/sbin/proxmenux-fwbr-tune</code> que aplica <code>rp_filter=0</code> y <code>log_martians=0</code> a las interfaces <code>fwbr*</code> / <code>fwln*</code> / <code>fwpr*</code> / <code>tap*</code> que Proxmox crea alrededor de VMs y contenedores. El helper lo lanza la unit oneshot <code>proxmenux-fwbr-tune.service</code> al arranque y la regla <code>/etc/udev/rules.d/99-proxmenux-fwbr-tune.rules</code> en cada evento <code>net add</code> que coincida con esos prefijos — cubriendo las interfaces que Proxmox recrea al iniciar/parar VMs, en reinicios y en migraciones en vivo.",
|
"fwbrBody": "Junto al perfil sysctl, ProxMenux instala un helper en <code>/usr/local/sbin/proxmenux-fwbr-tune</code> que aplica <code>rp_filter=0</code> y <code>log_martians=0</code> a las interfaces <code>fwbr*</code> / <code>fwln*</code> / <code>fwpr*</code> / <code>tap*</code> que Proxmox crea alrededor de VMs y contenedores. El helper lo lanza la unit oneshot <code>proxmenux-fwbr-tune.service</code> al arranque y la regla <code>/etc/udev/rules.d/99-zz-proxmenux-fwbr-tune.rules</code> en cada evento <code>net add</code> que coincida con esos prefijos — cubriendo las interfaces que Proxmox recrea al iniciar/parar VMs, en reinicios y en migraciones en vivo. Además el helper se ejecuta inmediatamente al terminar la instalación para barrer las interfaces ya presentes.",
|
||||||
"rpFilterTitle": "Por qué rp_filter=2 (loose) en lugar de 1 (strict)",
|
"rpFilterTitle": "Por qué rp_filter=2 (loose) en lugar de 1 (strict)",
|
||||||
"rpFilterBody": "El reverse-path filtering strict descarta paquetes cuya fuente se rutearía por una interfaz <em>distinta</em>. Es el valor por defecto correcto en una máquina cliente, pero rompe gravemente en un host Proxmox donde el tráfico de VMs a menudo llega por un bridge y sale por un uplink con rutas asimétricas. <code>rp_filter=2</code> (loose) solo descarta paquetes con fuentes verdaderamente no enrutables. Es un trade-off pragmático — ligera reducción en la detección de spoofing de IP local a cambio de no romper tu red de VMs."
|
"rpFilterBody": "El reverse-path filtering strict descarta paquetes cuya fuente se rutearía por una interfaz <em>distinta</em>. Es el valor por defecto correcto en una máquina cliente, pero rompe gravemente en un host Proxmox donde el tráfico de VMs a menudo llega por un bridge y sale por un uplink con rutas asimétricas. <code>rp_filter=2</code> (loose) solo descarta paquetes con fuentes verdaderamente no enrutables. Es un trade-off pragmático — ligera reducción en la detección de spoofing de IP local a cambio de no romper tu red de VMs."
|
||||||
},
|
},
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
"intro": "Instala <code>openvswitch-switch</code> + <code>openvswitch-common</code>. Estos paquetes añaden OVS como implementación alternativa de bridges a los bridges Linux estándar que Proxmox usa por defecto. La instalación por sí sola no cambia ninguna configuración de red — los bridges <code>vmbrX</code> existentes siguen funcionando. OVS pasa a estar disponible en la UI de Proxmox cuando <em>creas</em> un bridge nuevo y lo eliges del desplegable de tipo.",
|
"intro": "Instala <code>openvswitch-switch</code> + <code>openvswitch-common</code>. Estos paquetes añaden OVS como implementación alternativa de bridges a los bridges Linux estándar que Proxmox usa por defecto. La instalación por sí sola no cambia ninguna configuración de red — los bridges <code>vmbrX</code> existentes siguen funcionando. OVS pasa a estar disponible en la UI de Proxmox cuando <em>creas</em> un bridge nuevo y lo eliges del desplegable de tipo.",
|
||||||
"tipTitle": "Cuándo tiene sentido OVS",
|
"tipTitle": "Cuándo tiene sentido OVS",
|
||||||
"tipBody": "Considera OVS si necesitas <strong>trunking VLAN con IDs de VLAN no contiguos</strong>, <strong>LACP con LLDP en modos específicos</strong>, <strong>programación de flujos granular</strong> (OpenFlow), o interoperación con controladores SDN. Para un home lab con un par de VLANs y un único uplink LACP, los bridges Linux estándar + <code>vmbrX.VID</code> son más simples y van perfectamente bien.",
|
"tipBody": "Considera OVS si necesitas <strong>trunking VLAN con IDs de VLAN no contiguos</strong>, <strong>LACP con LLDP en modos específicos</strong>, <strong>programación de flujos granular</strong> (OpenFlow), o interoperación con controladores SDN. Para un home lab con un par de VLANs y un único uplink LACP, los bridges Linux estándar + <code>vmbrX.VID</code> son más simples y van perfectamente bien.",
|
||||||
"revertTitle": "No reversible desde el menú Uninstall",
|
"revertTitle": "Reversible desde el menú Uninstall",
|
||||||
"revertBody": "La instalación de OVS no se registra en Uninstall Optimizations. Si decides que no lo quieres, elimínalo a mano — pero solo después de migrar cualquier bridge de vuelta a bridges Linux:"
|
"revertBody": "OVS está registrado. <link>Uninstall Optimizations</link> ejecuta <code>apt purge</code> sobre <code>openvswitch-switch</code> y <code>openvswitch-common</code>. Migra cualquier bridge OVS de vuelta a bridges Linux <em>antes</em> de desinstalar, si no las VMs sobre esos bridges pierden red en el próximo arranque. Equivalente manual:"
|
||||||
},
|
},
|
||||||
"bbr": {
|
"bbr": {
|
||||||
"heading": "Activar TCP BBR + TCP Fast Open",
|
"heading": "Activar TCP BBR + TCP Fast Open",
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
"verifyTitle": "Verificación",
|
"verifyTitle": "Verificación",
|
||||||
"impactTitle": "El impacto depende de la carga",
|
"impactTitle": "El impacto depende de la carga",
|
||||||
"impactBody": "BBR brilla en enlaces de alta latencia o con pérdidas (replicación intercontinental, túneles VPN, clientes móviles). En una LAN entre dos máquinas en el mismo switch, la diferencia a menudo está dentro del ruido. TFO ayuda más a conexiones HTTP cortas y repetidas.",
|
"impactBody": "BBR brilla en enlaces de alta latencia o con pérdidas (replicación intercontinental, túneles VPN, clientes móviles). En una LAN entre dos máquinas en el mismo switch, la diferencia a menudo está dentro del ruido. TFO ayuda más a conexiones HTTP cortas y repetidas.",
|
||||||
"revertTitle": "No reversible desde el menú Uninstall",
|
"revertTitle": "Reversible desde el menú Uninstall",
|
||||||
"revertBody": "BBR/TFO no se registran. Para revertir, quita los dos archivos sysctl y recarga:"
|
"revertBody": "BBR/TFO están registrados. <link>Uninstall Optimizations</link> elimina los dos ficheros sysctl (<code>/etc/sysctl.d/99-tcp-bbr.conf</code> y <code>99-tcp-fastopen.conf</code>) y recarga sysctl para que el kernel vuelva a <code>cubic</code> y a <code>tcp_fastopen=1</code>. Equivalente manual:"
|
||||||
},
|
},
|
||||||
"names": {
|
"names": {
|
||||||
"heading": "Nombres de interfaz (persistentes)",
|
"heading": "Nombres de interfaz (persistentes)",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"title": "Optional Settings",
|
"title": "Optional Settings",
|
||||||
"intro": "La categoría <strong>Optional Settings</strong> ofrece funcionalidades y optimizaciones adicionales que puedes elegir aplicar a tu instalación de Proxmox VE. Estos ajustes no son esenciales pero pueden mejorar las capacidades de tu sistema en escenarios específicos.",
|
"intro": "La categoría <strong>Optional Settings</strong> ofrece funcionalidades y optimizaciones adicionales que puedes elegir aplicar a tu instalación de Proxmox VE. Estos ajustes no son esenciales pero pueden mejorar las capacidades de tu sistema en escenarios específicos.",
|
||||||
"available": "Funcionalidades opcionales disponibles",
|
"available": "Funcionalidades opcionales disponibles",
|
||||||
|
"stepLabel": "Paso",
|
||||||
"ceph": {
|
"ceph": {
|
||||||
"title": "Añadir soporte Ceph más reciente",
|
"title": "Añadir soporte Ceph más reciente",
|
||||||
"intro": "Esta opción instala el soporte más reciente del sistema de almacenamiento Ceph para Proxmox VE. Ceph es un sistema de almacenamiento distribuido que ofrece alto rendimiento, fiabilidad y escalabilidad.",
|
"intro": "Esta opción instala el soporte más reciente del sistema de almacenamiento Ceph para Proxmox VE. Ceph es un sistema de almacenamiento distribuido que ofrece alto rendimiento, fiabilidad y escalabilidad.",
|
||||||
@@ -28,9 +29,8 @@
|
|||||||
"doesIntro": "Qué hace:",
|
"doesIntro": "Qué hace:",
|
||||||
"doesItems": [
|
"doesItems": [
|
||||||
"Detecta si hay presente una CPU AMD EPYC o Ryzen",
|
"Detecta si hay presente una CPU AMD EPYC o Ryzen",
|
||||||
"Aplica el parámetro de kernel 'idle=nomwait' para prevenir crashes aleatorios",
|
"Aplica el parámetro de kernel 'idle=nomwait' para prevenir crashes aleatorios (vía /etc/kernel/cmdline en hosts con systemd-boot, o /etc/default/grub en hosts con GRUB — con un .bak del original)",
|
||||||
"Configura KVM para que ignore ciertos MSRs (Model Specific Registers) y mejorar la compatibilidad con guests Windows",
|
"Configura KVM para que ignore ciertos MSRs (Model Specific Registers) y mejorar la compatibilidad con guests Windows"
|
||||||
"Instala el último kernel de Proxmox VE"
|
|
||||||
],
|
],
|
||||||
"howUse": "Cómo usarlo: Estos fixes se aplican automáticamente y requieren un reinicio del sistema para surtir efecto.",
|
"howUse": "Cómo usarlo: Estos fixes se aplican automáticamente y requieren un reinicio del sistema para surtir efecto.",
|
||||||
"automates": "Este ajuste automatiza los siguientes comandos:"
|
"automates": "Este ajuste automatiza los siguientes comandos:"
|
||||||
@@ -47,21 +47,16 @@
|
|||||||
"howUse": "Cómo usarlo: Tras activar estos servicios, puedes configurar grupos y recursos HA en la interfaz web de Proxmox VE.",
|
"howUse": "Cómo usarlo: Tras activar estos servicios, puedes configurar grupos y recursos HA en la interfaz web de Proxmox VE.",
|
||||||
"automates": "Este ajuste automatiza los siguientes comandos:"
|
"automates": "Este ajuste automatiza los siguientes comandos:"
|
||||||
},
|
},
|
||||||
"testing": {
|
"pveam": {
|
||||||
"title": "Activar el repositorio testing de Proxmox",
|
"title": "Actualizar el Proxmox VE Appliance Manager",
|
||||||
"intro": "Esta opción activa el repositorio testing de Proxmox, dando acceso a las versiones más recientes y potencialmente inestables de los paquetes de Proxmox VE.",
|
"intro": "Refresca el índice local de plantillas de contenedores que <code>pveam</code> expone en la UI de Proxmox, para que la lista de appliances disponibles esté al día la próxima vez que crees un LXC.",
|
||||||
"doesIntro": "Qué hace:",
|
"doesIntro": "Qué hace:",
|
||||||
"doesItems": [
|
"doesItems": [
|
||||||
"Añade el repositorio testing de Proxmox a las fuentes de paquetes del sistema",
|
"Ejecuta <code>pveam update</code> contra los mirrors de Proxmox para bajar el catálogo actual de appliances",
|
||||||
"Crea un archivo nuevo en /etc/apt/sources.list.d/ para el repositorio testing",
|
"Puebla la lista de appliances que muestra la web UI al crear un contenedor"
|
||||||
"Actualiza las listas de paquetes para incluir paquetes del nuevo repositorio"
|
|
||||||
],
|
],
|
||||||
"howUse": "Cómo usarlo: Tras activar este repositorio, puedes actualizar y hacer upgrade de tu sistema para obtener las últimas versiones testing de los paquetes de Proxmox VE. Úsalo con precaución ya que estas versiones pueden ser inestables.",
|
"howUse": "Cómo usarlo: ejecútalo cuando la lista de appliances en la UI se sienta desactualizada, o tras cambiar de mirror. No descarga las plantillas en sí — solo el índice del catálogo.",
|
||||||
"manualIntro": "Para añadir el repositorio testing de Proxmox manualmente, puedes usar estos comandos:",
|
"automates": "Este ajuste automatiza el siguiente comando:"
|
||||||
"noteLabel": "Nota:",
|
|
||||||
"noteBody": "$(lsb_release -cs) detecta automáticamente el codename de la versión de tu Proxmox VE (p. ej., bullseye).",
|
|
||||||
"warnLabel": "Advertencia:",
|
|
||||||
"warnBody": "Activar el repositorio testing puede provocar inestabilidad del sistema. Se recomienda solo para entornos de pruebas."
|
|
||||||
},
|
},
|
||||||
"fastfetch": {
|
"fastfetch": {
|
||||||
"title": "Instalar y configurar Fastfetch",
|
"title": "Instalar y configurar Fastfetch",
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
],
|
],
|
||||||
"replacesTitle": "Esto sustituye un binario del sistema",
|
"replacesTitle": "Esto sustituye un binario del sistema",
|
||||||
"replacesBody": "Sustituir <code>/bin/gzip</code> por un wrapper es inusual. Es seguro (el wrapper produce salida compatible con gzip), pero conviene saberlo: scripts que tengan paths hardcodeados, que se ejecuten dentro de chroots restrictivos o que verifiquen hashes de binarios pueden comportarse de forma distinta. El binario original se conserva como <code>/bin/gzip.original</code> para que siempre puedas dar marcha atrás.",
|
"replacesBody": "Sustituir <code>/bin/gzip</code> por un wrapper es inusual. Es seguro (el wrapper produce salida compatible con gzip), pero conviene saberlo: scripts que tengan paths hardcodeados, que se ejecuten dentro de chroots restrictivos o que verifiquen hashes de binarios pueden comportarse de forma distinta. El binario original se conserva como <code>/bin/gzip.original</code> para que siempre puedas dar marcha atrás.",
|
||||||
"revertTitle": "No reversible desde el menú Uninstall",
|
"revertTitle": "Reversible desde el menú Uninstall",
|
||||||
"revertBody": "Esta optimización se aplica desde Customizable, pero <strong>actualmente no tiene una entrada equivalente en el menú Uninstall Optimizations</strong>. Para revertirla a mano, restaura el gzip original y borra el wrapper:",
|
"revertBody": "Esta optimización está registrada. <link>Uninstall Optimizations</link> restaura <code>/bin/gzip.original</code> en su sitio, elimina el <code>pigzwrapper</code>, revierte las dos líneas añadidas a <code>/etc/vzdump.conf</code> y ejecuta <code>apt purge pigz</code>. Equivalente manual:",
|
||||||
"verifyTitle": "Verificación",
|
"verifyTitle": "Verificación",
|
||||||
"verifyBody": "Tras aplicar, <code>gzip --version</code> debería mencionar pigz. Un benchmark rápido también muestra la diferencia de velocidad en un host multinúcleo:",
|
"verifyBody": "Tras aplicar, <code>gzip --version</code> debería mencionar pigz. Un benchmark rápido también muestra la diferencia de velocidad en un host multinúcleo:",
|
||||||
"whenTitle": "Cuándo importa más",
|
"whenTitle": "Cuándo importa más",
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
"nfsTitle": "No desactives esto si usas NFS",
|
"nfsTitle": "No desactives esto si usas NFS",
|
||||||
"nfsBody": "El servidor NFS <strong>y</strong> el cliente NFS dependen de <code>rpcbind</code> para negociar los puertos que usan <code>mountd</code>, <code>statd</code>, <code>lockd</code>, etc. Si tu host Proxmox <em>exporta</em> shares NFS a otras máquinas o <em>monta</em> shares NFS desde un NAS, no apliques esta opción. Los montajes fallarán con <code>mount.nfs: rpc.statd is not running</code> o similar.",
|
"nfsBody": "El servidor NFS <strong>y</strong> el cliente NFS dependen de <code>rpcbind</code> para negociar los puertos que usan <code>mountd</code>, <code>statd</code>, <code>lockd</code>, etc. Si tu host Proxmox <em>exporta</em> shares NFS a otras máquinas o <em>monta</em> shares NFS desde un NAS, no apliques esta opción. Los montajes fallarán con <code>mount.nfs: rpc.statd is not running</code> o similar.",
|
||||||
"runsTitle": "Qué ejecuta ProxMenux",
|
"runsTitle": "Qué ejecuta ProxMenux",
|
||||||
"runsOutro": "El paquete se queda instalado (para que tú u otra herramienta podáis reactivarlo más tarde). La unidad de servicio se desactiva para que el servicio no vuelva tras un reinicio.",
|
"runsOutro": "El paquete permanece instalado. ProxMenux registra el estado original de activación y ejecución de rpcbind.service y rpcbind.socket, y después detiene y desactiva ambas unidades para impedir que la activación por socket vuelva a iniciar el servicio.",
|
||||||
"verifyTitle": "Verificación",
|
"verifyTitle": "Verificación",
|
||||||
"verifyBody": "Tras aplicar, confirma que <code>rpcbind</code> está apagado y que nada escucha en el puerto 111:",
|
"verifyBody": "Tras aplicar, confirma que <code>rpcbind</code> está apagado y que nada escucha en el puerto 111:",
|
||||||
"reversibleTitle": "Reversible desde el menú Uninstall",
|
"reversibleTitle": "Restaura el estado original del servicio",
|
||||||
"reversibleBody": "Este cambio queda registrado. Abre <link>Uninstall Optimizations</link> y elige <em>RPC Disable</em> para restaurarlo. No se purga nada del sistema — simplemente se vuelve a activar el servicio y arranca de nuevo."
|
"reversibleBody": "El cambio queda registrado en <code>installed_tools.json</code>. <link>Uninstall Optimizations</link> devuelve cada unidad de rpcbind al estado habilitado o deshabilitado y activo o inactivo que tenía antes de aplicar ProxMenux; no presupone que rpcbind estuviera habilitado en todos los hosts."
|
||||||
},
|
},
|
||||||
"related": {
|
"related": {
|
||||||
"heading": "Relacionado",
|
"heading": "Relacionado",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user