diff --git a/pve-mod-gui-nvidia.sh b/pve-mod-gui-nvidia.sh new file mode 100644 index 0000000..07a9365 --- /dev/null +++ b/pve-mod-gui-nvidia.sh @@ -0,0 +1,545 @@ +#!/usr/bin/env bash +# +# This bash script installs a modification to the Proxmox Virtual Environment (PVE) +# web user interface (UI) to display NVIDIA GPU information. +# +# Author: Based on pve-mod-gui-sensors.sh by Meliox +# License: MIT +# + +################### Configuration ############# + +# Temperature thresholds (Celsius) +TEMP_WARNING=70 +TEMP_CRITICAL=85 + +# Overwrite default backup location (leave empty for default ~/PVE-MODS) +BACKUP_DIR="" + +##################### DO NOT EDIT BELOW ####################### + +# This script's working directory +SCRIPT_CWD="$(dirname "$(readlink -f "$0")")" + +# File paths +PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js" +NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm" + +#region message tools +# Section header (bold) +function msgb() { + local message="$1" + echo -e "\e[1m${message}\e[0m" +} + +# Info (green) +function info() { + local message="$1" + echo -e "\e[0;32m[info] ${message}\e[0m" +} + +# Warning (yellow) +function warn() { + local message="$1" + echo -e "\e[0;33m[warning] ${message}\e[0m" +} + +# Error (red) +function err() { + local message="$1" + echo -e "\e[0;31m[error] ${message}\e[0m" + exit 1 +} + +# Prompts (cyan) +function ask() { + local prompt="$1" + local response + read -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response + echo "$response" +} +#endregion message tools + +# Function to display usage information +function usage { + msgb "\nUsage:\n$0 [install | uninstall]\n" + msgb "Options:" + echo " install Install the NVIDIA GPU monitoring modification" + echo " uninstall Remove the modification and restore original files" + echo "" + exit 1 +} + +# System checks +function check_root_privileges() { + if [[ $EUID -ne 0 ]]; then + err "This script must be run as root. Please run it with 'sudo $0'." + fi + info "Root privileges verified." +} + +# Check if nvidia-smi is available +function check_nvidia_smi() { + if ! command -v nvidia-smi &>/dev/null; then + err "nvidia-smi is not installed or not in PATH. Please install NVIDIA drivers first." + fi + info "nvidia-smi found." +} + +# Detect NVIDIA GPUs +function detect_gpus() { + local gpu_count + gpu_count=$(nvidia-smi --query-gpu=count --format=csv,noheader,nounits 2>/dev/null | head -1) + + if [[ -z "$gpu_count" ]] || [[ "$gpu_count" -eq 0 ]]; then + err "No NVIDIA GPUs detected by nvidia-smi." + fi + + echo "$gpu_count" +} + +# Configure installation options +function configure() { + msgb "\n=== Detecting NVIDIA GPUs ===" + + check_nvidia_smi + + local gpu_count + gpu_count=$(detect_gpus) + + info "Detected $gpu_count NVIDIA GPU(s):" + + # Display detected GPUs + nvidia-smi --query-gpu=index,name --format=csv,noheader 2>/dev/null | while read -r line; do + echo " GPU $line" + done + + # Temperature unit selection + msgb "\n=== Display Settings ===" + local unit + unit=$(ask "Display temperatures in Celsius [C] or Fahrenheit [f]? (C/f)") + case "$unit" in + [fF]) + TEMP_UNIT="F" + info "Using Fahrenheit." + ;; + *) + TEMP_UNIT="C" + info "Using Celsius." + ;; + esac +} + +# Function to check if the modification is already installed +function check_mod_installation() { + if grep -q 'nvidiaGpuOutput' "$NODES_PM_FILE" 2>/dev/null; then + err "NVIDIA GPU mod is already installed. Please uninstall first before reinstalling." + fi +} + +# Set backup directory +function set_backup_directory() { + if [[ -z "$BACKUP_DIR" ]]; then + BACKUP_DIR="$HOME/PVE-MODS" + info "Using default backup directory: $BACKUP_DIR" + else + if [[ ! -d "$BACKUP_DIR" ]]; then + err "The specified backup directory does not exist: $BACKUP_DIR" + fi + info "Using custom backup directory: $BACKUP_DIR" + fi +} + +# Create backup directory +function create_backup_directory() { + set_backup_directory + + if [[ ! -d "$BACKUP_DIR" ]]; then + mkdir -p "$BACKUP_DIR" 2>/dev/null || { + err "Failed to create backup directory: $BACKUP_DIR. Please check permissions." + } + info "Created backup directory: $BACKUP_DIR" + else + info "Backup directory already exists: $BACKUP_DIR" + fi +} + +# Create file backup +function create_file_backup() { + local source_file="$1" + local timestamp="$2" + local filename + + filename=$(basename "$source_file") + local backup_file="$BACKUP_DIR/nvidia-gpu.${filename}.$timestamp" + + [[ -f "$source_file" ]] || err "Source file does not exist: $source_file" + [[ -r "$source_file" ]] || err "Cannot read source file: $source_file" + + cp "$source_file" "$backup_file" || err "Failed to create backup: $backup_file" + + # Verify backup integrity + if ! cmp -s "$source_file" "$backup_file"; then + err "Backup verification failed for: $backup_file" + fi + + info "Created backup: $backup_file" +} + +# Perform backup of files +function perform_backup() { + local timestamp + timestamp=$(date +%Y%m%d_%H%M%S) + + msgb "\n=== Creating backups of modified files ===" + + create_backup_directory + create_file_backup "$NODES_PM_FILE" "$timestamp" + create_file_backup "$PVE_MANAGER_LIB_JS_FILE" "$timestamp" +} + +# Restart pveproxy service +function restart_proxy() { + info "Restarting PVE proxy..." + systemctl restart pveproxy +} + +# Insert NVIDIA GPU data collection into Nodes.pm +function insert_node_info() { + msgb "\n=== Inserting NVIDIA GPU data retrieval code ===" + + local nvidia_cmd='nvidia-smi --query-gpu=index,name,temperature.gpu,utilization.gpu,utilization.memory,memory.used,memory.total,power.draw,power.limit,fan.speed --format=csv,noheader,nounits 2>/dev/null' + + # Insert the nvidia-smi command into Nodes.pm + # This adds a new field to the API response + sed -i "/my \$dinfo = df('\/', 1);/i\\ +# Collect NVIDIA GPU data\\ +\$res->{nvidiaGpuOutput} = \`$nvidia_cmd\`;\\ +" "$NODES_PM_FILE" + + if [[ $? -ne 0 ]]; then + err "Failed to insert NVIDIA GPU retrieval code into Nodes.pm" + fi + + info "NVIDIA GPU retriever added to \"$NODES_PM_FILE\"." +} + +# Insert GPU widget before the 'cpus' item in pvemanagerlib.js +function insert_gpu_widget() { + msgb "\n=== Inserting GPU widget into UI ===" + + local temp_js_file="/tmp/nvidia_gpu_widget.js" + + # Generate the GPU widget JavaScript code with proper indentation + # Indentation matches the pvemanagerlib.js style (tab-based) + cat > "$temp_js_file" << 'WIDGET_EOF' + { + xtype: 'box', + colspan: 2, + html: gettext('GPU(s)'), + }, + { + itemId: 'nvidiaGpu', + colspan: 2, + printBar: false, + title: gettext('NVIDIA GPU Status'), + iconCls: 'fa fa-fw fa-television', + textField: 'nvidiaGpuOutput', + renderer: function(value) { + if (!value || value.trim() === '') { + return '
No NVIDIA GPUs detected or nvidia-smi not available
'; + } + + // Temperature conversion settings +WIDGET_EOF + + # Add temperature settings based on user selection + if [[ "$TEMP_UNIT" == "F" ]]; then + cat >> "$temp_js_file" << 'WIDGET_EOF' + var toFahrenheit = true; + var tempUnit = '°F'; + var tempWarning = 158; // 70°C in F + var tempCritical = 185; // 85°C in F +WIDGET_EOF + else + cat >> "$temp_js_file" << 'WIDGET_EOF' + var toFahrenheit = false; + var tempUnit = '°C'; + var tempWarning = 70; + var tempCritical = 85; +WIDGET_EOF + fi + + # Continue with the rest of the widget code + cat >> "$temp_js_file" << 'WIDGET_EOF' + + function convertTemp(celsius) { + if (toFahrenheit) { + return (celsius * 9 / 5) + 32; + } + return celsius; + } + + function formatTemp(celsius) { + var temp = convertTemp(celsius); + var style = ''; + var convertedWarning = toFahrenheit ? tempWarning : 70; + var convertedCritical = toFahrenheit ? tempCritical : 85; + + if (temp >= convertedCritical) { + style = 'color: #ff4444; font-weight: bold;'; + } else if (temp >= convertedWarning) { + style = 'color: #FFC300; font-weight: bold;'; + } + return '' + temp.toFixed(0) + tempUnit + ''; + } + + function formatMemory(used, total) { + var percent = (used / total * 100).toFixed(1); + var style = ''; + if (percent >= 90) { + style = 'color: #ff4444; font-weight: bold;'; + } else if (percent >= 75) { + style = 'color: #FFC300;'; + } + return '' + used.toLocaleString() + ' of ' + total.toLocaleString() + ' MiB (' + percent + '%)'; + } + + function formatPower(draw, limit) { + // Handle NaN power draw (some GPUs don't report power) + if (isNaN(draw)) { + draw = 0; + } + if (isNaN(limit) || limit === 0) { + return '' + draw.toFixed(0) + 'W'; + } + var percent = (draw / limit * 100); + var style = ''; + if (percent >= 90) { + style = 'color: #ff4444; font-weight: bold;'; + } else if (percent >= 75) { + style = 'color: #FFC300;'; + } + return '' + draw.toFixed(0) + ' of ' + limit.toFixed(0) + 'W'; + } + + function formatUtilization(util) { + var style = ''; + if (util >= 90) { + style = 'color: #FFC300;'; + } + return '' + util + '%'; + } + + function formatFan(fan) { + if (fan === null || fan === undefined || isNaN(fan) || fan === '[Not Supported]' || fan === '') { + return 'N/A'; + } + return fan + '%'; + } + + var lines = value.trim().split('\n'); + var result = []; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line) continue; + + var parts = line.split(',').map(function(p) { return p.trim(); }); + + if (parts.length < 10) continue; + + var gpuIndex = parts[0]; + var gpuName = parts[1]; + var temp = parseFloat(parts[2]); + var gpuUtil = parseInt(parts[3], 10); + var memUtil = parseInt(parts[4], 10); + var memUsed = parseFloat(parts[5]); + var memTotal = parseFloat(parts[6]); + var powerDraw = parseFloat(parts[7]); + var powerLimit = parseFloat(parts[8]); + var fanSpeed = parts[9]; + + // Parse fan speed (may be [Not Supported] or a number) + var fanValue = parseFloat(fanSpeed); + if (isNaN(fanValue)) { + fanValue = null; + } + + var gpuHtml = '
'; + gpuHtml += '
GPU ' + gpuIndex + ': ' + gpuName + '
'; + gpuHtml += '
'; + gpuHtml += 'Temp: ' + formatTemp(temp); + gpuHtml += '  |  GPU: ' + formatUtilization(gpuUtil); + gpuHtml += '  |  Mem: ' + formatMemory(memUsed, memTotal); + gpuHtml += '  |  Power: ' + formatPower(powerDraw, powerLimit); + gpuHtml += '  |  Fan: ' + formatFan(fanValue); + gpuHtml += '
'; + + result.push(gpuHtml); + } + + if (result.length === 0) { + return '
No GPU data available
'; + } + + return '
' + result.join('') + '
'; + } + }, +WIDGET_EOF + + if [[ $? -ne 0 ]]; then + err "Failed to generate GPU widget code" + fi + + # Insert the widget BEFORE the 'cpus' item + # We use perl for reliable multi-line pattern matching and insertion + # The BEGIN block reads the widget file before processing the main file + perl -i -0777 -pe ' + BEGIN { + open(my $fh, "<", "/tmp/nvidia_gpu_widget.js") or die "Cannot open widget file: $!"; + local $/; + $::widget = <$fh>; + close($fh); + } + # Find the cpus item within StatusView and insert widget before it + s/(Ext\.define\('\''PVE\.node\.StatusView'\''.*?items:\s*\[.*?)({\s*itemId:\s*'\''cpus'\'')/$1$::widget$2/s; + ' "$PVE_MANAGER_LIB_JS_FILE" + + local insert_status=$? + + # Verify the insertion succeeded + if [[ $insert_status -ne 0 ]]; then + rm -f "$temp_js_file" + err "Failed to insert GPU widget into pvemanagerlib.js (perl error)" + fi + + # Verify the widget was actually inserted by checking for our itemId + if ! grep -q "itemId: 'nvidiaGpu'" "$PVE_MANAGER_LIB_JS_FILE"; then + rm -f "$temp_js_file" + err "Widget insertion verification failed - nvidiaGpu itemId not found in file" + fi + + rm -f "$temp_js_file" + info "GPU widget inserted into \"$PVE_MANAGER_LIB_JS_FILE\"." +} + +# Main installation function +function install_mod() { + msgb "\n=== Preparing NVIDIA GPU mod installation ===" + + check_root_privileges + check_mod_installation + configure + perform_backup + + insert_node_info + insert_gpu_widget + + msgb "\n=== Finalizing installation ===" + + restart_proxy + + info "Installation completed successfully." + msgb "\nIMPORTANT: Clear your browser cache (Ctrl+Shift+R) to see the changes." +} + +# Uninstall the modification +function uninstall_mod() { + msgb "\n=== Uninstalling NVIDIA GPU Mod ===" + + check_root_privileges + + # Check if mod is installed + if ! grep -q 'nvidiaGpuOutput' "$NODES_PM_FILE" 2>/dev/null; then + err "NVIDIA GPU mod is not installed." + fi + + set_backup_directory + + # Check for other mods that would be affected by backup restoration + local other_mods_detected=false + local detected_mods="" + + if grep -q 'sensorsOutput' "$NODES_PM_FILE" 2>/dev/null; then + other_mods_detected=true + detected_mods="pve-mod-gui-sensors" + fi + + if [[ "$other_mods_detected" == true ]]; then + warn "Other PVE mods detected: $detected_mods" + warn "Restoring from backup will remove ALL mods installed after the nvidia-gpu backup was created." + msgb "\nYou have two options:" + echo " 1) Continue - Restore backup, then reinstall other mods afterward" + echo " 2) Cancel - Manually remove nvidia-gpu code from files instead" + echo "" + local confirm + confirm=$(ask "Continue with backup restoration? (y/N)") + if [[ ! "$confirm" =~ ^[yY]$ ]]; then + info "Uninstall cancelled." + msgb "\nTo manually remove, edit these files:" + echo " - $NODES_PM_FILE (remove nvidiaGpuOutput lines)" + echo " - $PVE_MANAGER_LIB_JS_FILE (remove nvidiaGpu widget)" + exit 0 + fi + fi + + info "Restoring modified files..." + + # Find the latest Nodes.pm backup + local latest_nodes_pm + latest_nodes_pm=$(find "$BACKUP_DIR" -name "nvidia-gpu.Nodes.pm.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') + + if [[ -n "$latest_nodes_pm" ]]; then + msgb "Restoring Nodes.pm from backup: $latest_nodes_pm" + cp "$latest_nodes_pm" "$NODES_PM_FILE" + info "Restored Nodes.pm successfully." + else + warn "No Nodes.pm backup found. Attempting manual removal..." + # Remove the nvidia-smi lines manually + sed -i '/# Collect NVIDIA GPU data/,/nvidiaGpuOutput.*nvidia-smi/d' "$NODES_PM_FILE" + fi + + # Find the latest pvemanagerlib.js backup + local latest_pvemanagerlibjs + latest_pvemanagerlibjs=$(find "$BACKUP_DIR" -name "nvidia-gpu.pvemanagerlib.js.*" -type f -printf '%T+ %p\n' 2>/dev/null | sort -r | head -n 1 | awk '{print $2}') + + if [[ -n "$latest_pvemanagerlibjs" ]]; then + msgb "Restoring pvemanagerlib.js from backup: $latest_pvemanagerlibjs" + cp "$latest_pvemanagerlibjs" "$PVE_MANAGER_LIB_JS_FILE" + info "Restored pvemanagerlib.js successfully." + else + warn "No pvemanagerlib.js backup found. Manual restoration may be required." + warn "You can reinstall pve-manager package to restore: apt install --reinstall pve-manager" + fi + + restart_proxy + + info "Uninstallation completed." + msgb "\nIMPORTANT: Clear your browser cache (Ctrl+Shift+R) to see the changes." +} + +# Process command line arguments +executed=0 +while [[ $# -gt 0 ]]; do + case "$1" in + install) + executed=$((executed + 1)) + install_mod + ;; + uninstall) + executed=$((executed + 1)) + uninstall_mod + ;; + *) + warn "Unknown option: $1" + usage + ;; + esac + shift +done + +# If no arguments provided, show usage +if [[ $executed -eq 0 ]]; then + usage +fi diff --git a/pve-mod-nvidia.png b/pve-mod-nvidia.png new file mode 100644 index 0000000..4a39f26 Binary files /dev/null and b/pve-mod-nvidia.png differ diff --git a/readme.md b/readme.md index 45ca572..a062945 100644 --- a/readme.md +++ b/readme.md @@ -49,6 +49,52 @@ bash pve-mod-gui-sensors.sh install ``` Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information. +## NVIDIA GPU readings view +![Proxmox NVIDIA GPU mod](pve-mod-nvidia.png) + +This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display NVIDIA GPU information in the node status view. + +The following readings are displayed (per GPU): +- GPU name and index +- Temperature (Celsius/Fahrenheit) +- GPU utilization +- Memory utilization, used/total +- Power draw / power limit +- Fan speed (if supported) + +### How it works +The modification involves the following steps: +1. Backup original files in a backup directory (default: `~/PVE-MODS`, configurable via `BACKUP_DIR` in the script) + - `/usr/share/pve-manager/js/pvemanagerlib.js` + - `/usr/share/perl5/PVE/API2/Nodes.pm` +2. Patch `Nodes.pm` to add an API field (`nvidiaGpuOutput`) populated by `nvidia-smi`. +3. Modify `pvemanagerlib.js` to insert a new StatusView widget (“NVIDIA GPU Status”) before the CPU widget. +4. Restart the `pveproxy` service to apply changes. + +The script provides two options: +| **Option** | **Description** | +|-------------|------------------| +| `install` | Apply the modification. | +| `uninstall` | Restore original files from backups (see note below). | + +Notes: +- Requires NVIDIA drivers installed on the Proxmox host (`nvidia-smi` must be available). +- If you have other PVE UI mods installed (e.g. the sensors UI mod), uninstalling via backup restore may revert other changes depending on backup order. The script will warn if it detects other mods. +- Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required. + +### Install +Instructions be performed as 'root', as normal users do not have access to the files. + +``` +# Ensure NVIDIA drivers are installed and nvidia-smi works. +# (Example check) +nvidia-smi + +wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/pve-mod-gui-nvidia.sh +bash pve-mod-gui-nvidia.sh install +# Then clear the browser cache to ensure all changes are visualized. +``` + ## Nag screen deactivation (Tested compatibility: 7.x - 8.3.5) This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen.