#!/bin/bash # ========================================================== # ProxMenux - NVIDIA GPU Driver Installer # ========================================================== # Author : MacRimi # Copyright : (c) 2024 MacRimi # License : GPL-3.0 # Version : 1.3 # Last Updated: 26/08/2026 # ========================================================== # Description: # Installs and manages the NVIDIA proprietary driver on a # Proxmox VE host. Detects hardware, filters NVIDIA branches by # the installed GPU PCI IDs and handles the full lifecycle # (install / update / remove). # # Features: # - GPU detection + VFIO passthrough safety check # - GPU PCI-ID-aware branch filtering from NVIDIA supportedchips # - Nouveau blacklist + module unload # - DKMS-backed install (survives kernel upgrades) # - udev rules + nvidia-persistenced service # - Optional keylase/nvidia-patch (NVENC session limit) # - LXC container driver propagation (Alpine/Arch/Debian) # - Complete uninstall path # ========================================================== SCRIPT_TITLE="NVIDIA GPU Driver Installer for Proxmox VE" LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts" BASE_DIR="/usr/local/share/proxmenux" UTILS_FILE="$BASE_DIR/utils.sh" COMPONENTS_STATUS_FILE="$BASE_DIR/components_status.json" LOG_FILE="/tmp/nvidia_install.log" screen_capture="/tmp/proxmenux_nvidia_screen_capture_$$.txt" NVIDIA_BASE_URL="https://download.nvidia.com/XFree86/Linux-x86_64" 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 # containers are detected and the user confirms updating them after the host # install/reinstall finishes). NVIDIA_INSTALL_MIN_MB=2048 CT_ORIG_MEM="" export BASE_DIR export COMPONENTS_STATUS_FILE if [[ -f "$UTILS_FILE" ]]; then source "$UTILS_FILE" fi if [[ -f "$LOCAL_SCRIPTS/global/pci_passthrough_helpers.sh" ]]; then source "$LOCAL_SCRIPTS/global/pci_passthrough_helpers.sh" elif [[ -f "$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/global/pci_passthrough_helpers.sh" ]]; then source "$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)/global/pci_passthrough_helpers.sh" fi if [[ ! -f "$COMPONENTS_STATUS_FILE" ]]; then echo "{}" > "$COMPONENTS_STATUS_FILE" fi load_language initialize_cache # ========================================================== # GPU detection and current status # ========================================================== # Populated by detect_nvidia_gpus. Holds every video-controller PCI # Device ID (lowercase, 4-hex) so the version filter can drop branches # whose supportedchips.html doesn't list every card on this host. NVIDIA_HOST_GPU_IDS=() detect_nvidia_gpus() { # Video controllers only — the paired HDA audio functions (10de:xxxx # under class 0403) are not what the display driver ships support for. local lspci_output lspci_output=$(lspci -nn | grep -i "NVIDIA" \ | grep -Ei "VGA compatible controller|3D controller|Display controller" || true) if [[ -z "$lspci_output" ]]; then NVIDIA_GPU_PRESENT=false DETECTED_GPUS_TEXT="$(translate 'No NVIDIA GPU detected on this system.')" NVIDIA_HOST_GPU_IDS=() else NVIDIA_GPU_PRESENT=true DETECTED_GPUS_TEXT="" NVIDIA_HOST_GPU_IDS=() local i=1 while IFS= read -r line; do DETECTED_GPUS_TEXT+=" ${i}. ${line}\n" # Extract [10de:XXXX] — Vendor:Device pair. We keep only the # Device half (4-hex) lowercased, which is what NVIDIA lists in # each version's README/supportedchips.html. local dev_id dev_id=$(echo "$line" | grep -oiE '\[10de:[0-9a-f]{4}\]' | head -1 \ | sed -E 's/^\[10de:([0-9a-f]{4})\]$/\1/i' | tr 'A-F' 'a-f') if [[ -n "$dev_id" ]]; then NVIDIA_HOST_GPU_IDS+=("$dev_id") fi ((i++)) done <<< "$lspci_output" fi } check_gpu_not_in_vm_passthrough() { local dev vendor driver vfio_list="" for dev in /sys/bus/pci/devices/*; do vendor=$(cat "$dev/vendor" 2>/dev/null) [[ "$vendor" != "0x10de" ]] && continue if [[ -L "$dev/driver" ]]; then driver=$(basename "$(readlink "$dev/driver")") if [[ "$driver" == "vfio-pci" ]]; then vfio_list+=" • $(basename "$dev")\n" fi fi done [[ -z "$vfio_list" ]] && return 0 local msg msg="\n$(translate "One or more NVIDIA GPUs are currently configured for VM passthrough (vfio-pci):")\n\n" msg+="${vfio_list}\n" msg+="$(translate "Installing host drivers while the GPU is assigned to a VM could break passthrough and destabilize the system.")\n\n" msg+="$(translate "To install host drivers, first remove the GPU from VM passthrough configuration and reboot.")" dialog --backtitle "ProxMenux" \ --title "$(translate "GPU in VM Passthrough Mode")" \ --msgbox "$msg" 16 78 exit 0 } check_stale_vfio_config_for_nvidia() { local vfio_conf="/etc/modprobe.d/vfio.conf" [[ ! -f "$vfio_conf" ]] && return 0 local ids_line ids_part ids_line=$(grep "^options vfio-pci ids=" "$vfio_conf" 2>/dev/null | head -1) [[ -z "$ids_line" ]] && return 0 ids_part=$(echo "$ids_line" | grep -oE 'ids=[^[:space:]]+' | sed 's/ids=//') [[ -z "$ids_part" ]] && return 0 local dev vendor did vid_did local -a legacy_ids=() local legacy_list="" for dev in /sys/bus/pci/devices/*; do vendor=$(cat "$dev/vendor" 2>/dev/null) [[ "$vendor" != "0x10de" ]] && continue did=$(cat "$dev/device" 2>/dev/null) [[ -z "$did" ]] && continue vid_did="10de:${did#0x}" if echo ",${ids_part}," | grep -q ",${vid_did},"; then legacy_ids+=("$vid_did") legacy_list+=" • $(basename "$dev") [${vid_did}]\n" fi done [[ ${#legacy_ids[@]} -eq 0 ]] && return 0 local msg msg="\n$(translate 'A previous VFIO passthrough configuration was detected for the following NVIDIA GPU(s):')\n\n" msg+="${legacy_list}\n" msg+="$(translate 'The active kernel driver is not vfio-pci, but the entry in') /etc/modprobe.d/vfio.conf $(translate 'will rebind the GPU to vfio-pci on the next reboot, breaking the driver that is about to be installed.')\n\n" msg+="\Z1\Zb$(translate 'Do you want to remove the stale entry from vfio.conf and continue?')\Zn" dialog --colors --backtitle "ProxMenux" \ --title "$(translate 'Stale VFIO Config Detected')" \ --yesno "$msg" 18 78 || exit 0 if declare -F _clean_vfio_conf_ids >/dev/null 2>&1 \ && _clean_vfio_conf_ids "${legacy_ids[@]}"; then msg_info "$(translate 'Rebuilding initramfs after vfio.conf cleanup...')" update-initramfs -u >/dev/null 2>&1 || true msg_ok "$(translate 'Stale VFIO entries removed and initramfs rebuilt.')" | tee -a "$screen_capture" else msg_ok "$(translate 'No changes were needed in vfio.conf.')" | tee -a "$screen_capture" fi } detect_driver_status() { CURRENT_DRIVER_INSTALLED=false CURRENT_DRIVER_VERSION="" # First check if nvidia kernel module is actually loaded if grep -q "^nvidia " /proc/modules 2>/dev/null; then modprobe nvidia-uvm 2>/dev/null || true sleep 1 if 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) if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then CURRENT_DRIVER_INSTALLED=true # Register the installed driver version in components_status.json update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}' fi fi fi if $CURRENT_DRIVER_INSTALLED; then CURRENT_STATUS_TEXT="$(printf '%s %s' "$(translate 'NVIDIA driver installed:')" "$CURRENT_DRIVER_VERSION")" else CURRENT_STATUS_TEXT="$(translate 'No NVIDIA driver installed.')" fi if $CURRENT_DRIVER_INSTALLED; then CURRENT_STATUS_COLORED="${CURRENT_STATUS_TEXT}" else CURRENT_STATUS_COLORED="${CURRENT_STATUS_TEXT}" fi } # ========================================================== # LXC NVIDIA passthrough — discovery & userspace-libs update # Invoked after the host install/reinstall completes. Aligned with the install # path used in add_gpu_lxc.sh (distro-aware, memory/disk checks, --no-dkms, # --no-install-compat32-libs, visible progress via tee). # ========================================================== find_nvidia_containers() { NVIDIA_CONTAINERS=() for conf in /etc/pve/lxc/*.conf; do [[ -f "$conf" ]] || continue if grep -qiE "dev[0-9]+:.*nvidia" "$conf"; then NVIDIA_CONTAINERS+=("$(basename "$conf" .conf)") fi done } get_lxc_nvidia_version() { local ctid="$1" local version="" # Prefer nvidia-smi when the container is running (works with .run-installed drivers) if pct status "$ctid" 2>/dev/null | grep -q "running"; then version=$(pct exec "$ctid" -- nvidia-smi \ --query-gpu=driver_version --format=csv,noheader 2>/dev/null \ | head -1 | tr -d '[:space:]' || true) fi # Fallback: dpkg status for apt-installed libcuda1 (dir-type storage, no start needed) if [[ -z "$version" ]]; then local rootfs="/var/lib/lxc/${ctid}/rootfs" if [[ -f "${rootfs}/var/lib/dpkg/status" ]]; then version=$(grep -A5 "^Package: libcuda1$" "${rootfs}/var/lib/dpkg/status" \ | grep "^Version:" | head -1 | awk '{print $2}' | cut -d- -f1) fi fi echo "${version:-$(translate 'not installed')}" } _detect_container_distro() { local distro distro=$(pct exec "$1" -- grep "^ID=" /etc/os-release 2>/dev/null \ | cut -d= -f2 | tr -d '[:space:]"') echo "${distro:-unknown}" } _ensure_container_memory() { local ctid="$1" local cur_mem cur_mem=$(pct config "$ctid" 2>/dev/null | awk '/^memory:/{print $2}') [[ -z "$cur_mem" ]] && cur_mem=512 if [[ "$cur_mem" -lt "$NVIDIA_INSTALL_MIN_MB" ]]; then if whiptail --title "$(translate 'Low Container Memory')" --yesno \ "$(translate 'Container') ${ctid} $(translate 'has') ${cur_mem}MB RAM.\n\n$(translate 'The NVIDIA installer needs at least') ${NVIDIA_INSTALL_MIN_MB}MB $(translate 'to run without being killed by the OOM killer.')\n\n$(translate 'Increase container RAM temporarily to') ${NVIDIA_INSTALL_MIN_MB}MB?" \ 13 72; then CT_ORIG_MEM="$cur_mem" pct set "$ctid" -memory "$NVIDIA_INSTALL_MIN_MB" >>"$LOG_FILE" 2>&1 || true else msg_warn "$(translate 'Insufficient memory. Skipping LXC') ${ctid}." return 1 fi fi return 0 } _restore_container_memory() { local ctid="$1" if [[ -n "$CT_ORIG_MEM" ]]; then msg_info "$(translate 'Restoring container memory to') ${CT_ORIG_MEM}MB..." pct set "$ctid" -memory "$CT_ORIG_MEM" >>"$LOG_FILE" 2>&1 || true msg_ok "$(translate 'Memory restored.')" CT_ORIG_MEM="" fi } _start_container_and_wait() { local ctid="$1" msg_info "$(translate 'Starting container') ${ctid}..." pct start "$ctid" >>"$LOG_FILE" 2>&1 || true local ready=false for _ in {1..15}; do sleep 2 if pct exec "$ctid" -- true >/dev/null 2>&1; then ready=true break fi done if ! $ready; then msg_warn "$(translate 'Container') ${ctid} $(translate 'did not become ready. Skipping.')" return 1 fi msg_ok "$(translate 'Container') ${ctid} $(translate 'started.')" | tee -a "$screen_capture" return 0 } update_lxc_nvidia() { local ctid="$1" local version="$2" local started_here=false local old_version old_version=$(get_lxc_nvidia_version "$ctid") msg_info2 "$(translate 'Container') ${ctid}: $(translate 'updating NVIDIA userspace libs') (${old_version} → ${version})" if ! pct status "$ctid" 2>/dev/null | grep -q "running"; then started_here=true _start_container_and_wait "$ctid" || return 1 fi msg_info "$(translate 'Detecting container OS...')" local distro distro=$(_detect_container_distro "$ctid") msg_ok "$(translate 'Container OS:') ${distro}" | tee -a "$screen_capture" local install_rc=0 case "$distro" in arch|manjaro|endeavouros) msg_info2 "$(translate 'Upgrading NVIDIA utils (Arch)...')" pct exec "$ctid" -- bash -c \ "pacman -Syu --noconfirm nvidia-utils" \ 2>&1 | tee -a "$LOG_FILE" install_rc=${PIPESTATUS[0]} ;; *) local run_file="${NVIDIA_WORKDIR}/NVIDIA-Linux-x86_64-${version}.run" if [[ ! -f "$run_file" ]]; then msg_warn "$(translate 'Installer not found:') ${run_file}. $(translate 'Skipping LXC') ${ctid}." install_rc=1 elif ! _ensure_container_memory "$ctid"; then install_rc=1 else local free_mb free_mb=$(pct exec "$ctid" -- df -P -m / 2>/dev/null | awk 'END{print $4}') free_mb=${free_mb:-0} if [[ "$free_mb" -lt 1500 ]]; then _restore_container_memory "$ctid" whiptail --backtitle "ProxMenux" \ --title "$(translate 'Insufficient Disk Space')" \ --msgbox "\n$(translate 'Container') ${ctid} $(translate 'has only') ${free_mb}MB $(translate 'of free disk space.')\n\n$(translate 'NVIDIA libs require approximately 1.5GB of free space.')" \ 11 72 msg_warn "$(translate 'Insufficient disk space. Skipping LXC') ${ctid}." install_rc=1 else local extract_dir="${NVIDIA_WORKDIR}/extracted_${version}" local archive="/tmp/nvidia_lxc_${version}.tar.gz" msg_info2 "$(translate 'Extracting NVIDIA installer on host...')" rm -rf "$extract_dir" sh "$run_file" --extract-only --target "$extract_dir" 2>&1 | tee -a "$LOG_FILE" if [[ ${PIPESTATUS[0]} -ne 0 ]]; then msg_warn "$(translate 'Extraction failed. Check log:') ${LOG_FILE}" _restore_container_memory "$ctid" install_rc=1 else msg_ok "$(translate 'NVIDIA installer extracted.')" | tee -a "$screen_capture" msg_info2 "$(translate 'Packing installer archive...')" tar --checkpoint=5000 --checkpoint-action=dot \ -czf "$archive" -C "$extract_dir" . 2>&1 | tee -a "$LOG_FILE" echo "" local archive_size archive_size=$(du -sh "$archive" 2>/dev/null | cut -f1) msg_ok "$(translate 'Archive ready') (${archive_size})." | tee -a "$screen_capture" msg_info "$(translate 'Copying installer to container') ${ctid}..." if ! pct push "$ctid" "$archive" /tmp/nvidia_lxc.tar.gz >>"$LOG_FILE" 2>&1; then msg_warn "$(translate 'pct push failed. Check log:') ${LOG_FILE}" rm -f "$archive" rm -rf "$extract_dir" _restore_container_memory "$ctid" install_rc=1 else rm -f "$archive" msg_ok "$(translate 'Installer copied to container.')" | tee -a "$screen_capture" msg_info2 "$(translate 'Running NVIDIA installer in container. This may take several minutes...')" echo "" >>"$LOG_FILE" if [[ "$distro" == "alpine" ]]; then # Alpine uses musl libc and does not ship a glibc dynamic # loader, so the nvidia-installer binary (glibc) cannot # execute. We pull `gcompat` to provide the glibc loader # and a libc shim, then copy the userspace libs and the # standard NVIDIA binaries by hand. SONAME symlinks are # built from `readelf` (binutils) instead of trusting a # hard-coded list — the .run ships ~50 .so files and the # set varies between branches. pct exec "$ctid" -- sh -c ' set -e mkdir -p /tmp/nvidia_lxc_install tar -xzf /tmp/nvidia_lxc.tar.gz -C /tmp/nvidia_lxc_install apk add --no-cache gcompat binutils >/dev/null cd /tmp/nvidia_lxc_install mkdir -p /usr/lib /usr/bin cp -P *.so* /usr/lib/ 2>/dev/null || true for lib in /usr/lib/lib*.so.*; do [ -f "$lib" ] || continue soname=$(readelf -d "$lib" 2>/dev/null | grep SONAME | head -n1 | sed -e "s/.*\[//" -e "s/\].*//") [ -n "$soname" ] && [ "$(basename "$lib")" != "$soname" ] && ln -sf "$(basename "$lib")" "/usr/lib/$soname" done for bin in nvidia-smi nvidia-debugdump nvidia-cuda-mps-control nvidia-cuda-mps-server nvidia-persistenced nvidia-modprobe; do [ -f "$bin" ] && cp -P "$bin" /usr/bin/ && chmod 755 "/usr/bin/$bin" done rm -rf /tmp/nvidia_lxc_install /tmp/nvidia_lxc.tar.gz ' 2>&1 | tee -a "$LOG_FILE" install_rc=${PIPESTATUS[0]} else pct exec "$ctid" -- bash -c " mkdir -p /tmp/nvidia_lxc_install tar -xzf /tmp/nvidia_lxc.tar.gz -C /tmp/nvidia_lxc_install 2>&1 /tmp/nvidia_lxc_install/nvidia-installer \ --no-kernel-modules \ --no-questions \ --ui=none \ --no-nouveau-check \ --no-dkms \ --no-install-compat32-libs EXIT=\$? rm -rf /tmp/nvidia_lxc_install /tmp/nvidia_lxc.tar.gz exit \$EXIT " 2>&1 | tee -a "$LOG_FILE" install_rc=${PIPESTATUS[0]} fi rm -rf "$extract_dir" _restore_container_memory "$ctid" fi fi fi fi ;; esac if [[ $install_rc -ne 0 ]]; then msg_warn "$(translate 'NVIDIA update failed for LXC') ${ctid} (rc=${install_rc}). $(translate 'Check log:') ${LOG_FILE}" if $started_here; then pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true fi return 1 fi if pct exec "$ctid" -- sh -c "which nvidia-smi" >/dev/null 2>&1; then local new_ver new_ver=$(pct exec "$ctid" -- nvidia-smi \ --query-gpu=driver_version --format=csv,noheader 2>/dev/null \ | head -1 | tr -d '[:space:]' || true) msg_ok "$(translate 'Container') ${ctid}: ${old_version} → ${new_ver:-$version}" | tee -a "$screen_capture" else msg_warn "$(translate 'nvidia-smi not found in container') ${ctid} $(translate 'after update.')" fi if $started_here; then msg_info "$(translate 'Stopping container') ${ctid}..." pct stop "$ctid" >>"$LOG_FILE" 2>&1 || true msg_ok "$(translate 'Container stopped.')" | tee -a "$screen_capture" fi return 0 } # Post-host-install LXC update offer — scans for NVIDIA LXCs and, if any are # found, asks the user if they want to propagate the driver update to them. offer_lxc_updates_if_any() { local target_version="$1" find_nvidia_containers [[ ${#NVIDIA_CONTAINERS[@]} -eq 0 ]] && return 0 local info ctid lxc_ver ct_name info="\n$(translate 'The following LXC containers have NVIDIA passthrough configured:')\n\n" for ctid in "${NVIDIA_CONTAINERS[@]}"; do lxc_ver=$(get_lxc_nvidia_version "$ctid") ct_name=$(pct config "$ctid" 2>/dev/null | grep "^hostname:" | awk '{print $2}') info+=" CT ${ctid} ${ct_name:+(${ct_name})} — $(translate 'driver:') ${lxc_ver}\n" done info+="\n$(translate 'Do you want to update the NVIDIA userspace libraries inside these containers to match the host?')" if ! hybrid_whiptail_yesno "$(translate 'Update NVIDIA in LXC Containers')" "$info" 20 80; then msg_info2 "$(translate 'LXC update skipped by user.')" return 0 fi for ctid in "${NVIDIA_CONTAINERS[@]}"; do update_lxc_nvidia "$ctid" "$target_version" || true done } # ========================================================== # System preparation (repos, headers, etc.) # ========================================================== ensure_repos_and_headers() { # Bootstrap APT repos FIRST. On a fresh Proxmox install the # pve-no-subscription / debian repos aren't configured by default # → `pve-headers-$(uname -r)` and `build-essential` come back as # "Unable to locate package" and the NVIDIA install bails out with # "no cc found". We delegate to the shared helper (same one the # post-install flow uses), which owns its own spinner pair — that's # why this block has to run BEFORE we open our own msg_info. if ! declare -F ensure_repositories >/dev/null 2>&1; then local _utils_install="$LOCAL_SCRIPTS/global/utils-install-functions.sh" [[ ! -f "$_utils_install" ]] && _utils_install="/usr/local/share/proxmenux/scripts/global/utils-install-functions.sh" # shellcheck source=/dev/null [[ -f "$_utils_install" ]] && source "$_utils_install" fi if declare -F ensure_repositories >/dev/null 2>&1; then ensure_repositories >>"$LOG_FILE" 2>&1 || true fi # Now own the spinner for the headers + build-tools check. msg_info "$(translate 'Checking kernel headers and build tools...')" local kver kver=$(uname -r) apt-get update -qq >>"$LOG_FILE" 2>&1 if ! dpkg -s "pve-headers-$kver" >/dev/null 2>&1 && \ ! dpkg -s "proxmox-headers-$kver" >/dev/null 2>&1; then apt-get install -y "pve-headers-$kver" "proxmox-headers-$kver" build-essential dkms >>"$LOG_FILE" 2>&1 || true else apt-get install -y build-essential dkms >>"$LOG_FILE" 2>&1 || true fi 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() { msg_info "$(translate 'Blacklisting nouveau driver...')" local legacy_owned=false if _nouveau_legacy_file_is_proxmenux_shape; then rm -f "$NVIDIA_NOUVEAU_LEGACY_BLACKLIST" legacy_owned=true _nouveau_state_set "legacy_migrated" fi if ! grep -q '^blacklist nouveau$' "$NVIDIA_GLOBAL_BLACKLIST" 2>/dev/null; then 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 options nouveau modeset=0 EOF # Attempt to unload nouveau if currently loaded. # Close the spinner from the opening msg_info before going further — # otherwise the second msg_info below leaves the first one spinning # forever (visible on fresh installs where nouveau is loaded; invisible # on reinstalls where this branch is skipped). if grep -q "^nouveau " /proc/modules 2>/dev/null; then msg_ok "$(translate 'nouveau driver has been blacklisted.')" | tee -a "$screen_capture" msg_info "$(translate 'Nouveau module is loaded, attempting to unload...')" modprobe -r nouveau 2>/dev/null || true sleep 1 # Check if unload succeeded if grep -q "^nouveau " /proc/modules 2>/dev/null; then NOUVEAU_STILL_LOADED=true msg_warn "$(translate 'Could not unload nouveau module (may be in use). The blacklist will take effect after reboot. Installation will continue but a reboot will be required.')" echo "WARNING: nouveau module still loaded after unload attempt" >> "$LOG_FILE" else NOUVEAU_STILL_LOADED=false msg_ok "$(translate 'nouveau module unloaded successfully.')" | tee -a "$screen_capture" fi else NOUVEAU_STILL_LOADED=false msg_ok "$(translate 'nouveau driver has been blacklisted.')" | tee -a "$screen_capture" fi } ensure_modules_config() { msg_info "$(translate 'Configuring NVIDIA modules...')" cat > /etc/modules-load.d/nvidia-vfio.conf <<'EOF' nvidia nvidia_uvm EOF msg_ok "$(translate 'Modules configuration updated.')" | tee -a "$screen_capture" } stop_and_disable_nvidia_services() { local services=( "nvidia-persistenced.service" "nvidia-persistenced" "nvidia-powerd.service" ) local services_detected=0 for service in "${services[@]}"; do if systemctl is-active --quiet "$service" 2>/dev/null || \ systemctl is-enabled --quiet "$service" 2>/dev/null; then services_detected=1 break fi done if [ "$services_detected" -eq 1 ]; then msg_info "$(translate 'Stopping and disabling NVIDIA services...')" for service in "${services[@]}"; do if systemctl is-active --quiet "$service" 2>/dev/null; then systemctl stop "$service" >/dev/null 2>&1 || true fi if systemctl is-enabled --quiet "$service" 2>/dev/null; then systemctl disable "$service" >/dev/null 2>&1 || true fi done sleep 2 msg_ok "$(translate 'NVIDIA services stopped and disabled.')" | tee -a "$screen_capture" fi } unload_nvidia_modules() { for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do modprobe -r "$mod" >/dev/null 2>&1 || true done # Give the kernel a moment to finalize sysfs teardown before re-checking. # Reading /proc/modules directly (instead of lsmod) avoids the # "could not open /sys/module//holders" race when a module has just # been removed from /proc/modules but its sysfs dir hasn't been reaped yet. sleep 1 if grep -q "^nvidia" /proc/modules 2>/dev/null; then for mod in nvidia_uvm nvidia_drm nvidia_modeset nvidia; do modprobe -r --force "$mod" >/dev/null 2>&1 || true done sleep 1 fi if grep -q "^nvidia" /proc/modules 2>/dev/null; then if command -v lsof >/dev/null 2>&1; then echo "$(translate 'Processes using NVIDIA:'):" >> "$LOG_FILE" lsof /dev/nvidia* 2>/dev/null >> "$LOG_FILE" || true fi else msg_ok "$(translate 'NVIDIA kernel modules unloaded successfully.')" | tee -a "$screen_capture" fi } complete_nvidia_uninstall() { stop_and_disable_nvidia_services unload_nvidia_modules if command -v nvidia-uninstall >/dev/null 2>&1; then msg_info "$(translate 'Running NVIDIA uninstaller...')" nvidia-uninstall --silent >>"$LOG_FILE" 2>&1 || true msg_ok "$(translate 'NVIDIA uninstaller completed.')" fi msg_ok "$(translate 'NVIDIA uninstallation steps completed.')" | tee -a "$screen_capture" cleanup_nvidia_dkms msg_info "$(translate 'Removing NVIDIA packages...')" apt-get -y purge 'nvidia-*' 'libnvidia-*' 'cuda-*' 'libcudnn*' >>"$LOG_FILE" 2>&1 || true apt-get -y autoremove --purge >>"$LOG_FILE" 2>&1 || true apt-get -y autoclean >>"$LOG_FILE" 2>&1 || true rm -f /etc/modules-load.d/nvidia-vfio.conf rm -f /etc/udev/rules.d/70-nvidia.rules rm -rf /usr/lib/modprobe.d/nvidia*.conf rm -rf /etc/modprobe.d/nvidia*.conf restore_nouveau_after_uninstall 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-patch" -exec rm -rf {} + 2>/dev/null || true fi update_component_status "nvidia_driver" "removed" "" "gpu" '{}' msg_ok "$(translate 'Complete NVIDIA uninstallation finished.')" | tee -a "$screen_capture" } cleanup_nvidia_dkms() { local versions versions=$(dkms status 2>/dev/null | awk -F, '/nvidia/ {gsub(/ /,"",$2); print $2}' || true) [[ -z "$versions" ]] && return 0 msg_info "$(translate 'Removing NVIDIA DKMS entries...')" while IFS= read -r ver; do [[ -z "$ver" ]] && continue dkms remove -m nvidia -v "$ver" --all >/dev/null 2>&1 || true done <<< "$versions" msg_ok "$(translate 'NVIDIA DKMS entries removed.')" } ensure_workdir() { mkdir -p "$NVIDIA_WORKDIR" } # ========================================================== # System detection # ========================================================== get_system_info() { if [[ -f /etc/pve/.version ]]; then PVE_VERSION=$(cat /etc/pve/.version) else PVE_VERSION="unknown" fi } is_current_nvidia_patched() { local status_file="/usr/local/share/proxmenux/components_status.json" [[ -f "$status_file" ]] || return 1 command -v jq >/dev/null 2>&1 || return 1 local patched patched=$(jq -r '.nvidia_driver.patched // false' "$status_file" 2>/dev/null) [[ "$patched" == "true" ]] } KEYLASE_PATCH_CACHE="/var/cache/proxmenux/keylase_patch_versions.txt" KEYLASE_PATCH_TTL_SECONDS=$((7 * 86400)) KEYLASE_PATCH_URL="https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh" # NVIDIA branch classification comes from the vendor's own Unix drivers # page, not the CDN — the CDN publishes every branch (production, new # feature, vulkan-beta, developer) in the same flat directory, whereas # the vendor page carries the current heads clearly labelled "Production # Branch", "New Feature Branch" and "Legacy GPU version". Extracting the # majors from those three lines gives us the set of branches NVIDIA # currently endorses for end users, with zero manual maintenance on our # side — when NVIDIA promotes a new rama the cache picks it up on the # next 24 h refresh. Cache is fail-open: if the fetch is blocked or the # page layout changes, we skip the branch filter rather than emptying # the picker. NVIDIA_BRANCHES_CACHE="/var/cache/proxmenux/nvidia_stable_branches.txt" NVIDIA_PRODUCTION_HEAD_CACHE="/var/cache/proxmenux/nvidia_production_head.txt" NVIDIA_BRANCH_HEADS_CACHE="/var/cache/proxmenux/nvidia_branch_heads.txt" NVIDIA_GPU_SUPPORT_CACHE_PREFIX="/var/cache/proxmenux/nvidia_gpu_support_" NVIDIA_BRANCHES_TTL_SECONDS=$((24 * 3600)) NVIDIA_BRANCHES_URL="https://www.nvidia.com/en-us/drivers/unix/" refresh_nvidia_branches_cache() { local now ts age now=$(date +%s) if [[ -f "$NVIDIA_BRANCHES_CACHE" ]]; then ts=$(stat -c '%Y' "$NVIDIA_BRANCHES_CACHE" 2>/dev/null || echo 0) age=$(( now - ts )) if (( age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s "$NVIDIA_BRANCHES_CACHE" ]]; then return 0 fi fi mkdir -p "$(dirname "$NVIDIA_BRANCHES_CACHE")" 2>/dev/null || return 1 local html tmp html=$(curl -fsSL -A "Mozilla/5.0" --max-time 15 "$NVIDIA_BRANCHES_URL" 2>/dev/null) || return 1 [[ -z "$html" ]] && return 1 local clean tmp_full clean=$(echo "$html" | perl -0777 -pe 's///gs' 2>/dev/null) tmp=$(mktemp) tmp_full=$(mktemp) # Two label shapes on the page: # • Production / New Feature → "… Branch Version:" then full. # • Legacy → "Legacy GPU version (NNN.xx series):" # then full — the word "Version" # lives inside the parenthesised label # so the Production/Feature regex misses # it (separate alternative below). # HTML comments are stripped first so vestigial `` blocks in older ia32 rows don't leak stale majors. # Perl one-liner because we want to keep the label text next to the # version — shell greps can only give us one or the other, and we # need the label to tag each head as production / feature / legacy. echo "$clean" \ | perl -ne ' while (/(Production Branch Version|New Feature Branch Version|Legacy GPU version \(([0-9]+)\.xx series\)):[^<]*(?:<\/span>)?\s*]*>([0-9]+\.[0-9]+(?:\.[0-9]+)?)/gi) { my $label = lc($1); my $ver = $3; my ($maj) = split(/\./, $ver); my $type = "unknown"; if ($label =~ /production branch/) { $type = "production" } elsif ($label =~ /new feature branch/) { $type = "feature" } elsif ($label =~ /legacy gpu version/) { $type = "legacy" } print "$type|$maj|$ver\n"; }' \ | sort -u -t'|' -k1,1 -k2,2n > "$tmp_full" if [[ ! -s "$tmp_full" ]]; then rm -f "$tmp" "$tmp_full" return 1 fi # Derive the majors-only file from the same source, but ONLY for # production + legacy — New Feature Branch heads (610.x today) are # intentionally NOT in the endorsed whitelist because they carry # kernel/driver features still under stabilisation. Superseded # production branches still qualify via the release-count heuristic # in filter_option_c_branch, so operators on 580 / 570 / 550 / 535 # keep bugfix upgrade options. awk -F'|' '$1 == "production" || $1 == "legacy" { print $2 }' \ "$tmp_full" | sort -un > "$tmp" if [[ ! -s "$tmp" ]]; then rm -f "$tmp" "$tmp_full" return 1 fi mv "$tmp" "$NVIDIA_BRANCHES_CACHE" mv "$tmp_full" "$NVIDIA_BRANCH_HEADS_CACHE" # Extra pass: capture the full Production Branch head so the picker # can default to it instead of the highest numeric available (which # could be a New Feature Branch head — NVIDIA doesn't recommend those # as the general-purpose default). Best-effort. local prod_head prod_head=$(echo "$clean" \ | grep -oiE 'Production Branch Version:[^<]*()?\s*]*>[0-9]+\.[0-9]+(\.[0-9]+)?' \ | grep -oE '>[0-9]+\.[0-9]+(\.[0-9]+)?' \ | tr -d '>' \ | head -n1) if [[ -n "$prod_head" ]]; then echo "$prod_head" > "$NVIDIA_PRODUCTION_HEAD_CACHE" else rm -f "$NVIDIA_PRODUCTION_HEAD_CACHE" 2>/dev/null || true fi return 0 } # Return the full head version associated with a major from the # branch-heads cache (e.g. `get_nvidia_branch_head 595` → 595.91.07). # Cache lines are `type|major|version` — filter by major, print # version. Used to know which release inside a branch to hit for the # PCI-ID supported-GPUs list. get_nvidia_branch_head() { local major="$1" [[ -f "$NVIDIA_BRANCH_HEADS_CACHE" && -s "$NVIDIA_BRANCH_HEADS_CACHE" ]] || return 1 awk -F'|' -v m="$major" '$2 == m { print $3; exit }' "$NVIDIA_BRANCH_HEADS_CACHE" } # Refresh the per-branch supported-GPU cache. Uses the branch head as # the "sample release" for the whole branch — NVIDIA rarely drops chip # support inside a live branch, so this is a solid proxy that also # minimises fetch count (~3 heads total instead of one per release). # Written to nvidia_gpu_support_MAJOR.txt with one lowercase hex device # id per line. Same 24h TTL as the branches cache. refresh_nvidia_gpu_support_for_major() { local major="$1" [[ -z "$major" ]] && return 1 local cache="${NVIDIA_GPU_SUPPORT_CACHE_PREFIX}${major}.txt" local now ts age now=$(date +%s) if [[ -f "$cache" ]]; then ts=$(stat -c '%Y' "$cache" 2>/dev/null || echo 0) age=$(( now - ts )) if (( age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s "$cache" ]]; then return 0 fi fi local head_ver head_ver=$(get_nvidia_branch_head "$major") || return 1 [[ -z "$head_ver" ]] && return 1 local url="https://download.nvidia.com/XFree86/Linux-x86_64/${head_ver}/README/supportedchips.html" local html tmp html=$(curl -fsSL -A "Mozilla/5.0" --max-time 20 "$url" 2>/dev/null) || return 1 [[ -z "$html" ]] && return 1 mkdir -p "$(dirname "$cache")" 2>/dev/null || return 1 tmp=$(mktemp) # NVIDIA's supportedchips.html lays out each GPU row as a with # the PCI Device ID in 4-char hex. Anchor on the surrounding tag so # we don't sweep up unrelated 4-hex strings elsewhere in the page. echo "$html" \ | grep -oiE '[0-9A-F]{4}' \ | grep -oiE '[0-9A-F]{4}' \ | tr 'A-F' 'a-f' \ | sort -u > "$tmp" if [[ -s "$tmp" ]]; then mv "$tmp" "$cache" return 0 fi rm -f "$tmp" return 1 } # True if every detected NVIDIA GPU on this host has its device id in # the branch's supported list. Fail-open when the cache is missing so # a network hiccup never locks the picker out. is_branch_compatible_with_host_gpus() { local major="$1" local cache="${NVIDIA_GPU_SUPPORT_CACHE_PREFIX}${major}.txt" [[ -f "$cache" && -s "$cache" ]] || return 0 [[ ${#NVIDIA_HOST_GPU_IDS[@]} -eq 0 ]] && return 0 local id for id in "${NVIDIA_HOST_GPU_IDS[@]}"; do grep -qFx "$id" "$cache" || return 1 done return 0 } # Reject Vulkan-beta / short-lived / developer branches by counting # how many releases NVIDIA actually shipped inside that major on the # CDN. Production and long-lived New Feature branches accumulate many # release rows (470=17, 535=20, 550=14, 570=12, 580=14 …); Vulkan-beta # and developer branches only ever get 1-4 releases before being # superseded (590=2, 565=2, 530=2, 555=4 …). Threshold 5 separates the # two groups cleanly at time of writing. # Fail-open: if the release-count map hasn't been built for whatever # reason, the branch passes (kernel + GPU-compat + curated whitelist # are still enforced upstream). The map is populated once per # `filter_option_c_branch` invocation, so no repeated CDN scraping. NVIDIA_BRANCH_MIN_RELEASES=5 declare -A NVIDIA_BRANCH_RELEASE_COUNT=() is_branch_release_count_sufficient() { local major="$1" [[ -z "$major" ]] && return 1 [[ ${#NVIDIA_BRANCH_RELEASE_COUNT[@]} -eq 0 ]] && return 0 local n="${NVIDIA_BRANCH_RELEASE_COUNT[$major]:-0}" (( n >= NVIDIA_BRANCH_MIN_RELEASES )) } get_nvidia_production_head() { [[ -f "$NVIDIA_PRODUCTION_HEAD_CACHE" && -s "$NVIDIA_PRODUCTION_HEAD_CACHE" ]] || return 1 local v v=$(head -n1 "$NVIDIA_PRODUCTION_HEAD_CACHE" | tr -d '[:space:]') [[ -z "$v" ]] && return 1 printf '%s\n' "$v" } is_nvidia_stable_branch() { local major="$1" [[ -z "$major" ]] && return 1 # Fail-open: no cache → don't filter (upstream behaviour preserved). [[ -f "$NVIDIA_BRANCHES_CACHE" && -s "$NVIDIA_BRANCHES_CACHE" ]] || return 0 grep -qFx "$major" "$NVIDIA_BRANCHES_CACHE" } refresh_keylase_patch_cache() { local now ts age now=$(date +%s) if [[ -f "$KEYLASE_PATCH_CACHE" ]]; then ts=$(stat -c '%Y' "$KEYLASE_PATCH_CACHE" 2>/dev/null || echo 0) age=$(( now - ts )) if (( age < KEYLASE_PATCH_TTL_SECONDS )) && [[ -s "$KEYLASE_PATCH_CACHE" ]]; then return 0 fi fi mkdir -p "$(dirname "$KEYLASE_PATCH_CACHE")" 2>/dev/null || return 1 local tmp tmp=$(mktemp) if curl -fsSL --max-time 15 "$KEYLASE_PATCH_URL" 2>/dev/null \ | grep -oE '\["[0-9]+\.[0-9]+(\.[0-9]+)?"\]' \ | sed -E 's/\["([0-9.]+)"\]/\1/' \ | sort -u > "$tmp" && [[ -s "$tmp" ]]; then mv "$tmp" "$KEYLASE_PATCH_CACHE" return 0 fi rm -f "$tmp" return 1 } is_keylase_patch_supported() { local ver="$1" [[ -z "$ver" ]] && return 1 [[ -f "$KEYLASE_PATCH_CACHE" && -s "$KEYLASE_PATCH_CACHE" ]] || return 1 grep -qFx "$ver" "$KEYLASE_PATCH_CACHE" } filter_keylase_supported() { local versions_in="$1" while IFS= read -r ver; do [[ -z "$ver" ]] && continue if is_keylase_patch_supported "$ver"; then printf '%s\n' "$ver" fi done <<< "$versions_in" } filter_option_c_branch() { local versions_in="$1" local current="$2" local _unused_recommended_branch="$3" refresh_nvidia_branches_cache 2>/dev/null || true local target_branch="" if [[ -f "$NVIDIA_BRANCHES_CACHE" && -s "$NVIDIA_BRANCHES_CACHE" ]]; then target_branch=$(head -n1 "$NVIDIA_BRANCHES_CACHE") fi # Build a majors→count map from the incoming version list. This is # what backs `is_branch_release_count_sufficient` — done once per # call so the tight loop below stays local-arithmetic only. NVIDIA_BRANCH_RELEASE_COUNT=() while IFS= read -r _v; do [[ -z "$_v" ]] && continue local _m="${_v%%.*}" NVIDIA_BRANCH_RELEASE_COUNT[$_m]=$(( ${NVIDIA_BRANCH_RELEASE_COUNT[$_m]:-0} + 1 )) done <<< "$versions_in" # Grab the head (highest version) of every major so we know which # release to sample for supportedchips.html. We use the CDN listing # directly for this — the branch-heads cache only carries the # endorsed heads, not the superseded ones. declare -A _major_head=() while IFS= read -r _v; do [[ -z "$_v" ]] && continue local _m="${_v%%.*}" [[ -z "${_major_head[$_m]:-}" ]] && _major_head[$_m]="$_v" done < <(printf '%s\n' "$versions_in") # Warm the supported-GPU cache for every stable major (whitelist # heads: head already known → normal path; superseded heads: seed the # cache-file's head-version by directly writing a lightweight lookup). # For endorsed majors we can use refresh_nvidia_gpu_support_for_major # as-is (it looks up NVIDIA_BRANCH_HEADS_CACHE). For non-endorsed # majors we need to fetch supportedchips.html against the highest # release we saw in the CDN listing. local _m _head _cache _now _ts _age _html _tmp _now=$(date +%s) for _m in "${!_major_head[@]}"; do _head="${_major_head[$_m]}" _cache="${NVIDIA_GPU_SUPPORT_CACHE_PREFIX}${_m}.txt" if [[ -f "$_cache" ]]; then _ts=$(stat -c '%Y' "$_cache" 2>/dev/null || echo 0) _age=$(( _now - _ts )) if (( _age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s "$_cache" ]]; then continue fi fi _html=$(curl -fsSL -A "Mozilla/5.0" --max-time 20 \ "https://download.nvidia.com/XFree86/Linux-x86_64/${_head}/README/supportedchips.html" \ 2>/dev/null) || continue [[ -z "$_html" ]] && continue mkdir -p "$(dirname "$_cache")" 2>/dev/null || continue _tmp=$(mktemp) echo "$_html" \ | grep -oiE '[0-9A-F]{4}' \ | grep -oiE '[0-9A-F]{4}' \ | tr 'A-F' 'a-f' \ | sort -u > "$_tmp" if [[ -s "$_tmp" ]]; then mv "$_tmp" "$_cache" else rm -f "$_tmp" fi done while IFS= read -r ver; do [[ -z "$ver" ]] && continue local ver_major="${ver%%.*}" if [[ -n "$target_branch" ]] && (( 10#$ver_major < 10#$target_branch )); then continue fi if is_nvidia_stable_branch "$ver_major" || is_branch_release_count_sufficient "$ver_major"; then if is_branch_compatible_with_host_gpus "$ver_major"; then printf '%s\n' "$ver" fi fi done <<< "$versions_in" } version_le() { local v1="$1" local v2="$2" IFS='.' read -r a1 b1 c1 <<<"$v1" IFS='.' read -r a2 b2 c2 <<<"$v2" a1=${a1:-0}; b1=${b1:-0}; c1=${c1:-0} a2=${a2:-0}; b2=${b2:-0}; c2=${c2:-0} a1=$((10#$a1)); b1=$((10#$b1)); c1=$((10#$c1)) a2=$((10#$a2)); b2=$((10#$b2)); c2=$((10#$c2)) if (( a1 < a2 )); then return 0 elif (( a1 > a2 )); then return 1 fi if (( b1 < b2 )); then return 0 elif (( b1 > b2 )); then return 1 fi if (( c1 <= c2 )); then return 0 else return 1 fi } # ========================================================== # NVIDIA version management - FIXED VERSION # ========================================================== download_latest_version() { local latest_line version latest_line=$(curl -fsSL "${NVIDIA_BASE_URL}/latest.txt" 2>&1) if [[ -z "$latest_line" ]]; then echo "" >&2 return 1 fi version=$(echo "$latest_line" | awk '{print $1}' | tr -d '[:space:]') if [[ -z "$version" ]]; then echo "" >&2 return 1 fi if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then echo "" >&2 return 1 fi echo "$version" return 0 } list_available_versions() { local html_content versions html_content=$(curl -s "$NVIDIA_BASE_URL/" 2>&1) if [[ -z "$html_content" ]]; then echo "" >&2 return 1 fi versions=$(echo "$html_content" \ | grep -o 'href=[^ >]*' \ | awk -F"'" '{print $2}' \ | grep -E '^[0-9]' \ | sed 's/\/$//' \ | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ | sort -Vr \ | uniq) if [[ -z "$versions" ]]; then echo "" >&2 return 1 fi echo "$versions" return 0 } verify_version_exists() { local version="$1" local url="${NVIDIA_BASE_URL}/${version}/" if curl -fsSL --head "$url" >/dev/null 2>&1; then return 0 else return 1 fi } download_nvidia_installer() { ensure_workdir local version="$1" version=$(echo "$version" | tr -d '[:space:]' | tr -d '\n' | tr -d '\r') if [[ ! "$version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then msg_error "Invalid version format: $version" >&2 echo "ERROR: Invalid version format: '$version'" >> "$LOG_FILE" return 1 fi local run_file="$NVIDIA_WORKDIR/NVIDIA-Linux-x86_64-${version}.run" if [[ -f "$run_file" ]]; then echo "Found existing file: $run_file" >> "$LOG_FILE" local existing_size file_type existing_size=$(stat -c%s "$run_file" 2>/dev/null || stat -f%z "$run_file" 2>/dev/null || echo "0") file_type=$(file "$run_file" 2>/dev/null || echo "unknown") echo "Existing file size: $existing_size bytes" >> "$LOG_FILE" echo "Existing file type: $file_type" >> "$LOG_FILE" if [[ $existing_size -gt 40000000 ]] && echo "$file_type" | grep -q "executable"; then if sh "$run_file" --check 2>&1 | tee -a "$LOG_FILE" | grep -q "OK"; then echo "Existing file passed integrity check" >> "$LOG_FILE" msg_ok "$(translate 'Installer already downloaded and verified.')" >&2 printf '%s\n' "$run_file" return 0 else echo "Existing file FAILED integrity check, removing..." >> "$LOG_FILE" msg_warn "$(translate 'Existing file, re-downloading...')" >&2 rm -f "$run_file" fi else echo "Existing file invalid (size or type), removing..." >> "$LOG_FILE" msg_warn "$(translate 'Removing invalid existing file...')" >&2 rm -f "$run_file" fi fi if ! verify_version_exists "$version"; then msg_error "Version $version does not exist on NVIDIA servers" >&2 echo "ERROR: Version $version not found on server" >> "$LOG_FILE" return 1 fi local urls=( "${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}.run" "${NVIDIA_BASE_URL}/${version}/NVIDIA-Linux-x86_64-${version}-no-compat32.run" ) # Web mode (ProxMenux Monitor) runs scripts without a controlling TTY, so # /dev/tty is not writable and progress-bar animations using \r don't render # in the web terminal. Fall back to a quiet wget in that case; interactive # users (SSH / console) still get the ISO-like progress bar. local _nv_has_tty=false if ! is_web_mode 2>/dev/null && [[ -t 2 ]]; then _nv_has_tty=true fi if $_nv_has_tty; then printf '\n %s NVIDIA-Linux-x86_64-%s.run\n' \ "$(translate 'Downloading')" "$version" >/dev/tty else echo " $(translate 'Downloading') NVIDIA-Linux-x86_64-${version}.run" >&2 fi local success=false local url_index=0 for url in "${urls[@]}"; do ((url_index++)) echo "Attempting download from: $url" >> "$LOG_FILE" rm -f "$run_file" local _dl_ok=false if $_nv_has_tty; then # Interactive: progress bar to /dev/tty (bypasses any caller redirection). if wget --no-verbose --show-progress \ --connect-timeout=30 --timeout=600 --tries=1 \ -O "$run_file" "$url" 2>/dev/tty; then _dl_ok=true fi else # Web / no-TTY: silent wget, log errors only. if wget --quiet \ --connect-timeout=30 --timeout=600 --tries=1 \ -O "$run_file" "$url" 2>>"$LOG_FILE"; then _dl_ok=true fi fi if $_dl_ok; then echo "Download completed, verifying file..." >> "$LOG_FILE" if [[ ! -f "$run_file" ]]; then echo "ERROR: File not created after download" >> "$LOG_FILE" continue fi local file_size file_size=$(stat -c%s "$run_file" 2>/dev/null || stat -f%z "$run_file" 2>/dev/null || echo "0") echo "Downloaded file size: $file_size bytes" >> "$LOG_FILE" if [[ $file_size -lt 40000000 ]]; then echo "ERROR: File too small ($file_size bytes, expected >40MB)" >> "$LOG_FILE" head -c 200 "$run_file" >> "$LOG_FILE" 2>&1 rm -f "$run_file" continue fi local file_type file_type=$(file "$run_file" 2>/dev/null) echo "File type: $file_type" >> "$LOG_FILE" if echo "$file_type" | grep -q "executable"; then echo "SUCCESS: Valid executable downloaded" >> "$LOG_FILE" success=true break else echo "ERROR: Not a valid executable" >> "$LOG_FILE" head -c 200 "$run_file" | od -c >> "$LOG_FILE" 2>&1 rm -f "$run_file" fi else echo "ERROR: wget failed for $url (exit code: $?)" >> "$LOG_FILE" rm -f "$run_file" fi done if ! $success; then msg_error "$(translate 'Download failed for all attempted URLs')" >&2 msg_error "Version $version may not be available for your architecture" >&2 echo "ERROR: All download attempts failed" >> "$LOG_FILE" return 1 fi chmod +x "$run_file" echo "Installation file ready: $run_file" >> "$LOG_FILE" printf '%s\n' "$run_file" } # ========================================================== # Installation / uninstallation # ========================================================== run_nvidia_installer() { local installer="$1" msg_info2 "$(translate 'Starting NVIDIA installer. This may take several minutes...')" echo "" >>"$LOG_FILE" echo "=== Running NVIDIA installer: $installer ===" >>"$LOG_FILE" # If nouveau is still loaded, rebuild initramfs first so the blacklist takes # effect for the installer sanity checks. Without this the .run installer # detects nouveau as active and aborts even when --disable-nouveau is passed. if [[ "${NOUVEAU_STILL_LOADED:-false}" == "true" ]]; then msg_info "$(translate 'Rebuilding initramfs to apply nouveau blacklist before installation...')" update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true # Try one more time to unload nouveau after initramfs rebuild modprobe -r nouveau 2>/dev/null || true sleep 1 if grep -q "^nouveau " /proc/modules 2>/dev/null; then echo "WARNING: nouveau still loaded after initramfs rebuild, proceeding with --no-nouveau-check" >> "$LOG_FILE" msg_warn "$(translate 'nouveau still active. Proceeding with installation. A reboot will be required for the driver to work.')" else NOUVEAU_STILL_LOADED=false msg_ok "$(translate 'nouveau module unloaded after initramfs rebuild.')" | tee -a "$screen_capture" fi fi local tmp_extract_dir="$NVIDIA_WORKDIR/tmp_extract" mkdir -p "$tmp_extract_dir" # --no-nouveau-check: prevents the installer from aborting when nouveau is # still loaded. The blacklist files are already in place; nouveau will be # gone after the reboot that the script offers at the end. sh "$installer" \ --tmpdir="$tmp_extract_dir" \ --no-questions \ --ui=none \ --disable-nouveau \ --no-nouveau-check \ --dkms \ 2>&1 | tee -a "$LOG_FILE" local rc=${PIPESTATUS[0]} echo "" >>"$LOG_FILE" rm -rf "$tmp_extract_dir" if [[ $rc -ne 0 ]]; then msg_error "$(translate 'NVIDIA installer reported an error. Check /tmp/nvidia_install.log')" update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}' return 1 fi msg_ok "$(translate 'NVIDIA driver installed successfully.')" | tee -a "$screen_capture" return 0 } remove_nvidia_driver() { complete_nvidia_uninstall } install_udev_rules_and_persistenced() { msg_info "$(translate 'Installing NVIDIA udev rules and persistence service...')" cat >/etc/udev/rules.d/70-nvidia.rules <<'EOF' # /etc/udev/rules.d/70-nvidia.rules KERNEL=="nvidia", RUN+="/bin/bash -c '/usr/bin/nvidia-smi -L'" KERNEL=="nvidia_uvm", RUN+="/bin/bash -c '/usr/bin/nvidia-modprobe -c0 -u'" EOF udevadm control --reload-rules udevadm trigger --subsystem-match=drm --subsystem-match=pci || true ensure_workdir cd "$NVIDIA_WORKDIR" || return 1 # Pin to the last release tag so a hostile push to upstream `master` # can't slip arbitrary code into the install. Bump as needed; the # `--depth 1` keeps the clone fast. Audit Tier 6 — `nvidia-persistenced` # git clone sin pinning de versión. local NVIDIA_PERSISTENCED_TAG="${NVIDIA_PERSISTENCED_TAG:-575.64.05}" if [[ ! -d nvidia-persistenced ]]; then git clone --depth 1 --branch "$NVIDIA_PERSISTENCED_TAG" \ https://github.com/NVIDIA/nvidia-persistenced.git >>"$LOG_FILE" 2>&1 \ || git clone --depth 1 https://github.com/NVIDIA/nvidia-persistenced.git >>"$LOG_FILE" 2>&1 \ || true fi if [[ -d nvidia-persistenced/init ]]; then cd nvidia-persistenced/init || return 1 ./install.sh >>"$LOG_FILE" 2>&1 || true fi msg_ok "$(translate 'NVIDIA udev rules and persistence service installed.')" | tee -a "$screen_capture" } apply_nvidia_patch_if_needed() { # NVIDIA_PATCH_AUTO=yes|no skips the yes/no prompt for non-interactive # callers; unset preserves the interactive menu behavior. case "${NVIDIA_PATCH_AUTO:-}" in yes) : ;; no) msg_info2 "$(translate 'NVIDIA patch not applied.')" update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}' return 0 ;; *) if ! hybrid_whiptail_yesno "$(translate 'NVIDIA Patch')" \ "\n$(translate 'Do you want to apply the optional NVIDIA patch to remove some GPU limitations?')"; then msg_info2 "$(translate 'NVIDIA patch not applied.')" update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}' return 0 fi ;; esac msg_info "$(translate 'Cloning and applying NVIDIA patch (keylase/nvidia-patch)...')" ensure_workdir cd "$NVIDIA_WORKDIR" || return 1 # Pin keylase/nvidia-patch to a known-good commit. Override via env var # for forward-compat as new driver versions land. patch.sh ships a list # of supported drivers in the repo; if our running driver isn't covered # the patch silently no-ops, so we surface a warning before running. # Audit Tier 6 — `keylase/nvidia-patch` sin pinning + sin compat check. local NVIDIA_PATCH_REF="${NVIDIA_PATCH_REF:-master}" if [[ ! -d nvidia-patch ]]; then git clone --depth 1 --branch "$NVIDIA_PATCH_REF" \ https://github.com/keylase/nvidia-patch.git >>"$LOG_FILE" 2>&1 \ || git clone --depth 1 https://github.com/keylase/nvidia-patch.git >>"$LOG_FILE" 2>&1 \ || true fi # Best-effort compatibility check: peek the supported-driver list in # patch.sh and warn if our driver isn't on it. if [[ -n "$CURRENT_DRIVER_VERSION" && -f nvidia-patch/patch.sh ]]; then if ! grep -qF "$CURRENT_DRIVER_VERSION" nvidia-patch/patch.sh 2>/dev/null; then msg_warn "$(translate 'NVIDIA driver') $CURRENT_DRIVER_VERSION $(translate 'is not in the patch.sh supported list. The patch may no-op or fail; review keylase/nvidia-patch README before continuing.')" fi fi if [[ -x nvidia-patch/patch.sh ]]; then cd nvidia-patch || return 1 ./patch.sh >>"$LOG_FILE" 2>&1 || true msg_ok "$(translate 'NVIDIA patch applied - check README for supported versions.')" update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":true}' else msg_warn "$(translate 'Could not run NVIDIA patch script. Please verify repository and driver version.')" update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}' fi } restart_prompt() { if hybrid_whiptail_yesno "$(translate 'NVIDIA Drivers')" \ "\n$(translate 'The installation/changes require a server restart to apply correctly. Do you want to reboot now?')"; then msg_success "$(translate 'Installation completed. Press Enter to continue...')" read -r msg_warn "$(translate 'Restarting the server...')" rm -f "$screen_capture" reboot else msg_success "$(translate 'Installation completed. Please reboot the server manually as soon as possible.')" msg_success "$(translate 'Completed. Press Enter to return to menu...')" read -r rm -f "$screen_capture" fi } # ========================================================== # Dialog menus # ========================================================== show_action_menu_if_installed() { if ! $CURRENT_DRIVER_INSTALLED; then ACTION="install" return 0 fi local menu_choices=( "install" "$(translate 'Reinstall/Update NVIDIA drivers')" "remove" "$(translate 'Uninstall NVIDIA drivers and configuration')" ) ACTION=$(hybrid_menu "ProxMenux" "$(translate 'NVIDIA Actions')\n\n$(translate 'Choose an action:')" 26 80 16 "${menu_choices[@]}") || ACTION="cancel" } show_install_overview() { local overview overview="\n$(translate 'This installation will:')\n\n" overview+=" • $(translate 'Install NVIDIA proprietary drivers')\n" overview+=" • $(translate 'Configure GPU passthrough with VFIO')\n" overview+=" • $(translate 'Blacklist nouveau driver')\n" overview+=" • $(translate 'Enable IOMMU support if not enabled')\n" overview+=" • $(translate 'Optionally update NVIDIA libs in LXC containers with passthrough')\n\n" overview+="$(translate 'Detected GPU(s):')\n" overview+="\Zb\Z4$DETECTED_GPUS_TEXT\Zn\n" overview+="\n\Zn$(translate 'Current status: ') " overview+="\Zb${CURRENT_STATUS_TEXT}\Zn\n" # Scan for LXC containers with NVIDIA passthrough and surface them in the # overview so the user knows upfront they will be offered a driver update. find_nvidia_containers if [[ ${#NVIDIA_CONTAINERS[@]} -gt 0 ]]; then overview+="\n$(translate 'LXC containers with NVIDIA passthrough:')\n" local ctid lxc_ver ct_name for ctid in "${NVIDIA_CONTAINERS[@]}"; do lxc_ver=$(get_lxc_nvidia_version "$ctid") ct_name=$(pct config "$ctid" 2>/dev/null | grep "^hostname:" | awk '{print $2}') overview+=" \Zb\Z4CT ${ctid}\Zn ${ct_name:+(${ct_name})} — $(translate 'driver:') ${lxc_ver}\n" done fi overview+="\n$(translate 'After confirming, you will be asked to choose the NVIDIA driver version to install.')\n\n" overview+="$(translate 'Do you want to continue?')" hybrid_yesno "$(translate 'NVIDIA GPU Driver Installation')" "$overview" 24 90 } 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...')" latest=$(download_latest_version 2>/dev/null) versions_list=$(list_available_versions 2>/dev/null) 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 fi if [[ -z "$latest" ]] && [[ -n "$versions_list" ]]; then latest=$(echo "$versions_list" | head -n1) fi if [[ -n "$latest" ]] && [[ -z "$versions_list" ]]; then versions_list="$latest" fi # Clean latest version latest=$(echo "$latest" | tr -d '[:space:]') local current_list="$versions_list" if [[ -n "$current_list" ]]; then current_list=$(filter_option_c_branch "$current_list" "$CURRENT_DRIVER_VERSION" "") fi # Historically the picker capped candidates at `latest` (from the # CDN's `latest.txt`) so users never saw versions newer than the # global "latest". But latest.txt lags the Production Branch head # (595.91.07 today vs 595.84 in latest.txt) and also hides the New # Feature Branch head (610.x) that is a legitimate option once # kernel + GPU compat pass. Kernel floor, endorsement whitelist, # release-count heuristic and GPU-compat filter already narrow the # list to safe candidates; the Production head still stands out in # the "Latest available" recommendation, so an artificial ceiling # only masked valid options. # If the user has the keylase NVENC patch applied, only offer versions # that the patch supports — picking an unsupported version reinstalls # the driver fine but the patch silently no-ops afterwards, so the # user loses NVENC limit removal without warning. local patch_filtered=false local patch_filter_note="" if is_current_nvidia_patched && [[ -n "$current_list" ]]; then if refresh_keylase_patch_cache; then local trimmed trimmed=$(filter_keylase_supported "$current_list") if [[ -n "$trimmed" ]]; then current_list="$trimmed" patch_filtered=true else patch_filter_note="$(translate 'No version in this branch is currently supported by keylase/nvidia-patch — the NVENC patch will not reapply after reinstall.')" fi else patch_filter_note="$(translate 'Could not fetch keylase/nvidia-patch supported list — patch reapply compatibility is not verified.')" fi fi # Pick the default "Recommended" version. Three-tier priority so the # picker stays consistent with what the Monitor's driver-update # notification promised the user: # 1. If a driver is already installed AND its branch is still # offered in the filtered list, recommend the highest release # of that same branch (bugfix upgrade in place). Matches the # Monitor's Hardware card, which surfaces "v580.178.04 # available" for a 580.x install — the user hitting Actualizar # then expects to land on 580.178.04, not a cross-branch jump # to Production. Cross-branch is still one row away in the # list. # 2. Fresh install (no current driver) → Production Branch head # from NVIDIA's Unix drivers page, when present in the list. # 3. Fallback → highest numeric in the list (Production may have # been filtered out by maintained-branch / GPU PCI-ID / patch # awareness). latest="" if [[ -n "$CURRENT_DRIVER_VERSION" && -n "$current_list" ]]; then local _cur_branch="${CURRENT_DRIVER_VERSION%%.*}" if [[ -n "$_cur_branch" ]]; then local _same_branch_head _same_branch_head=$(printf '%s\n' "$current_list" \ | awk -F. -v b="$_cur_branch" '$1 == b { print; exit }' \ | tr -d '[:space:]') if [[ -n "$_same_branch_head" ]]; then latest="$_same_branch_head" fi fi fi if [[ -z "$latest" ]]; then local prod_head="" prod_head=$(get_nvidia_production_head 2>/dev/null) || prod_head="" if [[ -n "$prod_head" && -n "$current_list" ]]; then if printf '%s\n' "$current_list" | grep -qFx "$prod_head"; then latest="$prod_head" fi fi fi if [[ -z "$latest" && -n "$current_list" ]]; then latest=$(printf '%s\n' "$current_list" | head -n1 | tr -d '[:space:]') fi 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. 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 menu_text+="\n\n$(translate 'NVENC patch detected — list narrowed to versions supported by keylase/nvidia-patch.')" elif [[ -n "$patch_filter_note" ]]; then menu_text+="\n\n${patch_filter_note}" fi local choices=() if [[ -n "$latest" ]]; then choices+=("$latest" "$latest — $(translate 'Recommended')") else choices+=("" "$(translate 'Recommended')") fi choices+=("" "") if [[ -n "$current_list" ]]; then while IFS= read -r ver; do [[ -z "$ver" ]] && continue ver=$(echo "$ver" | tr -d '[:space:]') [[ -z "$ver" ]] && continue choices+=("$ver" "$ver") done <<< "$current_list" else choices+=("" "$(translate 'No supported NVIDIA versions found for this GPU')") fi stop_spinner local selection=$(hybrid_menu "$(translate 'NVIDIA Driver Version')" "$menu_text" 26 90 16 "${choices[@]}") || { DRIVER_VERSION="cancel"; return 1; } case "$selection" in "") DRIVER_VERSION="cancel" return 1 ;; *) DRIVER_VERSION=$(echo "$selection" | tr -d '[:space:]') return 0 ;; esac } # ========================================================== # Main flow # ========================================================== main() { # Rotate the previous run's log instead of truncating — when the # current install fails, the user can compare against the previous # attempt to see what changed. Audit Tier 7 — log truncation. if [[ -f "$LOG_FILE" && -s "$LOG_FILE" ]]; then cp -p "$LOG_FILE" "${LOG_FILE}.prev" 2>/dev/null || true fi : >"$LOG_FILE" : >"$screen_capture" NOUVEAU_STILL_LOADED=false detect_nvidia_gpus detect_driver_status check_gpu_not_in_vm_passthrough check_stale_vfio_config_for_nvidia if ! $NVIDIA_GPU_PRESENT; then dialog --backtitle "ProxMenux" --title "$(translate 'NVIDIA GPU Driver Installation')" --msgbox \ "\n$(translate 'No NVIDIA GPU has been detected on this system. The installer will now exit.')" 20 70 exit 1 fi show_action_menu_if_installed case "$ACTION" in install) if ! show_install_overview; then exit 0 fi get_system_info show_version_menu if [[ "$DRIVER_VERSION" == "cancel" || -z "$DRIVER_VERSION" ]]; then exit 0 fi if $CURRENT_DRIVER_INSTALLED; then if [[ "$CURRENT_DRIVER_VERSION" == "$DRIVER_VERSION" ]]; then local confirm_text confirm_text="\n\n\n$(translate 'Version') \Zb\Z4$DRIVER_VERSION\Zn\n\n$(translate 'is already installed. Do you want to reinstall it? This will perform a clean uninstall first.')" if ! hybrid_yesno "$(translate 'Same Version Detected')" "$confirm_text" 14 70; then exit 0 fi else local confirm_text confirm_text="\n\n$(translate 'Current version:') \Zb$CURRENT_DRIVER_VERSION\Zn\n" confirm_text+="$(translate 'New version:') \Zb\Z4$DRIVER_VERSION\Zn\n\n" confirm_text+="$(translate 'The current driver will be completely uninstalled before installing the new version. Continue?')" if ! hybrid_yesno "$(translate 'Version Change Detected')" "$confirm_text" 20 70; then exit 0 fi fi show_proxmenux_logo msg_title "$(translate "$SCRIPT_TITLE")" msg_info2 "$(translate 'Uninstalling current NVIDIA driver before installing new version')" complete_nvidia_uninstall sleep 2 CURRENT_DRIVER_INSTALLED=false CURRENT_DRIVER_VERSION="" fi show_proxmenux_logo msg_title "$(translate "$SCRIPT_TITLE")" ensure_repos_and_headers blacklist_nouveau ensure_modules_config stop_and_disable_nvidia_services unload_nvidia_modules local installer installer=$(download_nvidia_installer "$DRIVER_VERSION") local download_result=$? if [[ $download_result -ne 0 ]]; then msg_error "$(translate 'Failed to download NVIDIA installer')" exit 1 fi msg_ok "$(translate 'NVIDIA installer downloaded successfully')" | tee -a "$screen_capture" if [[ -z "$installer" || ! -f "$installer" ]]; then msg_error "$(translate 'Internal error: NVIDIA installer path is empty or file not found.')" rm -f "$screen_capture" exit 1 fi if ! run_nvidia_installer "$installer"; then rm -f "$screen_capture" exit 1 fi sleep 2 show_proxmenux_logo msg_title "$(translate "$SCRIPT_TITLE")" cat "$screen_capture" echo -e "${TAB}${GN}📄 $(translate "Log file")${CL}: ${BL}$LOG_FILE${CL}" install_udev_rules_and_persistenced msg_info "$(translate 'Updating initramfs for all kernels...')" update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true msg_ok "$(translate 'initramfs updated.')" msg_info2 "$(translate 'Checking NVIDIA driver status with nvidia-smi')" if command -v nvidia-smi >/dev/null 2>&1; then nvidia-smi || true CURRENT_DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1) CURRENT_DRIVER_INSTALLED=true else msg_warn "$(translate 'nvidia-smi not found in PATH. Please verify the driver installation.')" fi if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then msg_ok "$(translate 'NVIDIA driver') $CURRENT_DRIVER_VERSION $(translate 'installed successfully.')" update_component_status "nvidia_driver" "installed" "$CURRENT_DRIVER_VERSION" "gpu" '{"patched":false}' msg_success "$(translate 'Driver installed successfully. Press Enter to continue...')" read -r else msg_error "$(translate 'Failed to detect installed NVIDIA driver version.')" update_component_status "nvidia_driver" "failed" "" "gpu" '{"patched":false}' fi # Propagate the new driver to LXC containers with NVIDIA passthrough, if any. # Uses the same .run installer cached in $NVIDIA_WORKDIR — runs only if the # host install succeeded and the user confirms. if [[ -n "$CURRENT_DRIVER_VERSION" ]]; then offer_lxc_updates_if_any "$CURRENT_DRIVER_VERSION" fi apply_nvidia_patch_if_needed restart_prompt ;; remove) if hybrid_yesno "$(translate 'NVIDIA Driver Uninstall')" \ "\n\n\n$(translate 'This will remove NVIDIA drivers and related configuration. Do you want to continue?')" 14 70; then show_proxmenux_logo msg_title "$(translate "$SCRIPT_TITLE")" remove_nvidia_driver msg_info "$(translate 'Updating initramfs for all kernels...')" update-initramfs -u -k all >>"$LOG_FILE" 2>&1 || true proxmox-boot-tool refresh >>"$LOG_FILE" 2>&1 || true msg_ok "$(translate 'initramfs updated.')" restart_prompt fi ;; cancel|*) exit 0 ;; esac } # ========================================================== # Non-interactive auto-reinstall entry point # ========================================================== # Invoked after a host-config restore by apply_cluster_postboot.sh # when components_status.json reports nvidia_driver as installed # but the kernel module isn't loaded on the live system (i.e. the # restore brought back the configs but not the binary driver from # /lib/modules//). Replays the install path the user # originally ran via `menu → 2`, using the recorded version, with # no dialogs. # # Exit codes: # 0 installed (or no-op — GPU absent / driver already present) # 1 state file unreadable or no nvidia_driver entry # 2 install failed auto_reinstall_from_state() { : >"$LOG_FILE" echo "=== auto_reinstall_from_state started $(date -Iseconds) ===" >>"$LOG_FILE" if ! command -v jq >/dev/null 2>&1; then echo "jq not available — cannot read components_status.json" | tee -a "$LOG_FILE" return 1 fi if [[ ! -f "$COMPONENTS_STATUS_FILE" ]]; then echo "No components_status.json at $COMPONENTS_STATUS_FILE" | tee -a "$LOG_FILE" return 1 fi local recorded_status recorded_version recorded_patched recorded_status=$(jq -r '.nvidia_driver.status // ""' "$COMPONENTS_STATUS_FILE" 2>/dev/null) recorded_version=$(jq -r '.nvidia_driver.version // ""' "$COMPONENTS_STATUS_FILE" 2>/dev/null) recorded_patched=$(jq -r '.nvidia_driver.patched // false' "$COMPONENTS_STATUS_FILE" 2>/dev/null) if [[ "$recorded_status" != "installed" ]]; then echo "nvidia_driver not marked installed in state ($recorded_status) — nothing to do" | tee -a "$LOG_FILE" return 0 fi if [[ -z "$recorded_version" || "$recorded_version" == "null" ]]; then echo "nvidia_driver marked installed but no version recorded — aborting" | tee -a "$LOG_FILE" return 1 fi echo "Recorded driver: $recorded_version (patched=$recorded_patched)" >>"$LOG_FILE" detect_nvidia_gpus if ! $NVIDIA_GPU_PRESENT; then echo "No NVIDIA GPU detected on this host — skipping reinstall" | tee -a "$LOG_FILE" return 0 fi # Skip when the host is configured for VFIO passthrough. Two # signals — config wins over runtime: # # 1. modprobe.d declares the nvidia* modules blacklisted (the # file ProxMenux drops when switching the GPU to VM mode is # `proxmenux-nvidia-vfio-blacklist.conf`, but any blacklist # file counts). # 2. The PCI device is bound to vfio-pci. # # The config check has to come FIRST because right after a host # restore + reboot, the binding may not yet be effective (the # GPU temporarily shows "no driver" while udev/initramfs settle). # Reinstalling NVIDIA in that window forces the module onto a # PCI device vfio-pci has already reserved → "NVRM: Try unloading # the conflicting kernel module" and exit 1. if [[ -f /etc/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf ]] \ || grep -qrhE '^[[:space:]]*blacklist[[:space:]]+nvidia([[:space:]_]|$)' \ /etc/modprobe.d/ 2>/dev/null; then echo "NVIDIA blacklisted (host is VFIO-configured) — skipping host driver reinstall" \ | tee -a "$LOG_FILE" return 0 fi local _vfio_dev for _vfio_dev in /sys/bus/pci/devices/*; do [[ "$(cat "$_vfio_dev/vendor" 2>/dev/null)" == "0x10de" ]] || continue if [[ -L "$_vfio_dev/driver" ]] \ && [[ "$(basename "$(readlink "$_vfio_dev/driver")")" == "vfio-pci" ]]; then echo "NVIDIA GPU bound to vfio-pci — skipping host driver reinstall" | tee -a "$LOG_FILE" return 0 fi done detect_driver_status if $CURRENT_DRIVER_INSTALLED && [[ "$CURRENT_DRIVER_VERSION" == "$recorded_version" ]]; then echo "Driver $recorded_version already installed and matches state — no-op" | tee -a "$LOG_FILE" return 0 fi DRIVER_VERSION="$recorded_version" # Same install path as the interactive main() flow, minus all # dialogs and confirmations. echo "Reinstalling NVIDIA driver $DRIVER_VERSION non-interactively..." | tee -a "$LOG_FILE" ensure_workdir ensure_repos_and_headers >>"$LOG_FILE" 2>&1 blacklist_nouveau >>"$LOG_FILE" 2>&1 ensure_modules_config >>"$LOG_FILE" 2>&1 if $CURRENT_DRIVER_INSTALLED; then echo "Different version currently installed; cleaning up first..." | tee -a "$LOG_FILE" complete_nvidia_uninstall >>"$LOG_FILE" 2>&1 fi local installer installer=$(download_nvidia_installer "$DRIVER_VERSION" 2>>"$LOG_FILE") if [[ -z "$installer" || ! -f "$installer" ]]; then echo "Download failed — see $LOG_FILE" | tee -a "$LOG_FILE" return 2 fi echo "Installer ready: $installer" >>"$LOG_FILE" if ! run_nvidia_installer "$installer" >>"$LOG_FILE" 2>&1; then echo "Install failed — see $LOG_FILE" | tee -a "$LOG_FILE" return 2 fi install_udev_rules_and_persistenced >>"$LOG_FILE" 2>&1 # Preserve the recorded patched state across the reinstall. The patch # helper writes its own update_component_status on success. CURRENT_DRIVER_VERSION="$DRIVER_VERSION" if [[ "$recorded_patched" == "true" ]]; then echo "Recorded state had patched=true — re-applying NVIDIA patch..." | tee -a "$LOG_FILE" NVIDIA_PATCH_AUTO=yes apply_nvidia_patch_if_needed >>"$LOG_FILE" 2>&1 || true else if declare -F update_component_status >/dev/null 2>&1; then update_component_status "nvidia_driver" "installed" "$DRIVER_VERSION" "gpu" '{"patched":false}' >>"$LOG_FILE" 2>&1 fi fi echo "✓ NVIDIA driver $DRIVER_VERSION reinstalled" | tee -a "$LOG_FILE" return 0 } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then if [[ "${1:-}" == "--auto-reinstall" ]]; then auto_reinstall_from_state exit $? fi main fi