mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-05 12:22:08 +00:00
fix(monitor): announce the NVIDIA driver the installer itself recommends
This commit is contained in:
@@ -24,6 +24,7 @@ on:
|
|||||||
- 'tests/test_figurine_installation.py'
|
- 'tests/test_figurine_installation.py'
|
||||||
- 'scripts/gpu_tpu/nvidia_installer.sh'
|
- 'scripts/gpu_tpu/nvidia_installer.sh'
|
||||||
- 'tests/test_nvidia*.py'
|
- 'tests/test_nvidia*.py'
|
||||||
|
- 'AppImage/scripts/managed_installs.py'
|
||||||
- 'scripts/post_install/customizable_post_install.sh'
|
- 'scripts/post_install/customizable_post_install.sh'
|
||||||
- 'AppImage/scripts/post_install_versions.py'
|
- 'AppImage/scripts/post_install_versions.py'
|
||||||
- 'AppImage/scripts/flask_proxmenux_routes.py'
|
- 'AppImage/scripts/flask_proxmenux_routes.py'
|
||||||
@@ -103,6 +104,9 @@ on:
|
|||||||
- 'AppImage/scripts/tests/test_proxmox_node_name.py'
|
- 'AppImage/scripts/tests/test_proxmox_node_name.py'
|
||||||
- 'AppImage/scripts/tests/test_pve_notifications_cfg_writes.py'
|
- 'AppImage/scripts/tests/test_pve_notifications_cfg_writes.py'
|
||||||
- 'tests/test_lvm_disk_read_policy.py'
|
- 'tests/test_lvm_disk_read_policy.py'
|
||||||
|
- 'scripts/gpu_tpu/nvidia_installer.sh'
|
||||||
|
- 'tests/test_nvidia*.py'
|
||||||
|
- 'AppImage/scripts/managed_installs.py'
|
||||||
- 'oci/**'
|
- 'oci/**'
|
||||||
- 'AppImage/scripts/oci_menu_bridge.py'
|
- 'AppImage/scripts/oci_menu_bridge.py'
|
||||||
- 'AppImage/scripts/lxc_apps.py'
|
- 'AppImage/scripts/lxc_apps.py'
|
||||||
|
|||||||
@@ -1188,21 +1188,15 @@ def _check_oci_app(entry: dict) -> dict:
|
|||||||
|
|
||||||
# ── NVIDIA driver checker ──
|
# ── NVIDIA driver checker ──
|
||||||
#
|
#
|
||||||
# Source of truth for what's available upstream:
|
# The driver update is the version the NVIDIA installer itself would
|
||||||
# `https://download.nvidia.com/XFree86/Linux-x86_64/latest.txt`
|
# recommend on this host: it filters by the running kernel, NVIDIA's
|
||||||
# returns the single newest version, e.g. "580.105.08"
|
# maintained branches, the PCI IDs of the host's GPUs and the NVENC patch,
|
||||||
# `https://download.nvidia.com/XFree86/Linux-x86_64/`
|
# and keeps the installed branch when it can. Asking the installer means the
|
||||||
# HTML directory listing — we scrape it for per-branch latest
|
# Monitor never announces a version the installer then refuses. The installer
|
||||||
# (so a user on 570.x gets 570.x's latest, without an automatic
|
# keeps its own caches of NVIDIA's pages for 24 hours.
|
||||||
# cross-branch upgrade).
|
|
||||||
#
|
|
||||||
# Cache TTL is 7 days because NVIDIA's release cadence on each branch
|
|
||||||
# is roughly monthly. The cache is in-memory only; AppImage restarts
|
|
||||||
# refresh it for free.
|
|
||||||
|
|
||||||
_NVIDIA_BASE = "https://download.nvidia.com/XFree86/Linux-x86_64"
|
_NVIDIA_INSTALLER = "/usr/local/share/proxmenux/scripts/gpu_tpu/nvidia_installer.sh"
|
||||||
_NVIDIA_CACHE_TTL = 7 * 86400
|
_NVIDIA_CHECK_TIMEOUT = 300
|
||||||
_nvidia_cache: dict[str, Any] = {"versions": [], "fetched_at": 0}
|
|
||||||
|
|
||||||
|
|
||||||
def _version_tuple(v: str) -> tuple:
|
def _version_tuple(v: str) -> tuple:
|
||||||
@@ -1219,64 +1213,46 @@ def _version_tuple(v: str) -> tuple:
|
|||||||
return tuple(out[:3])
|
return tuple(out[:3])
|
||||||
|
|
||||||
|
|
||||||
def _fetch_nvidia_versions(force: bool = False) -> list[str]:
|
def _nvidia_recommended_version(current: str) -> tuple[Optional[str], Optional[str]]:
|
||||||
"""Return the cached list of all upstream versions, or fetch fresh."""
|
"""The version the installer recommends for this host, or an error."""
|
||||||
now = time.time()
|
|
||||||
if not force and _nvidia_cache["versions"] and \
|
|
||||||
now - _nvidia_cache["fetched_at"] < _NVIDIA_CACHE_TTL:
|
|
||||||
return _nvidia_cache["versions"]
|
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(
|
# An older installer would ignore the option and open its menu.
|
||||||
_NVIDIA_BASE + "/",
|
with open(_NVIDIA_INSTALLER, encoding="utf-8", errors="replace") as handle:
|
||||||
headers={"User-Agent": "ProxMenux-Monitor/1.0"},
|
if "--recommended-version" not in handle.read():
|
||||||
)
|
return None, "the NVIDIA installer on this host is older than ProxMenux Monitor"
|
||||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
done = subprocess.run(
|
||||||
html = resp.read().decode("utf-8", errors="replace")
|
["bash", _NVIDIA_INSTALLER, "--recommended-version", current],
|
||||||
except Exception as e:
|
capture_output=True, text=True, timeout=_NVIDIA_CHECK_TIMEOUT,
|
||||||
print(f"[ProxMenux] NVIDIA version fetch failed: {e}")
|
# The host's curl and Python, not the AppImage's libraries.
|
||||||
return _nvidia_cache.get("versions", [])
|
env={k: v for k, v in os.environ.items() if k not in ("PYTHONPATH", "LD_LIBRARY_PATH")},
|
||||||
versions = sorted(
|
|
||||||
{m.group(1) for m in re.finditer(
|
|
||||||
r"""href=['"](\d+\.\d+(?:\.\d+)?)/?['"]""", html)},
|
|
||||||
key=_version_tuple,
|
|
||||||
reverse=True,
|
|
||||||
)
|
)
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as e:
|
||||||
|
return None, f"the NVIDIA installer could not be run: {e}"
|
||||||
|
versions = [line.strip() for line in done.stdout.splitlines()
|
||||||
|
if re.fullmatch(r"\d+\.\d+(\.\d+)?", line.strip())]
|
||||||
if versions:
|
if versions:
|
||||||
_nvidia_cache["versions"] = versions
|
return versions[-1], None
|
||||||
_nvidia_cache["fetched_at"] = now
|
if done.returncode == 0:
|
||||||
return versions
|
return None, None
|
||||||
|
return None, "the NVIDIA installer could not read the version list"
|
||||||
|
|
||||||
|
|
||||||
def _check_nvidia_xfree86(entry: dict) -> dict:
|
def _check_nvidia_xfree86(entry: dict) -> dict:
|
||||||
"""Report same-branch bugfix upgrades for the installed driver."""
|
"""Report the update the NVIDIA installer would recommend."""
|
||||||
current = entry.get("current_version")
|
current = entry.get("current_version")
|
||||||
if not current or not re.match(r"^\d+\.\d+(\.\d+)?$", current):
|
if not current or not re.match(r"^\d+\.\d+(\.\d+)?$", current):
|
||||||
return {"available": False, "latest": None,
|
return {"available": False, "latest": None,
|
||||||
"last_check": _now_iso(), "error": "no installed version"}
|
"last_check": _now_iso(), "error": "no installed version"}
|
||||||
|
|
||||||
versions = _fetch_nvidia_versions()
|
recommended, error = _nvidia_recommended_version(current)
|
||||||
if not versions:
|
available = bool(recommended) and _version_tuple(recommended) > _version_tuple(current)
|
||||||
return {"available": False, "latest": None,
|
same_branch = bool(recommended) and recommended.split(".")[0] == current.split(".")[0]
|
||||||
"last_check": _now_iso(),
|
|
||||||
"error": "could not parse upstream version listing"}
|
|
||||||
|
|
||||||
current_branch = current.split(".")[0]
|
|
||||||
same_branch = [v for v in versions if v.split(".")[0] == current_branch]
|
|
||||||
same_branch_latest = same_branch[0] if same_branch else None
|
|
||||||
|
|
||||||
available = False
|
|
||||||
latest: Optional[str] = None
|
|
||||||
if same_branch_latest and \
|
|
||||||
_version_tuple(same_branch_latest) > _version_tuple(current):
|
|
||||||
latest = same_branch_latest
|
|
||||||
available = True
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available": available,
|
"available": available,
|
||||||
"latest": latest,
|
"latest": recommended if available else None,
|
||||||
"last_check": _now_iso(),
|
"last_check": _now_iso(),
|
||||||
"error": None,
|
"error": error,
|
||||||
"_upgrade_kind": "patch" if available else None,
|
"_upgrade_kind": ("patch" if same_branch else "branch") if available else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1842,13 +1818,9 @@ def check_for_updates(force: bool = False) -> list[dict]:
|
|||||||
UI reads ``get_active_items()`` to render the inline "update
|
UI reads ``get_active_items()`` to render the inline "update
|
||||||
available" line.
|
available" line.
|
||||||
|
|
||||||
``force`` invalidates the per-source caches (currently only the
|
``force`` is kept for callers; the NVIDIA installer and OCI keep
|
||||||
NVIDIA versions list — OCI keeps its own internal cache).
|
their own caches.
|
||||||
"""
|
"""
|
||||||
if force:
|
|
||||||
_nvidia_cache["versions"] = []
|
|
||||||
_nvidia_cache["fetched_at"] = 0
|
|
||||||
|
|
||||||
updates_available: list[dict] = []
|
updates_available: list[dict] = []
|
||||||
successful_lxc_checks: list[tuple[int, int]] = []
|
successful_lxc_checks: list[tuple[int, int]] = []
|
||||||
with _lock:
|
with _lock:
|
||||||
|
|||||||
@@ -2004,23 +2004,22 @@ show_install_overview() {
|
|||||||
hybrid_yesno "$(translate 'NVIDIA GPU Driver Installation')" "$overview" 24 90
|
hybrid_yesno "$(translate 'NVIDIA GPU Driver Installation')" "$overview" 24 90
|
||||||
}
|
}
|
||||||
|
|
||||||
show_version_menu() {
|
# The versions this host is offered and the one recommended. The version
|
||||||
|
# menu and ProxMenux Monitor both use it, so the Monitor announces an update
|
||||||
|
# only when the installer would recommend it. Sets NVIDIA_OFFERED_VERSIONS,
|
||||||
|
# NVIDIA_RECOMMENDED_VERSION and the NVENC patch notes; fails when NVIDIA's
|
||||||
|
# version list cannot be read.
|
||||||
|
compute_nvidia_offer() {
|
||||||
local latest versions_list
|
local latest versions_list
|
||||||
local kernel_version
|
NVIDIA_OFFERED_VERSIONS=""
|
||||||
kernel_version=$(uname -r)
|
NVIDIA_RECOMMENDED_VERSION=""
|
||||||
|
NVIDIA_PATCH_FILTERED=false
|
||||||
show_proxmenux_logo
|
NVIDIA_PATCH_FILTER_NOTE=""
|
||||||
msg_title "$(translate 'NVIDIA GPU Driver Installation')"
|
|
||||||
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)
|
||||||
|
|
||||||
if [[ -z "$latest" ]] && [[ -z "$versions_list" ]]; then
|
if [[ -z "$latest" ]] && [[ -z "$versions_list" ]]; then
|
||||||
stop_spinner
|
|
||||||
hybrid_msgbox "$(translate 'Error')" \
|
|
||||||
"$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80
|
|
||||||
DRIVER_VERSION="cancel"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -2117,6 +2116,47 @@ show_version_menu() {
|
|||||||
latest=$(printf '%s\n' "$current_list" | head -n1 | tr -d '[:space:]')
|
latest=$(printf '%s\n' "$current_list" | head -n1 | tr -d '[:space:]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
NVIDIA_OFFERED_VERSIONS="$current_list"
|
||||||
|
NVIDIA_RECOMMENDED_VERSION="$latest"
|
||||||
|
NVIDIA_PATCH_FILTERED="$patch_filtered"
|
||||||
|
NVIDIA_PATCH_FILTER_NOTE="$patch_filter_note"
|
||||||
|
}
|
||||||
|
|
||||||
|
# For ProxMenux Monitor: the version the menu would recommend on this host for
|
||||||
|
# the driver in use (passed in, or read from nvidia-smi), with no interface and
|
||||||
|
# no change to the host. Prints nothing when none is recommended.
|
||||||
|
print_recommended_version() {
|
||||||
|
detect_nvidia_gpus
|
||||||
|
$NVIDIA_GPU_PRESENT || return 1
|
||||||
|
CURRENT_DRIVER_VERSION="${1:-}"
|
||||||
|
if [[ -z "$CURRENT_DRIVER_VERSION" ]] && command -v nvidia-smi >/dev/null 2>&1; then
|
||||||
|
CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
|
||||||
|
fi
|
||||||
|
compute_nvidia_offer || return 2
|
||||||
|
[[ -z "$NVIDIA_RECOMMENDED_VERSION" ]] || printf '%s\n' "$NVIDIA_RECOMMENDED_VERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_version_menu() {
|
||||||
|
local latest versions_list
|
||||||
|
local kernel_version
|
||||||
|
kernel_version=$(uname -r)
|
||||||
|
|
||||||
|
show_proxmenux_logo
|
||||||
|
msg_title "$(translate 'NVIDIA GPU Driver Installation')"
|
||||||
|
msg_info "$(translate 'Fetching NVIDIA driver versions supported by your GPU...')"
|
||||||
|
|
||||||
|
if ! compute_nvidia_offer; then
|
||||||
|
stop_spinner
|
||||||
|
hybrid_msgbox "$(translate 'Error')" \
|
||||||
|
"$(translate 'Could not retrieve versions list from NVIDIA. Please check your internet connection.')\n\nURL: ${NVIDIA_BASE_URL}" 10 80
|
||||||
|
DRIVER_VERSION="cancel"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local current_list="$NVIDIA_OFFERED_VERSIONS"
|
||||||
|
local patch_filtered="$NVIDIA_PATCH_FILTERED"
|
||||||
|
local patch_filter_note="$NVIDIA_PATCH_FILTER_NOTE"
|
||||||
|
latest="$NVIDIA_RECOMMENDED_VERSION"
|
||||||
|
|
||||||
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 belong to maintained NVIDIA branches that list your GPU PCI ID and are new enough to build against the running kernel. DKMS compilation is the final validation. The recommended version keeps the current branch, or uses the NVIDIA Production Branch on a fresh install.')"
|
menu_text+="$(translate 'Versions shown belong to maintained NVIDIA branches that list your GPU PCI ID and are new enough to build against the running kernel. DKMS compilation is the final validation. The recommended version keeps the current branch, or uses the NVIDIA Production Branch on a fresh install.')"
|
||||||
menu_text+="\n\n$(translate 'Running kernel:') ${kernel_version} — $(translate 'oldest driver offered:') $(nvidia_kernel_driver_floor)"
|
menu_text+="\n\n$(translate 'Running kernel:') ${kernel_version} — $(translate 'oldest driver offered:') $(nvidia_kernel_driver_floor)"
|
||||||
@@ -2548,5 +2588,9 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|||||||
auto_reinstall_from_state
|
auto_reinstall_from_state
|
||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
if [[ "${1:-}" == "--recommended-version" ]]; then
|
||||||
|
print_recommended_version "${2:-}"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
main
|
main
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""ProxMenux Monitor announces the NVIDIA driver the installer would recommend,
|
||||||
|
worked out by the same installer function the version menu uses."""
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
INSTALLER = (ROOT / "scripts/gpu_tpu/nvidia_installer.sh").read_text()
|
||||||
|
sys.path.insert(0, str(ROOT / "AppImage/scripts"))
|
||||||
|
|
||||||
|
import managed_installs # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def function(name):
|
||||||
|
return re.search(rf"^{name}\(\) \{{\n.*?^\}}\n", INSTALLER, re.M | re.S).group(0)
|
||||||
|
|
||||||
|
|
||||||
|
def answer(stdout, returncode=0):
|
||||||
|
return subprocess.CompletedProcess([], returncode, stdout, "")
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorAsksTheInstallerTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
installer = patch.object(managed_installs, "_NVIDIA_INSTALLER", str(ROOT / "scripts/gpu_tpu/nvidia_installer.sh"))
|
||||||
|
installer.start()
|
||||||
|
self.addCleanup(installer.stop)
|
||||||
|
|
||||||
|
def check(self, current, stdout, returncode=0):
|
||||||
|
with patch.object(managed_installs.subprocess, "run", return_value=answer(stdout, returncode)) as run:
|
||||||
|
result = managed_installs._check_nvidia_xfree86({"current_version": current})
|
||||||
|
return result, run
|
||||||
|
|
||||||
|
def test_an_older_installer_is_not_run(self):
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile("w", suffix=".sh") as old:
|
||||||
|
old.write("main\n"); old.flush()
|
||||||
|
with patch.object(managed_installs, "_NVIDIA_INSTALLER", old.name), \
|
||||||
|
patch.object(managed_installs.subprocess, "run") as run:
|
||||||
|
result = managed_installs._check_nvidia_xfree86({"current_version": "595.80"})
|
||||||
|
run.assert_not_called()
|
||||||
|
self.assertFalse(result["available"])
|
||||||
|
self.assertIn("older", result["error"])
|
||||||
|
|
||||||
|
def test_the_recommended_version_is_announced_and_the_host_tools_are_used(self):
|
||||||
|
with patch.dict(managed_installs.os.environ, {"LD_LIBRARY_PATH": "/appimage/lib"}):
|
||||||
|
result, run = self.check("595.80", "595.112.07\n")
|
||||||
|
self.assertEqual((result["available"], result["latest"], result["_upgrade_kind"]),
|
||||||
|
(True, "595.112.07", "patch"))
|
||||||
|
argv, kwargs = run.call_args.args[0], run.call_args.kwargs
|
||||||
|
self.assertEqual(argv[-2:], ["--recommended-version", "595.80"])
|
||||||
|
self.assertNotIn("LD_LIBRARY_PATH", kwargs["env"])
|
||||||
|
|
||||||
|
def test_nothing_is_announced_when_the_installer_recommends_the_installed_driver(self):
|
||||||
|
result, _ = self.check("580.196.07", "580.196.07\n")
|
||||||
|
self.assertEqual((result["available"], result["latest"], result["error"]), (False, None, None))
|
||||||
|
|
||||||
|
def test_nothing_is_announced_when_the_installer_offers_nothing(self):
|
||||||
|
# A GPU no maintained branch supports: the installer recommends nothing.
|
||||||
|
result, _ = self.check("470.256.02", "")
|
||||||
|
self.assertEqual((result["available"], result["error"]), (False, None))
|
||||||
|
|
||||||
|
def test_a_failed_version_list_is_an_error_not_an_update(self):
|
||||||
|
result, _ = self.check("595.80", "", returncode=2)
|
||||||
|
self.assertFalse(result["available"])
|
||||||
|
self.assertIn("could not read", result["error"])
|
||||||
|
|
||||||
|
def test_a_branch_change_is_marked(self):
|
||||||
|
result, _ = self.check("570.211.01", "595.112.07\n")
|
||||||
|
self.assertEqual(result["_upgrade_kind"], "branch")
|
||||||
|
|
||||||
|
|
||||||
|
class OneCalculationTests(unittest.TestCase):
|
||||||
|
def test_the_menu_and_the_monitor_mode_share_the_calculation(self):
|
||||||
|
self.assertIn("compute_nvidia_offer", function("show_version_menu"))
|
||||||
|
self.assertIn("compute_nvidia_offer", function("print_recommended_version"))
|
||||||
|
self.assertNotIn("download.nvidia.com", (ROOT / "AppImage/scripts/managed_installs.py").read_text())
|
||||||
|
|
||||||
|
def test_the_monitor_mode_prints_only_the_recommendation(self):
|
||||||
|
stubs = ('detect_nvidia_gpus() { NVIDIA_GPU_PRESENT=true; }\n'
|
||||||
|
'compute_nvidia_offer() { NVIDIA_RECOMMENDED_VERSION="595.112.07"; }\n')
|
||||||
|
done = subprocess.run(["bash", "-c", stubs + function("print_recommended_version")
|
||||||
|
+ "print_recommended_version 595.80"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
self.assertEqual((done.returncode, done.stdout), (0, "595.112.07\n"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user