diff --git a/src/Scripts/pve-mod-configure b/src/Scripts/pve-mod-configure index 695de3d..c251ba6 100644 --- a/src/Scripts/pve-mod-configure +++ b/src/Scripts/pve-mod-configure @@ -3,19 +3,19 @@ # 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. +# +# Module-specific logic lives in /usr/lib/pve-mod/configure.d/.sh, +# sourced at startup. Each module file provides the standard four-function +# API: _defaults, _load_conf, _configure, _write_conf. 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" REVERT_PATCHES="/usr/lib/pve-mod/revert-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"; } @@ -31,436 +31,36 @@ mod_status() { local val="$1" on_label="${2:-[enabled]}" off_label="${3:-[disabled]}" [[ "$val" == "1" ]] && echo -e "\e[0;32m${on_label}\e[0m" || echo -e "\e[0;33m${off_label}\e[0m" } +#endregion helpers -_load_conf() { - # Load main config (module toggles + pve_trigger) - if [[ -f "$CONF_FILE" ]]; then - local section="" 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.node_info) MOD_NODE_INFO="$val" ;; - modules.nag_screen) MOD_NAG_SCREEN="$val" ;; - pve_trigger.enabled) PVE_TRIGGER_ENABLED="$val" ;; - esac - done < "$CONF_FILE" - fi +# Source all module configure scripts. Override CONFIGURE_D_DIR for testing. +CONFIGURE_D_DIR="${PVE_MOD_CONFIGURE_D:-/usr/lib/pve-mod/configure.d}" +for _f in "${CONFIGURE_D_DIR}"/*.sh; do + [[ -f "$_f" ]] || continue + # shellcheck source=/dev/null + source "$_f" +done; unset _f - # Load node_info mod config - [[ -f "$NODE_INFO_CONF" ]] || return 0 - local in_debug=0 line key val section="" +_load_main_conf() { + [[ -f "$CONF_FILE" ]] || return 0 + local section="" line key val while IFS= read -r line; do case "$line" in - '#'*|'') continue ;; - '['*']') section="${line#[}"; section="${section%]}"; continue ;; + '#'*|'') 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" ;; + modules.node_info) MOD_NODE_INFO="$val" ;; + modules.nag_screen) MOD_NAG_SCREEN="$val" ;; + pve_trigger.enabled) PVE_TRIGGER_ENABLED="$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" + done < "$CONF_FILE" } -_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 - - # Write JSON devices file for debug/cache use by the Perl collector - local json="[" - local first=1 - while IFS= read -r line; do - # Parse: "card0 Intel Alderlake_n (Gen12) pci:vendor=8086,device=46D0,card=0" - if [[ "$line" =~ ^(card[0-9]+)[[:space:]]+(.+[^[:space:]])[[:space:]]+(pci:[^[:space:]]+) ]]; then - local card="${BASH_REMATCH[1]}" - local name="${BASH_REMATCH[2]}" - local path="${BASH_REMATCH[3]}" - name="${name%"${name##*[![:space:]]}"}" # rtrim - [[ "$first" -eq 0 ]] && json+="," - json+="{\"card\":\"$card\",\"name\":\"$name\",\"path\":\"$path\",\"drm_path\":\"/dev/dri/$card\"}" - first=0 - fi - done <<< "$intelCards" - json+="]" - echo "$json" > "$DEBUG_INTEL_FILE" - info "Intel GPU device list saved to $DEBUG_INTEL_FILE" - 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 - - if [[ "$ENABLE_SYSTEM_INFO" -eq 1 ]]; then - local cache_dir="/var/lib/pve-mod" - mkdir -p "$cache_dir" - local cache_file="${cache_dir}/dmidecode-type${SYSTEM_INFO_TYPE}.txt" - dmidecode -t "$SYSTEM_INFO_TYPE" > "$cache_file" 2>/dev/null || true - chmod 644 "$cache_file" - info "DMI data cached to $cache_file" - fi - #endregion System info -} -#endregion node-info wizard - -#region write config -write_config() { +write_main_conf() { mkdir -p "$(dirname "$CONF_FILE")" "$CONFD_DIR" - - # Main config: which mods are enabled + global trigger/service settings. cat > "$CONF_FILE" < "$NODE_INFO_CONF" < "$NAG_SCREEN_CONF" < "$NAG_SCREEN_CONF" </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 +} + +# --- standard API ------------------------------------------------------------ + +node_info_defaults() { + 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 + DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json" + DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.json" + 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" +} + +node_info_load_conf() { + [[ -f "$NODE_INFO_CONF" ]] || return 0 + local line key val section="" + 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 + 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.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.enabled) ENABLE_UPS="$val" ;; + ups.device_name) UPS_DEVICE_NAME="$val" ;; + system_info.enabled) ENABLE_SYSTEM_INFO="$val" ;; + system_info.type) SYSTEM_INFO_TYPE="$val" ;; + 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" +} + +#region node-info wizard +node_info_configure() { + # 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 + + # Write JSON devices file for debug/cache use by the Perl collector + local json="[" + local first=1 + while IFS= read -r line; do + # Parse: "card0 Intel Alderlake_n (Gen12) pci:vendor=8086,device=46D0,card=0" + if [[ "$line" =~ ^(card[0-9]+)[[:space:]]+(.+[^[:space:]])[[:space:]]+(pci:[^[:space:]]+) ]]; then + local card="${BASH_REMATCH[1]}" + local name="${BASH_REMATCH[2]}" + local path="${BASH_REMATCH[3]}" + name="${name%"${name##*[![:space:]]}"}" # rtrim + [[ "$first" -eq 0 ]] && json+="," + json+="{\"card\":\"$card\",\"name\":\"$name\",\"path\":\"$path\",\"drm_path\":\"/dev/dri/$card\"}" + first=0 + fi + done <<< "$intelCards" + json+="]" + echo "$json" > "$DEBUG_INTEL_FILE" + info "Intel GPU device list saved to $DEBUG_INTEL_FILE" + 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 + + if [[ "$ENABLE_SYSTEM_INFO" -eq 1 ]]; then + local cache_dir="/var/lib/pve-mod" + mkdir -p "$cache_dir" + local cache_file="${cache_dir}/dmidecode-type${SYSTEM_INFO_TYPE}.txt" + dmidecode -t "$SYSTEM_INFO_TYPE" > "$cache_file" 2>/dev/null || true + chmod 644 "$cache_file" + info "DMI data cached to $cache_file" + fi + #endregion System info +} +#endregion node-info wizard + +node_info_write_conf() { + cat > "$NODE_INFO_CONF" <