From 5df83ec3695a42cb2b65db36a4dc7cf06a195b9a Mon Sep 17 00:00:00 2001 From: Meliox <5264368+Meliox@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:07:38 +0200 Subject: [PATCH] fix collectors, modular apply/unapply patches, correct config loading in pve-mod-configure (#208) * first * also check for output file * add full debug to intel device * implement ups debug and intel debug * fix system information call * add graphic info call and return if call not configured * add intel to safe init * write out intel to config * fix patch * Move collection of system information to cache * implement system info * fix config load * more intel fixes * fix tainted paths and intel graphics * resolved rrd crash by disabling rrd when not enabled * fix(configure): toggle install state, scope patching to selected mod * small fix for get_graphics_info naming * fix(configure): don't update config if patch apply/revert fails * revert version in config --------- Co-authored-by: Meliox --- src/Scripts/apply-patches.sh | 10 +- src/Scripts/pve-mod-configure | 222 ++++++++++-------- src/Scripts/revert-patches.sh | 17 +- src/node_info/files/Collector/Intel.pm | 143 +++++++---- src/node_info/files/Collector/Ups.pm | 16 +- .../files/Collector/systemInformation.pm | 10 +- src/node_info/files/Config.pm | 15 +- src/node_info/files/PveMod_SensorInfo.pm | 16 +- src/node_info/files/PveMod_pvemanagerlib.js | 18 +- src/node_info/files/Store.pm | 6 +- src/node_info/files/Utils.pm | 8 + src/node_info/node_info.conf | 1 + .../patches/01-nodes-pm-sensors.patch | 5 +- 13 files changed, 321 insertions(+), 166 deletions(-) diff --git a/src/Scripts/apply-patches.sh b/src/Scripts/apply-patches.sh index c131401..b543204 100644 --- a/src/Scripts/apply-patches.sh +++ b/src/Scripts/apply-patches.sh @@ -102,8 +102,14 @@ if ! command -v patch >/dev/null 2>&1; then exit 1 fi -for mod in $(list_modules); do - [[ "$(read_conf "$MAIN_CONF" modules "$mod" 0)" == "1" ]] || continue +mapfile -t _all_modules < <(list_modules) +_target_modules=("${@:-${_all_modules[@]}}") +for mod in "${_target_modules[@]}"; do + # When modules are explicitly named as args, the caller decides what to apply. + # When running for all modules (no args from dpkg trigger), respect the config. + if [[ $# -eq 0 ]]; then + [[ "$(read_conf "$MAIN_CONF" modules "$mod" 0)" == "1" ]] || continue + fi mod_dir="$PATCHES_DIR/$mod" manifest="$mod_dir/patches.list" diff --git a/src/Scripts/pve-mod-configure b/src/Scripts/pve-mod-configure index ab66c09..695de3d 100644 --- a/src/Scripts/pve-mod-configure +++ b/src/Scripts/pve-mod-configure @@ -11,6 +11,7 @@ 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-") @@ -26,10 +27,33 @@ ask() { echo "$response" } bool() { [[ "$1" == true ]] && echo 1 || echo 0; } +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" +} _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 + + # Load node_info mod config [[ -f "$NODE_INFO_CONF" ]] || return 0 - local in_debug=0 line key val + local in_debug=0 line key val section="" while IFS= read -r line; do case "$line" in '#'*|'') continue ;; @@ -69,6 +93,7 @@ _load_conf() { 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" ;; @@ -281,7 +306,12 @@ configure_node_info() { if [[ -n "$intelCards" ]]; then info "Intel GPU(s) detected (debug):" echo "$intelCards" | while IFS= read -r line; do echo " $line"; done - ENABLE_INTEL_GPU_INFO=1 + 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 @@ -291,6 +321,25 @@ configure_node_info() { 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 @@ -306,7 +355,12 @@ configure_node_info() { if [[ -n "$nvidiaCards" ]]; then info "NVIDIA GPU(s) detected (debug):" echo "$nvidiaCards" | while IFS= read -r line; do echo " $line"; done - ENABLE_NVIDIA_GPU_INFO=1 + 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 @@ -347,16 +401,20 @@ configure_node_info() { [yY]) local upsConn modelName upsOutput upsConn=$(ask "Enter UPS connection string (e.g. upsname@hostname[:port])") - if [[ "$DEBUG_UPS" -eq 1 && -f "$DEBUG_UPS_FILE" ]]; then - info "[debug] Using UPS data from $DEBUG_UPS_FILE" - upsOutput=$(cat "$DEBUG_UPS_FILE") - else - if ! command -v upsc &>/dev/null; then - err "'upsc' is not available. Install 'nut-client' first." + 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=$(upsc "$upsConn" 2>&1) + 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 echo "$upsOutput" | grep -q "device.model:"; then + if [[ -n "$upsOutput" ]]; then modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d: -f2- | xargs) ENABLE_UPS=1 UPS_DEVICE_NAME="$upsConn" @@ -365,7 +423,7 @@ configure_node_info() { warn "Could not connect to UPS at '$upsConn'. UPS info will be disabled." ENABLE_UPS=0 fi - ;; + ;; *) info "UPS information disabled." ;; esac #endregion UPS @@ -385,6 +443,15 @@ configure_node_info() { [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 @@ -407,9 +474,6 @@ nag_screen=${MOD_NAG_SCREEN} [pve_trigger] enabled=${PVE_TRIGGER_ENABLED} - -[service] -mode=embedded EOF info "Main configuration saved to $CONF_FILE" @@ -450,6 +514,7 @@ lm_sensors_mode=${DEBUG_LM_SENSORS} lm_sensors_output_file=${DEBUG_LM_SENSORS_FILE} intel_mode=${DEBUG_INTEL} intel_devices_file=${DEBUG_INTEL_FILE} +intel_output_file=${DEBUG_INTEL_OUTPUT_FILE} nvidia_mode=${DEBUG_NVIDIA} nvidia_devices_file=${DEBUG_NVIDIA_DEVICES_FILE} nvidia_output_file=${DEBUG_NVIDIA_OUTPUT_FILE} @@ -473,69 +538,6 @@ EOF } #endregion write config -#region debug wizard -configure_debug() { - msgb "\n=== Debug / Development Mode ===" - echo "Debug mode substitutes real tools with pre-captured data files." - echo "When a collector's mode is enabled the tool (sensors/intel_gpu_top/etc.) is not required." - local choice - choice=$(ask "Configure debug file overrides? (y/N)") - [[ "$choice" != [yY] ]] && return - - local newpath - - choice=$(ask "Enable LM-Sensors debug? (y/N)") - if [[ "$choice" == [yY] ]]; then - DEBUG_LM_SENSORS=1 - newpath=$(ask "Path to sensors JSON file [${DEBUG_LM_SENSORS_FILE}]") - DEBUG_LM_SENSORS_FILE="${newpath:-$DEBUG_LM_SENSORS_FILE}" - info "LM-Sensors debug: ${DEBUG_LM_SENSORS_FILE}" - fi - - choice=$(ask "Enable Intel GPU debug? (y/N)") - if [[ "$choice" == [yY] ]]; then - DEBUG_INTEL=1 - newpath=$(ask "Path to Intel GPU devices JSON [${DEBUG_INTEL_FILE}]") - DEBUG_INTEL_FILE="${newpath:-$DEBUG_INTEL_FILE}" - info "Intel GPU debug: ${DEBUG_INTEL_FILE}" - fi - - choice=$(ask "Enable NVIDIA GPU debug? (y/N)") - if [[ "$choice" == [yY] ]]; then - DEBUG_NVIDIA=1 - newpath=$(ask "Path to nvidia-smi output CSV [${DEBUG_NVIDIA_OUTPUT_FILE}]") - DEBUG_NVIDIA_OUTPUT_FILE="${newpath:-$DEBUG_NVIDIA_OUTPUT_FILE}" - newpath=$(ask "Path to nvidia-smi devices CSV [${DEBUG_NVIDIA_DEVICES_FILE}]") - DEBUG_NVIDIA_DEVICES_FILE="${newpath:-$DEBUG_NVIDIA_DEVICES_FILE}" - info "NVIDIA GPU debug: output=${DEBUG_NVIDIA_OUTPUT_FILE} devices=${DEBUG_NVIDIA_DEVICES_FILE}" - fi - - choice=$(ask "Enable AMD GPU debug? (y/N)") - if [[ "$choice" == [yY] ]]; then - DEBUG_AMD=1 - newpath=$(ask "Path to AMD GPU devices JSON [${DEBUG_AMD_FILE}]") - DEBUG_AMD_FILE="${newpath:-$DEBUG_AMD_FILE}" - info "AMD GPU debug: ${DEBUG_AMD_FILE}" - fi - - choice=$(ask "Enable UPS debug? (y/N)") - if [[ "$choice" == [yY] ]]; then - DEBUG_UPS=1 - newpath=$(ask "Path to UPS output JSON [${DEBUG_UPS_FILE}]") - DEBUG_UPS_FILE="${newpath:-$DEBUG_UPS_FILE}" - info "UPS debug: ${DEBUG_UPS_FILE}" - fi - - choice=$(ask "Enable debug logging? (y/N)") - if [[ "$choice" == [yY] ]]; then - DEBUG_LOG=1 - newpath=$(ask "Log file path [${DEBUG_LOG_FILE}]") - DEBUG_LOG_FILE="${newpath:-$DEBUG_LOG_FILE}" - info "Debug log: ${DEBUG_LOG_FILE}" - fi -} -#endregion debug wizard - main() { # ── Welcome banner ──────────────────────────────────────────────────────── local _version @@ -578,6 +580,7 @@ main() { 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.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" @@ -590,41 +593,60 @@ main() { # 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)") + echo -e " [1] Node Info $(mod_status "$MOD_NODE_INFO") — sensor readings, GPU stats, UPS, system information" + echo -e " [2] Nag Screen $(mod_status "$MOD_NAG_SCREEN") — remove Proxmox subscription nag screen" + echo -e " [3] Auto re-patch on PVE upgrade $(mod_status "$PVE_TRIGGER_ENABLED") — automatically re-apply patches when" + echo " pve-manager is upgraded" + echo " [n] None / cancel" + local modChoice selected_mod="" patching_needed=0 + modChoice=$(ask "Select an option (1/2/3/n)") case "$modChoice" in 1) - MOD_NODE_INFO=1 - msgb "\n=== Node Info Configuration ===" - configure_node_info + selected_mod="node_info" + if [[ "$MOD_NODE_INFO" == "1" ]]; then + MOD_NODE_INFO=0; info "Node Info will be removed." + else + MOD_NODE_INFO=1; patching_needed=1 + msgb "\n=== Node Info Configuration ===" + configure_node_info + fi ;; 2) - MOD_NAG_SCREEN=1 - msgb "\n=== Nag Screen ===" - info "Subscription nag screen removal will be applied." + selected_mod="nag_screen" + if [[ "$MOD_NAG_SCREEN" == "1" ]]; then + MOD_NAG_SCREEN=0; info "Nag Screen will be removed." + else + MOD_NAG_SCREEN=1; patching_needed=1 + info "Subscription nag screen removal will be applied." + fi ;; 3) - PVE_TRIGGER_ENABLED=1 - info "Auto re-patching on PVE upgrade enabled." + if [[ "$PVE_TRIGGER_ENABLED" == "1" ]]; then + PVE_TRIGGER_ENABLED=0; info "Auto re-patch disabled." + else + PVE_TRIGGER_ENABLED=1; info "Auto re-patching on PVE upgrade enabled." + fi ;; [nN]) info "No option selected. Exiting."; exit 0 ;; *) warn "Invalid selection. Exiting."; exit 0 ;; esac - # ── Debug mode ──────────────────────────────────────────────────────────── - configure_debug - - # ── Write config and apply ──────────────────────────────────────────────── + # ── Write config and apply/revert ───────────────────────────────────────── + # For mods with patches: apply if being enabled, revert if being disabled. + # Option 3 (pve_trigger) has no patches — write_config is sufficient. + if [[ "$patching_needed" == "1" ]]; then + msgb "\n=== Applying patches ===" + if ! "$APPLY_PATCHES" "$selected_mod"; then + err "Could not apply all patches for $selected_mod. Configuration was not updated." + fi + elif [[ -n "$selected_mod" ]]; then + msgb "\n=== Removing patches ===" + if ! "$REVERT_PATCHES" "$selected_mod"; then + err "Could not revert all patches for $selected_mod. Configuration was not updated." + fi + fi 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." diff --git a/src/Scripts/revert-patches.sh b/src/Scripts/revert-patches.sh index 455778b..584bec4 100644 --- a/src/Scripts/revert-patches.sh +++ b/src/Scripts/revert-patches.sh @@ -39,7 +39,7 @@ revert_one_patch() { fi warn " $(basename "$patch") could not be reverted cleanly; manual cleanup may be needed" patch -R -p1 -F0 -f --dry-run --verbose -d "$PVE_MOD_ROOT" < "$patch" >&2 || true - return 1 + return 2 } # ── main ────────────────────────────────────────────────────────────────────── @@ -51,10 +51,13 @@ fi [[ -d "$PATCHES_DIR" ]] || exit 0 CHANGED=false +FAILED=false -for mod_dir in "$PATCHES_DIR"/*/; do +mapfile -t _all_modules < <(for d in "$PATCHES_DIR"/*/; do [[ -d "$d" ]] && basename "$d"; done) +_target_modules=("${@:-${_all_modules[@]}}") +for mod in "${_target_modules[@]}"; do + mod_dir="$PATCHES_DIR/$mod/" [[ -d "$mod_dir" ]] || continue - mod="$(basename "$mod_dir")" manifest="$mod_dir/patches.list" mod_conf="$CONFD_DIR/$mod.conf" @@ -88,9 +91,13 @@ for mod_dir in "$PATCHES_DIR"/*/; do patch_name="${patches[$i]}" patch_file="$mod_dir/$patch_name" [[ -f "$patch_file" ]] || continue - if revert_one_patch "$patch_file"; then + revert_one_patch "$patch_file" + rc=$? + if [[ "$rc" -eq 0 ]]; then info " reverted $patch_name" CHANGED=true + elif [[ "$rc" -eq 2 ]]; then + FAILED=true fi done done @@ -99,4 +106,6 @@ if [[ "$CHANGED" == "true" ]]; then info "Restarting pveproxy..." systemctl restart pveproxy 2>/dev/null || true fi + +[[ "$FAILED" == "false" ]] || exit 1 exit 0 diff --git a/src/node_info/files/Collector/Intel.pm b/src/node_info/files/Collector/Intel.pm index 988ec60..f747060 100644 --- a/src/node_info/files/Collector/Intel.pm +++ b/src/node_info/files/Collector/Intel.pm @@ -5,7 +5,7 @@ use warnings; use Exporter 'import'; use PVE::PVEMod::Config qw(%config $process_type $pve_mod_working_dir); -use PVE::PVEMod::Utils qw(debug check_executable setup_collector_signals safe_write_json); +use PVE::PVEMod::Utils qw(debug check_executable setup_collector_signals safe_read_json safe_write_json); use PVE::PVEMod::Store qw(update_intel_gpu_rrd); our @EXPORT_OK = qw( @@ -20,26 +20,44 @@ our @EXPORT_OK = qw( sub get_intel_gpu_devices { my @devices = (); - debug(__LINE__, "Getting Intel GPU devices"); - if (open my $fh, '-|', 'intel_gpu_top -L') { - while (<$fh>) { - chomp; - # Parse: "card0 Intel Alderlake_n (Gen12) pci:vendor=8086,device=46D0,card=0" - # or: "card0 Intel Alderlake_n (Gen12) pci:0000:00:02.0" - if (/^(card\d+)\s+(.+?)\s+(pci:[^\s]+)/) { - my ($card, $name, $path) = ($1, $2, $3); + if ($config{debug}{intel_mode} && -f $config{debug}{intel_devices_file}) { + debug(__LINE__, "Debug mode: reading Intel GPU devices from $config{debug}{intel_devices_file}"); + my $data = safe_read_json($config{debug}{intel_devices_file}); + if ($data && ref $data eq 'ARRAY') { + for my $dev (@$data) { push @devices, { - card => $card, - name => $name, - path => $path, - drm_path => "/dev/dri/$card", + card => $dev->{card}, + name => $dev->{name}, + path => $dev->{path}, + drm_path => $dev->{drm_path}, }; - debug(__LINE__, "Found Intel device: $card -> $name ($path)"); + debug(__LINE__, "Found Intel device (debug): $dev->{card} -> $dev->{name} ($dev->{path})"); } + } else { + debug(__LINE__, "Failed to parse debug file $config{debug}{intel_devices_file}"); } - close $fh; } else { - debug(__LINE__, "Failed to run intel_gpu_top -L: $!"); + debug(__LINE__, "Getting Intel GPU devices"); + if (open my $fh, '-|', 'intel_gpu_top -L') { + while (<$fh>) { + chomp; + # Parse: "card0 Intel Alderlake_n (Gen12) pci:vendor=8086,device=46D0,card=0" + # or: "card0 Intel Alderlake_n (Gen12) pci:0000:00:02.0" + if (/^(card\d+)\s+(.+?)\s+(pci:[^\s]+)/) { + my ($card, $name, $path) = ($1, $2, $3); + push @devices, { + card => $card, + name => $name, + path => $path, + drm_path => "/dev/dri/$card", + }; + debug(__LINE__, "Found Intel device: $card -> $name ($path)"); + } + } + close $fh; + } else { + debug(__LINE__, "Failed to run intel_gpu_top -L: $!"); + } } return @devices; @@ -132,46 +150,81 @@ sub collector_for_intel_device { if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0; }); - debug(__LINE__, "About to open pipe to intel_gpu_top"); - my $intel_pull_interval = $config{intervals}{data_pull} * 1000; # milliseconds - $intel_gpu_top_pid = open(my $fh, '-|', - "intel_gpu_top -d $drm_dev -s $intel_pull_interval -l 2>&1"); - - unless (defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0) { - debug(__LINE__, "Failed to run intel_gpu_top for $drm_dev: $!"); - exit 1; - } - - debug(__LINE__, "Pipe opened successfully, PID=$intel_gpu_top_pid"); - my $node_name = "node0"; - while (my $line = <$fh>) { - last if $shutdown; - chomp $line; + if ($config{debug}{intel_mode} && -f $config{debug}{intel_output_file}) { + debug(__LINE__, "Debug mode: reading Intel GPU stats from $config{debug}{intel_output_file}"); + while (!$shutdown) { + if (open my $fh, '<', $config{debug}{intel_output_file}) { + while (my $line = <$fh>) { + chomp $line; + next if $line =~ /MHz|IRQ|RC6|Power|RCS|BCS|VCS|VECS|req\s+act|^\s*$/; - next if $line =~ /MHz|IRQ|RC6|Power|RCS|BCS|VCS|VECS|req\s+act|^\s*$/; + if ($line =~ /^\s*[\d\s\.]+$/) { + my $stats = _parse_intel_gpu_line($line); - if ($line =~ /^\s*[\d\s\.]+$/) { - my $stats = _parse_intel_gpu_line($line); + if ($stats) { + my $device_data = { + $node_name => { + name => $device->{name}, + device_path => $device->{path}, + drm_path => $device->{drm_path}, + stats => $stats, + } + }; - if ($stats) { - my $device_data = { - $node_name => { - name => $device->{name}, - device_path => $device->{path}, - drm_path => $device->{drm_path}, - stats => $stats, + safe_write_json($device_state_file, $device_data); + update_intel_gpu_rrd($device->{card}, $stats); + } } - }; + } + close $fh; + } else { + debug(__LINE__, "Failed to open debug file $config{debug}{intel_output_file}: $!"); + } + sleep $config{intervals}{data_pull} unless $shutdown; + } + } else { + debug(__LINE__, "About to open pipe to intel_gpu_top"); + my $intel_pull_interval = $config{intervals}{data_pull} * 1000; # milliseconds + $intel_gpu_top_pid = open(my $fh, '-|', + "intel_gpu_top -d $drm_dev -s $intel_pull_interval -l 2>&1"); - safe_write_json($device_state_file, $device_data); - update_intel_gpu_rrd($device->{card}, $stats); + unless (defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0) { + debug(__LINE__, "Failed to run intel_gpu_top for $drm_dev: $!"); + exit 1; + } + + debug(__LINE__, "Pipe opened successfully, PID=$intel_gpu_top_pid"); + + while (my $line = <$fh>) { + last if $shutdown; + chomp $line; + + next if $line =~ /MHz|IRQ|RC6|Power|RCS|BCS|VCS|VECS|req\s+act|^\s*$/; + + if ($line =~ /^\s*[\d\s\.]+$/) { + my $stats = _parse_intel_gpu_line($line); + + if ($stats) { + my $device_data = { + $node_name => { + name => $device->{name}, + device_path => $device->{path}, + drm_path => $device->{drm_path}, + stats => $stats, + } + }; + + safe_write_json($device_state_file, $device_data); + update_intel_gpu_rrd($device->{card}, $stats); + } } } + + close $fh; } - close $fh; debug(__LINE__, "Collector for $device->{card} shutting down"); exit 0; } diff --git a/src/node_info/files/Collector/Ups.pm b/src/node_info/files/Collector/Ups.pm index 261713f..487507a 100644 --- a/src/node_info/files/Collector/Ups.pm +++ b/src/node_info/files/Collector/Ups.pm @@ -6,7 +6,7 @@ use Exporter 'import'; use JSON; -use PVE::PVEMod::Config qw($process_type $ups_state_file); +use PVE::PVEMod::Config qw(%config $process_type $ups_state_file); use PVE::PVEMod::Utils qw(debug setup_collector_signals); our @EXPORT_OK = qw( @@ -40,7 +40,7 @@ sub collector_for_ups { debug(__LINE__, "Error writing UPS data: $@"); } - sleep 1 unless $shutdown; # $config{intervals}{data_pull} + sleep $config{intervals}{data_pull} unless $shutdown; } debug(__LINE__, "UPS collector shutting down"); @@ -56,7 +56,17 @@ sub _get_ups_status { debug(__LINE__, "Collecting UPS status for $ups_name"); - my $output = `/usr/bin/upsc $ups_name 2>/dev/null`; + my $output; + if ($config{debug}{ups_mode} && -f $config{debug}{ups_output_file}) { + debug(__LINE__, "Debug mode: reading UPS data from $config{debug}{ups_output_file}"); + open my $fh, '<', $config{debug}{ups_output_file} + or do { debug(__LINE__, "Failed to open debug file $config{debug}{ups_output_file}: $!"); return encode_json({ error => "Failed to open debug file" }); }; + local $/; + $output = <$fh>; + close $fh; + } else { + $output = `/usr/bin/upsc $ups_name 2>/dev/null`; + } unless (defined $output && length($output) > 0) { debug(__LINE__, "No output from upsc for $ups_name"); diff --git a/src/node_info/files/Collector/systemInformation.pm b/src/node_info/files/Collector/systemInformation.pm index de43892..c022548 100644 --- a/src/node_info/files/Collector/systemInformation.pm +++ b/src/node_info/files/Collector/systemInformation.pm @@ -44,10 +44,16 @@ sub get_system_information_data { sub _get_system_info { my ($type) = @_; - my $output = `/usr/sbin/dmidecode -t $type 2>/dev/null`; + my $cache_file = "/var/lib/pve-mod/dmidecode-type${type}.txt"; + my $output; + if (open(my $fh, '<', $cache_file)) { + local $/; + $output = <$fh>; + close($fh); + } unless (defined $output && length($output) > 0) { - debug(__LINE__, "No output from dmidecode -t $type"); + debug(__LINE__, "No cached DMI data at $cache_file - re-run pve-mod-configure as root to refresh"); return {}; } diff --git a/src/node_info/files/Config.pm b/src/node_info/files/Config.pm index 4678f72..2ccadc2 100644 --- a/src/node_info/files/Config.pm +++ b/src/node_info/files/Config.pm @@ -20,6 +20,13 @@ our @EXPORT_OK = qw( our $DEBUG_ENABLED = 1; our $VERSION = 'version-placeholder'; +# ============================================================================ +# Config paths +# ============================================================================ + +my $CONF_FILE = '/etc/pve-mod/pve-mod.conf'; +my $CONFD_DIR = '/etc/pve-mod/conf.d'; + # Runtime process-type tag — set to 'worker' or 'collector' after fork. # Each forked child gets its own copy of this variable. our $process_type = 'main'; # 'main', 'worker', or 'collector' @@ -42,6 +49,7 @@ our %config = ( lm_sensors_output_file => '/tmp/sensors-output.json', intel_mode => 0, intel_devices_file => '/tmp/intel-gpu-devices.json', + intel_output_file => '/tmp/intel-gpu-output.json', nvidia_mode => 0, nvidia_devices_file => '/tmp/nvidia-smi-devices.csv', nvidia_output_file => '/tmp/nvidia-smi-output.csv', @@ -104,8 +112,8 @@ our $RRD_BASE = '/var/lib/rrdcached/db/pve-mod-gpu'; # ============================================================================ sub _load_ini_file { - my $path = '/etc/pve-mod/pve-mod.conf'; - return unless -f $path; + my ($path) = @_; + return unless defined $path && -f $path; open my $fh, '<', $path or return; my $section = ''; @@ -146,6 +154,7 @@ sub _load_ini_file { close $fh; } -_load_ini_file(); +_load_ini_file($CONF_FILE); +_load_ini_file($_) for glob("$CONFD_DIR/*.conf"); 1; diff --git a/src/node_info/files/PveMod_SensorInfo.pm b/src/node_info/files/PveMod_SensorInfo.pm index 6e777c9..c38c4e9 100644 --- a/src/node_info/files/PveMod_SensorInfo.pm +++ b/src/node_info/files/PveMod_SensorInfo.pm @@ -149,8 +149,12 @@ sub _load_graphics_data { # API calls # ============================================================================ -sub get_graphic_info { - debug(__LINE__, "get_graphic_info called"); +sub get_graphics_info { + debug(__LINE__, "get_graphics_info called"); + if (!($config{gpu}{intel_enabled} || !$config{gpu}{nvidia_enabled} || !$config{gpu}{amd_enabled})) { + debug(__LINE__, "GPU information collection is disabled"); + return { }; + } # Start PVE Mod pve_mod_starter(); @@ -165,6 +169,10 @@ sub get_graphic_info { sub get_sensors_info { debug(__LINE__, "get_sensors_info called"); + if (!$config{lm_sensors}{enabled}) { + debug(__LINE__, "LM Sensors collection is disabled"); + return {}; + } # Start PVE Mod pve_mod_starter(); @@ -179,6 +187,10 @@ sub get_sensors_info { sub get_ups_info { debug(__LINE__, "get_ups_info called"); + if (!$config{ups}{enabled}) { + debug(__LINE__, "UPS collection is disabled"); + return {}; + } # Start PVE Mod pve_mod_starter(); diff --git a/src/node_info/files/PveMod_pvemanagerlib.js b/src/node_info/files/PveMod_pvemanagerlib.js index 39504d5..904e9e4 100644 --- a/src/node_info/files/PveMod_pvemanagerlib.js +++ b/src/node_info/files/PveMod_pvemanagerlib.js @@ -1000,9 +1000,23 @@ Ext.define('PVE.node.StatusView', { if (value === null || value === undefined) { return ''; } - return value; + + const titleMap = { + manufacturer: 'Manufacturer', + product_name: 'Product Name', + serial_number: 'Serial Number', + }; + + let parts = []; + ['manufacturer', 'product_name', 'serial_number'].forEach(function(key) { + if (value[key] !== undefined && value[key] !== null) { + parts.push(titleMap[key] + ': ' + value[key]); + } + }); + + return parts.length > 0 ? parts.join(' | ') : ''; } - }, + }, ], updateTitle: function () { diff --git a/src/node_info/files/Store.pm b/src/node_info/files/Store.pm index 436aa59..aa7afb4 100644 --- a/src/node_info/files/Store.pm +++ b/src/node_info/files/Store.pm @@ -8,7 +8,7 @@ use File::Path qw(make_path); use PVE::INotify; use RRDs; -use PVE::PVEMod::Config qw($RRD_SOCKET $RRD_BASE); +use PVE::PVEMod::Config qw(%config $RRD_SOCKET $RRD_BASE); use PVE::PVEMod::Utils qw(debug); our @EXPORT_OK = qw( @@ -40,6 +40,8 @@ sub gpu_rrd_path { # ============================================================================ sub _ensure_intel_gpu_rrd { + return unless $config{gpu}{gpu_history}; + my ($card) = @_; my $path = gpu_rrd_path($card); return if -f $path; @@ -104,6 +106,8 @@ sub update_intel_gpu_rrd { # ============================================================================ sub _ensure_nvidia_gpu_rrd { + return unless $config{gpu}{gpu_history}; + my ($index) = @_; my $card = "nvidia$index"; my $path = gpu_rrd_path($card); diff --git a/src/node_info/files/Utils.pm b/src/node_info/files/Utils.pm index 8dfdfdb..2dcc765 100644 --- a/src/node_info/files/Utils.pm +++ b/src/node_info/files/Utils.pm @@ -215,6 +215,10 @@ sub safe_write_json { my ($filepath, $data, $pretty) = @_; $pretty //= 1; + # Untaint filepath for taint-mode environments (pveproxy runs with -T) + ($filepath) = ($filepath =~ /^([a-zA-Z0-9_\/\-\.]+)$/) + or do { debug(__LINE__, "Unsafe filepath rejected: $filepath"); return 0; }; + eval { open my $fh, '>', $filepath or die "Failed to open $filepath: $!"; my $json = $pretty ? JSON->new->pretty->encode($data) : encode_json($data); @@ -232,6 +236,10 @@ sub safe_write_json { sub safe_read_json { my ($filepath, $as_string) = @_; + # Untaint filepath + ($filepath) = ($filepath =~ /^([a-zA-Z0-9_\/\-\.]+)$/) + or return; + return unless -f $filepath; my $result; diff --git a/src/node_info/node_info.conf b/src/node_info/node_info.conf index 13c4ed4..be6cb7b 100644 --- a/src/node_info/node_info.conf +++ b/src/node_info/node_info.conf @@ -34,6 +34,7 @@ lm_sensors_mode=0 lm_sensors_output_file=/tmp/sensors-output.json intel_mode=0 intel_devices_file=/tmp/intel-gpu-devices.json +intel_output_file=/tmp/intel-gpu-output.txt nvidia_mode=0 nvidia_devices_file=/tmp/nvidia-smi-devices.csv nvidia_output_file=/tmp/nvidia-smi-output.csv diff --git a/src/node_info/patches/01-nodes-pm-sensors.patch b/src/node_info/patches/01-nodes-pm-sensors.patch index c744077..edaf974 100644 --- a/src/node_info/patches/01-nodes-pm-sensors.patch +++ b/src/node_info/patches/01-nodes-pm-sensors.patch @@ -1,6 +1,6 @@ --- a/usr/share/perl5/PVE/API2/Nodes.pm +++ b/usr/share/perl5/PVE/API2/Nodes.pm -@@ -536,6 +536,12 @@ +@@ -536,6 +536,13 @@ $res->{pveversion} = PVE::pvecfg::package() . "/" . PVE::pvecfg::version_text(); @@ -9,7 +9,8 @@ + $res->{PveMod_JsonSensorInfo} = PVE::API2::PVEMod_SensorInfo::get_sensors_info(); + $res->{PveMod_Version} = PVE::API2::PVEMod_SensorInfo::get_pve_mod_version(); + $res->{PveMod_upsInfo} = PVE::API2::PVEMod_SensorInfo::get_ups_info(); -+ $res->{PveMod_systemInfo} = PVE::API2::PVEMod_SensorInfo::get_system_info(); ++ $res->{PveMod_graphicsInfo} = PVE::API2::PVEMod_SensorInfo::get_graphics_info(); ++ $res->{PveMod_systemInfo} = PVE::API2::PVEMod_SensorInfo::get_system_information(); my $dinfo = df('/', 1); # output is bytes $res->{rootfs} = {