#!/usr/bin/env bash # # pve-mod-configure - Interactive configuration tool for pve-mod # Detects hardware, asks the user which features to enable, writes # /etc/pve-mod/pve-mod.conf, and applies patches to PVE system files. set -euo pipefail CONF_FILE="/etc/pve-mod/pve-mod.conf" CONFD_DIR="/etc/pve-mod/conf.d" NODE_INFO_CONF="${CONFD_DIR}/node_info.conf" NAG_SCREEN_CONF="${CONFD_DIR}/nag_screen.conf" APPLY_PATCHES="/usr/lib/pve-mod/apply-patches.sh" NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-") #region helpers msgb() { echo -e "\e[1m${1}\e[0m"; } info() { echo -e "\e[0;32m[info] ${1}\e[0m"; } warn() { echo -e "\e[0;33m[warning] ${1}\e[0m"; } err() { echo -e "\e[0;31m[error] ${1}\e[0m"; exit 1; } ask() { local prompt="$1" response read -r -p $'\n\e[1;36m'"${prompt}:"$'\e[0m ' response echo "$response" } bool() { [[ "$1" == true ]] && echo 1 || echo 0; } _load_conf() { [[ -f "$NODE_INFO_CONF" ]] || return 0 local in_debug=0 line key val while IFS= read -r line; do case "$line" in '#'*|'') continue ;; '['*']') section="${line#[}"; section="${section%]}"; continue ;; esac [[ "$line" == *=* ]] || continue key="${line%%=*}"; val="${line#*=}" case "${section}.${key}" in # [modules] modules.node_info) MOD_NODE_INFO="$val" ;; modules.nag_screen) MOD_NAG_SCREEN="$val" ;; # [gpu] gpu.intel_enabled) ENABLE_INTEL_GPU_INFO="$val" ;; gpu.nvidia_enabled) ENABLE_NVIDIA_GPU_INFO="$val" ;; gpu.amd_enabled) ENABLE_AMD_GPU_INFO="$val" ;; gpu.gpu_history) ENABLE_GPU_HISTORY="$val" ;; # [lm_sensors] lm_sensors.enabled) LM_SENSORS_ENABLED="$val" ;; lm_sensors.enable_cpu) ENABLE_CPU="$val" ;; lm_sensors.cpu_temp_target) CPU_TEMP_TARGET="$val" ;; lm_sensors.enable_ram_temp) ENABLE_RAM_TEMP="$val" ;; lm_sensors.enable_hdd_temp) ENABLE_HDD_TEMP="$val" ;; lm_sensors.enable_nvme_temp) ENABLE_NVME_TEMP="$val" ;; lm_sensors.enable_fan_speed) ENABLE_FAN_SPEED="$val" ;; lm_sensors.display_zero_speed_fans) DISPLAY_ZERO_SPEED_FANS="$val" ;; lm_sensors.temp_unit) TEMP_UNIT="$val" ;; # [ups] ups.enabled) ENABLE_UPS="$val" ;; ups.device_name) UPS_DEVICE_NAME="$val" ;; # [system_info] system_info.enabled) ENABLE_SYSTEM_INFO="$val" ;; system_info.type) SYSTEM_INFO_TYPE="$val" ;; # [pve_trigger] pve_trigger.enabled) PVE_TRIGGER_ENABLED="$val" ;; # [debug] debug.lm_sensors_mode) DEBUG_LM_SENSORS="$val" ;; debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;; debug.intel_mode) DEBUG_INTEL="$val" ;; debug.intel_devices_file) DEBUG_INTEL_FILE="$val" ;; debug.intel_output_file) DEBUG_INTEL_OUTPUT_FILE="$val" ;; debug.nvidia_mode) DEBUG_NVIDIA="$val" ;; debug.nvidia_devices_file) DEBUG_NVIDIA_DEVICES_FILE="$val" ;; debug.nvidia_output_file) DEBUG_NVIDIA_OUTPUT_FILE="$val" ;; debug.amd_mode) DEBUG_AMD="$val" ;; debug.amd_devices_file) DEBUG_AMD_FILE="$val" ;; debug.ups_mode) DEBUG_UPS="$val" ;; debug.ups_output_file) DEBUG_UPS_FILE="$val" ;; debug.log_enabled) DEBUG_LOG="$val" ;; debug.log_file) DEBUG_LOG_FILE="$val" ;; esac done < "$NODE_INFO_CONF" } #endregion helpers sanitize_sensors_output() { local input="$1" echo "$input" | perl -0777 -pe ' s/ERROR:.+\s(\w+):\s(.+)/"$1": 0.000,/g; s/ERROR:.+\s(\w+)!/"$1": 0.000,/g; s/,\s*(\})/$1/g; s/\bNaN\b/null/g; s/"SODIMM"\s*:\s*\{\s*"temp(\d+)_input"/"SODIMM $1": {\n "temp$1_input"/g; s/"([^"]*Fan[^"]*)"\s*:\s*\{\s*"fan(\d+)_input"/"$1 $2": {\n "fan$2_input"/g; ' | python3 -m json.tool 2>/dev/null || echo "$input" } _check_or_install_tool() { local cmd="$1" pkg="$2" description="$3" if command -v "$cmd" &>/dev/null; then info "$description is installed." return 0 fi local choice choice=$(ask "$description is not installed. Install it now? (y/N)") case "$choice" in [yY]) apt-get update -qq apt-get install -y "$pkg" command -v "$cmd" &>/dev/null && { info "$description installed."; return 0; } || \ { warn "$description installation failed. Section will be skipped."; return 1; } ;; *) info "Skipping $description." return 1 ;; esac } _check_nvidia_tool() { if command -v nvidia-smi &>/dev/null; then info "nvidia-smi is installed." return 0 fi warn "nvidia-smi not found. NVIDIA monitoring requires NVIDIA drivers (not installable via apt)." return 1 } #region node-info wizard configure_node_info() { # Initialize all variables to off LM_SENSORS_ENABLED=0 ENABLE_CPU=0; CPU_TEMP_TARGET="Core" ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0 ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C" ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0 ENABLE_GPU_HISTORY=0 ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost" ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1 local lm_sensors_ok=false local sensors_detected=false if [[ "$DEBUG_LM_SENSORS" -eq 1 && -f "$DEBUG_LM_SENSORS_FILE" ]]; then info "[debug] Using sensor data from $DEBUG_LM_SENSORS_FILE" lm_sensors_ok=true; LM_SENSORS_ENABLED=1 else _check_or_install_tool sensors lm-sensors "lm-sensors" && lm_sensors_ok=true && LM_SENSORS_ENABLED=1 fi if [[ "$lm_sensors_ok" == true ]]; then local sensorsOutput if [[ "$DEBUG_LM_SENSORS" -eq 1 ]]; then sensorsOutput=$(cat "$DEBUG_LM_SENSORS_FILE") else sensorsOutput=$(sensors -j 2>/dev/null) || true fi local trimmedSensorsOutput trimmedSensorsOutput=$(echo "$sensorsOutput" | tr -d '[:space:]') if [[ -z "$trimmedSensorsOutput" || "$trimmedSensorsOutput" == "{}" ]]; then warn "lm-sensors is installed but reported no sensors." warn "No kernel sensor drivers appear to be loaded." warn "Run 'sensors-detect' and load the suggested modules, then re-run this configurator." warn "lm-sensors output is the foundation for this mod. Mod cannot be enabled without it." exit 0 lm_sensors_ok=false LM_SENSORS_ENABLED=0 fi fi if [[ "$lm_sensors_ok" == true ]]; then local sanitisedSensorsOutput sanitisedSensorsOutput=$(sanitize_sensors_output "$sensorsOutput") #region CPU msgb "\n=== Detecting CPU temperature sensors ===" local cpuList="" cpuCount=0 for pattern in "${KNOWN_CPU_SENSORS[@]}"; do local found_cpus found_cpus=$(echo "$sanitisedSensorsOutput" | grep -o "\"${pattern}[^\"]*\"" || true | sed 's/"//g') if [[ -n "$found_cpus" ]]; then while read -r sensor; do [[ -z "$sensor" ]] && continue cpuCount=$((cpuCount + 1)) cpuList="${cpuList:+$cpuList,}$sensor" ENABLE_CPU=1 done <<< "$found_cpus" fi done if [[ "$ENABLE_CPU" -eq 1 ]]; then info "Detected CPU sensors ($cpuCount): $cpuList" sensors_detected=true while true; do local choice choice=$(ask "Display temperatures for all cores [C] or average per CPU [a]? (C/a)") case "$choice" in [cC]|"") CPU_TEMP_TARGET="Core"; info "Showing per-core temperatures."; break ;; [aA]) CPU_TEMP_TARGET="Package"; info "Showing average per-CPU temperature."; break ;; *) warn "Invalid input, choose C or a." ;; esac done else warn "No CPU temperature sensors found." fi #endregion CPU #region RAM msgb "\n=== Detecting RAM temperature sensors ===" local ramCount ramCount=$(grep -c '"SODIMM[^"]*"' <<<"$sanitisedSensorsOutput" || true) if [[ "$ramCount" -gt 0 ]]; then info "Detected $ramCount RAM sensor(s)." ENABLE_RAM_TEMP=1; sensors_detected=true else warn "No RAM temperature sensors found." fi #endregion RAM #region HDD/SSD msgb "\n=== Detecting HDD/SSD temperature sensors ===" local hddList hddList=$(echo "$sanitisedSensorsOutput" | grep -o '"drivetemp-scsi[^"]*"' | sed 's/"//g' | wc -l || true) if [[ "$hddList" -gt 0 ]]; then info "Detected $hddList HDD/SSD sensor(s)." ENABLE_HDD_TEMP=1; sensors_detected=true else warn "No HDD/SSD temperature sensors found. (Requires kernel module 'drivetemp'.)" fi #endregion HDD/SSD #region NVMe msgb "\n=== Detecting NVMe temperature sensors ===" local nvmeCount nvmeCount=$(echo "$sanitisedSensorsOutput" | grep -c '"nvme[^"]*"' || true) if [[ "$nvmeCount" -gt 0 ]]; then info "Detected $nvmeCount NVMe sensor(s)." ENABLE_NVME_TEMP=1; sensors_detected=true else warn "No NVMe temperature sensors found." fi #endregion NVMe #region Fans msgb "\n=== Detecting fan speed sensors ===" local fanCount fanCount=$(grep -c 'fan[0-9]\+_input' <<<"$sanitisedSensorsOutput" || true) if [[ "$fanCount" -gt 0 ]]; then info "Detected $fanCount fan speed reading(s)." ENABLE_FAN_SPEED=1; sensors_detected=true local choice choice=$(ask "Display fans reporting zero speed? (Y/n)") case "$choice" in [nN]) DISPLAY_ZERO_SPEED_FANS=0; info "Zero-speed fans will be hidden." ;; *) DISPLAY_ZERO_SPEED_FANS=1; info "Zero-speed fans will be shown." ;; esac else warn "No fan speed sensors found." fi #endregion Fans #region Temperature unit if [[ "$sensors_detected" == true ]]; then msgb "\n=== Temperature unit ===" 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 fi #endregion Temperature unit fi #region Intel GPU msgb "\n=== Detecting Intel GPU ===" local intelCards="" if [[ "$DEBUG_INTEL" -eq 1 && -f "$DEBUG_INTEL_FILE" ]]; then info "[debug] Using Intel GPU data from $DEBUG_INTEL_FILE" intelCards=$(cat "$DEBUG_INTEL_FILE") if [[ -n "$intelCards" ]]; then info "Intel GPU(s) detected (debug):" echo "$intelCards" | while IFS= read -r line; do echo " $line"; done if [[ -f "$DEBUG_INTEL_OUTPUT_FILE" ]]; then info "[debug] Intel GPU stats output file found at $DEBUG_INTEL_OUTPUT_FILE" ENABLE_INTEL_GPU_INFO=1 else warn "[debug] Intel GPU stats output file not found at $DEBUG_INTEL_OUTPUT_FILE. GPU stats graphs will be empty." fi else warn "No Intel GPUs in debug file." fi elif _check_or_install_tool intel_gpu_top intel-gpu-tools "Intel GPU tools (intel-gpu-tools)"; then intelCards=$(intel_gpu_top -L 2>/dev/null | grep -E '^card[0-9]+' || true) if [[ -n "$intelCards" ]]; then info "Intel GPU(s) detected:" echo "$intelCards" | while IFS= read -r line; do echo " $line"; done ENABLE_INTEL_GPU_INFO=1 else warn "No Intel GPUs detected by intel_gpu_top." fi fi #endregion Intel GPU #region NVIDIA GPU msgb "\n=== Detecting NVIDIA GPU ===" if [[ "$DEBUG_NVIDIA" -eq 1 && -f "$DEBUG_NVIDIA_DEVICES_FILE" ]]; then info "[debug] Using NVIDIA GPU data from $DEBUG_NVIDIA_DEVICES_FILE" local nvidiaCards nvidiaCards=$(cat "$DEBUG_NVIDIA_DEVICES_FILE") if [[ -n "$nvidiaCards" ]]; then info "NVIDIA GPU(s) detected (debug):" echo "$nvidiaCards" | while IFS= read -r line; do echo " $line"; done if [[ -f "$DEBUG_NVIDIA_OUTPUT_FILE" ]]; then info "[debug] NVIDIA GPU stats output file found at $DEBUG_NVIDIA_OUTPUT_FILE" ENABLE_NVIDIA_GPU_INFO=1 else warn "[debug] NVIDIA GPU stats output file not found at $DEBUG_NVIDIA_OUTPUT_FILE. GPU stats graphs will be empty." fi else warn "No NVIDIA GPUs in debug file." fi elif _check_nvidia_tool; then local nvidiaCards nvidiaCards=$(nvidia-smi -L 2>/dev/null || true) if [[ -n "$nvidiaCards" ]]; then info "NVIDIA GPU(s) detected:" echo "$nvidiaCards" | while IFS= read -r line; do echo " $line"; done ENABLE_NVIDIA_GPU_INFO=1 else warn "No NVIDIA GPUs detected by nvidia-smi." fi fi #endregion NVIDIA GPU #region AMD GPU (placeholder) ENABLE_AMD_GPU_INFO=0 #endregion AMD GPU #region GPU history if [[ "$ENABLE_INTEL_GPU_INFO" -eq 1 || "$ENABLE_NVIDIA_GPU_INFO" -eq 1 ]]; then msgb "\n=== GPU Historical Data ===" local choice choice=$(ask "Store historical GPU data for graphs? (y/N)") case "$choice" in [yY]) ENABLE_GPU_HISTORY=1; info "Historical GPU data will be stored." ;; *) info "Historical GPU data disabled." ;; esac fi #endregion GPU history #region UPS msgb "\n=== UPS Information ===" local choiceUPS choiceUPS=$(ask "Enable UPS information? (y/N)") case "$choiceUPS" in [yY]) local upsConn modelName upsOutput upsConn=$(ask "Enter UPS connection string (e.g. upsname@hostname[:port])") if [[ "$DEBUG_UPS" -eq 1 ]]; then if [[ -f "$DEBUG_UPS_FILE" ]]; then info "[debug] Using UPS data from $DEBUG_UPS_FILE" else warn "[debug] Debug mode for UPS is enabled but file not found at $DEBUG_UPS_FILE." fi upsOutput=$(cat "$DEBUG_UPS_FILE" || true) ENABLE_UPS=1 elif _check_or_install_tool upsc nut-client "Network UPS Tools (upsc)" && [[ -n "$upsConn" ]]; then upsOutput=$(upsc "$upsConn" 2>/dev/null || true) else warn "Could not connect to UPS at '$upsConn'. UPS info will be disabled." fi if [[ -n "$upsOutput" ]]; then modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d: -f2- | xargs) ENABLE_UPS=1 UPS_DEVICE_NAME="$upsConn" info "Connected to UPS: $modelName at $upsConn" else warn "Could not connect to UPS at '$upsConn'. UPS info will be disabled." ENABLE_UPS=0 fi ;; *) info "UPS information disabled." ;; esac #endregion UPS #region System info msgb "\n=== System Information ===" echo " type 1) System information (manufacturer, product, serial)" dmidecode -t 1 2>/dev/null | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print " "$0}' || true echo " type 2) Baseboard/Motherboard information" dmidecode -t 2 2>/dev/null | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print " "$0}' || true local choiceSys choiceSys=$(ask "Enable system information? (1/2/n)") case "$choiceSys" in 1|"") ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=1; info "System information (type 1) will be shown." ;; 2) ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=2; info "Baseboard information (type 2) will be shown." ;; [nN]) info "System information disabled." ;; *) warn "Invalid selection. Defaulting to type 1."; ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=1 ;; esac #endregion System info } #endregion node-info wizard #region write config write_config() { mkdir -p "$(dirname "$CONF_FILE")" "$CONFD_DIR" # Main config: which mods are enabled + global trigger/service settings. cat > "$CONF_FILE" <.conf [modules] node_info=${MOD_NODE_INFO} nag_screen=${MOD_NAG_SCREEN} [pve_trigger] enabled=${PVE_TRIGGER_ENABLED} EOF info "Main configuration saved to $CONF_FILE" # node_info mod config. cat > "$NODE_INFO_CONF" < "$NAG_SCREEN_CONF" </dev/null || echo "unknown") echo -e "\e[1;34m" echo " ██████╗ ██╗ ██╗███████╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗" echo " ██╔══██╗██║ ██║██╔════╝ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝" echo " ██████╔╝██║ ██║█████╗ ██╔████╔██║██║ ██║██║ ██║███████╗" echo " ██╔═══╝ ╚██╗ ██╔╝██╔══╝ ██║╚██╔╝██║██║ ██║██║ ██║╚════██║" echo " ██║ ╚████╔╝ ███████╗ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████║" echo " ╚═╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝" echo -e "\e[0m" echo -e "\e[1m PVE-mods Configurator \e[0;36mv${_version}\e[0m" echo -e " Proxmox VE enhancement suite — hardware monitoring, GPU info & more\n" # ── Root check ──────────────────────────────────────────────────────────── [[ $EUID -eq 0 ]] || err "This script must be run as root." # ── Prerequisite check ──────────────────────────────────────────────────── if [[ ! -x "$APPLY_PATCHES" ]]; then err "pve-mod is not installed (cannot find $APPLY_PATCHES).\nInstall it first: dpkg -i pve-mod_*.deb" fi # ── Legacy installation check ───────────────────────────────────────────── if grep -qF '$res->{sensorsJSONOutput}' "$NODES_PM" 2>/dev/null || \ grep -qF '$res->{systemInfo}' "$NODES_PM" 2>/dev/null; then err "A legacy pve-mod bash-script installation was detected in Nodes.pm.\nRemove it first:\n bash /path/to/pve-mod-gui-sensors.sh uninstall" fi # ── Initialize all config variables with safe defaults ──────────────────── MOD_NODE_INFO=0; MOD_NAG_SCREEN=0 LM_SENSORS_ENABLED=0 ENABLE_CPU=0; CPU_TEMP_TARGET="Core" ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0 ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C" ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0 ENABLE_GPU_HISTORY=0 ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost" ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1 PVE_TRIGGER_ENABLED=0 DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json" DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.txt" DEBUG_INTEL_OUTPUT_FILE="/tmp/intel-gpu-top-output.txt" DEBUG_NVIDIA=0; DEBUG_NVIDIA_OUTPUT_FILE="/tmp/nvidia-smi-output.csv" DEBUG_NVIDIA_DEVICES_FILE="/tmp/nvidia-smi-devices.csv" DEBUG_AMD=0; DEBUG_AMD_FILE="/tmp/amd-gpu-devices.json" DEBUG_UPS=0; DEBUG_UPS_FILE="/tmp/ups-output.json" DEBUG_LOG=0; DEBUG_LOG_FILE="/tmp/pve-mod-debug.log" _load_conf # ── Module / feature selection ──────────────────────────────────────────── # Existing settings are loaded above; the user picks ONE option to (re)configure # this run, and all other options retain their previously saved values. msgb "\n=== pve-mod Module Selection ===" echo "Available options (select one):" echo " [1] Node Info — sensor readings, GPU stats, UPS, system information" echo " [2] Nag Screen — remove Proxmox subscription nag screen" echo " [3] Auto re-patch on PVE upgrade — automatically re-apply patches when" echo " pve-manager is upgraded" echo " [n] None / cancel" local modChoice modChoice=$(ask "Select an option to enable (1/2/3/n)") case "$modChoice" in 1) MOD_NODE_INFO=1 msgb "\n=== Node Info Configuration ===" configure_node_info ;; 2) MOD_NAG_SCREEN=1 msgb "\n=== Nag Screen ===" info "Subscription nag screen removal will be applied." ;; 3) PVE_TRIGGER_ENABLED=1 info "Auto re-patching on PVE upgrade enabled." ;; [nN]) info "No option selected. Exiting."; exit 0 ;; *) warn "Invalid selection. Exiting."; exit 0 ;; esac # ── Write config and apply ──────────────────────────────────────────────── write_config msgb "\n=== Applying patches ===" "$APPLY_PATCHES" msgb "\n=== Done ===" info "pve-mod is configured and active." info "Clear your browser cache to see the changes." } main