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 <na>
This commit is contained in:
Meliox 2026-06-14 13:07:38 +02:00 committed by GitHub
parent 8547e337b7
commit 5df83ec369
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 321 additions and 166 deletions

View File

@ -102,8 +102,14 @@ if ! command -v patch >/dev/null 2>&1; then
exit 1 exit 1
fi fi
for mod in $(list_modules); do 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 [[ "$(read_conf "$MAIN_CONF" modules "$mod" 0)" == "1" ]] || continue
fi
mod_dir="$PATCHES_DIR/$mod" mod_dir="$PATCHES_DIR/$mod"
manifest="$mod_dir/patches.list" manifest="$mod_dir/patches.list"

View File

@ -11,6 +11,7 @@ CONFD_DIR="/etc/pve-mod/conf.d"
NODE_INFO_CONF="${CONFD_DIR}/node_info.conf" NODE_INFO_CONF="${CONFD_DIR}/node_info.conf"
NAG_SCREEN_CONF="${CONFD_DIR}/nag_screen.conf" NAG_SCREEN_CONF="${CONFD_DIR}/nag_screen.conf"
APPLY_PATCHES="/usr/lib/pve-mod/apply-patches.sh" 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" NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm"
KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-") KNOWN_CPU_SENSORS=("coretemp-isa-" "k10temp-pci-")
@ -26,10 +27,33 @@ ask() {
echo "$response" echo "$response"
} }
bool() { [[ "$1" == true ]] && echo 1 || echo 0; } 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_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 [[ -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 while IFS= read -r line; do
case "$line" in case "$line" in
'#'*|'') continue ;; '#'*|'') continue ;;
@ -69,6 +93,7 @@ _load_conf() {
debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;; debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;;
debug.intel_mode) DEBUG_INTEL="$val" ;; debug.intel_mode) DEBUG_INTEL="$val" ;;
debug.intel_devices_file) DEBUG_INTEL_FILE="$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_mode) DEBUG_NVIDIA="$val" ;;
debug.nvidia_devices_file) DEBUG_NVIDIA_DEVICES_FILE="$val" ;; debug.nvidia_devices_file) DEBUG_NVIDIA_DEVICES_FILE="$val" ;;
debug.nvidia_output_file) DEBUG_NVIDIA_OUTPUT_FILE="$val" ;; debug.nvidia_output_file) DEBUG_NVIDIA_OUTPUT_FILE="$val" ;;
@ -281,7 +306,12 @@ configure_node_info() {
if [[ -n "$intelCards" ]]; then if [[ -n "$intelCards" ]]; then
info "Intel GPU(s) detected (debug):" info "Intel GPU(s) detected (debug):"
echo "$intelCards" | while IFS= read -r line; do echo " $line"; done 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 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 else
warn "No Intel GPUs in debug file." warn "No Intel GPUs in debug file."
fi fi
@ -291,6 +321,25 @@ configure_node_info() {
info "Intel GPU(s) detected:" info "Intel GPU(s) detected:"
echo "$intelCards" | while IFS= read -r line; do echo " $line"; done echo "$intelCards" | while IFS= read -r line; do echo " $line"; done
ENABLE_INTEL_GPU_INFO=1 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 else
warn "No Intel GPUs detected by intel_gpu_top." warn "No Intel GPUs detected by intel_gpu_top."
fi fi
@ -306,7 +355,12 @@ configure_node_info() {
if [[ -n "$nvidiaCards" ]]; then if [[ -n "$nvidiaCards" ]]; then
info "NVIDIA GPU(s) detected (debug):" info "NVIDIA GPU(s) detected (debug):"
echo "$nvidiaCards" | while IFS= read -r line; do echo " $line"; done 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 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 else
warn "No NVIDIA GPUs in debug file." warn "No NVIDIA GPUs in debug file."
fi fi
@ -347,16 +401,20 @@ configure_node_info() {
[yY]) [yY])
local upsConn modelName upsOutput local upsConn modelName upsOutput
upsConn=$(ask "Enter UPS connection string (e.g. upsname@hostname[:port])") upsConn=$(ask "Enter UPS connection string (e.g. upsname@hostname[:port])")
if [[ "$DEBUG_UPS" -eq 1 && -f "$DEBUG_UPS_FILE" ]]; then if [[ "$DEBUG_UPS" -eq 1 ]]; then
if [[ -f "$DEBUG_UPS_FILE" ]]; then
info "[debug] Using UPS data from $DEBUG_UPS_FILE" info "[debug] Using UPS data from $DEBUG_UPS_FILE"
upsOutput=$(cat "$DEBUG_UPS_FILE")
else else
if ! command -v upsc &>/dev/null; then warn "[debug] Debug mode for UPS is enabled but file not found at $DEBUG_UPS_FILE."
err "'upsc' is not available. Install 'nut-client' first."
fi 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 fi
if echo "$upsOutput" | grep -q "device.model:"; then if [[ -n "$upsOutput" ]]; then
modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d: -f2- | xargs) modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d: -f2- | xargs)
ENABLE_UPS=1 ENABLE_UPS=1
UPS_DEVICE_NAME="$upsConn" UPS_DEVICE_NAME="$upsConn"
@ -385,6 +443,15 @@ configure_node_info() {
[nN]) info "System information disabled." ;; [nN]) info "System information disabled." ;;
*) warn "Invalid selection. Defaulting to type 1."; ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=1 ;; *) warn "Invalid selection. Defaulting to type 1."; ENABLE_SYSTEM_INFO=1; SYSTEM_INFO_TYPE=1 ;;
esac 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 System info
} }
#endregion node-info wizard #endregion node-info wizard
@ -407,9 +474,6 @@ nag_screen=${MOD_NAG_SCREEN}
[pve_trigger] [pve_trigger]
enabled=${PVE_TRIGGER_ENABLED} enabled=${PVE_TRIGGER_ENABLED}
[service]
mode=embedded
EOF EOF
info "Main configuration saved to $CONF_FILE" 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} lm_sensors_output_file=${DEBUG_LM_SENSORS_FILE}
intel_mode=${DEBUG_INTEL} intel_mode=${DEBUG_INTEL}
intel_devices_file=${DEBUG_INTEL_FILE} intel_devices_file=${DEBUG_INTEL_FILE}
intel_output_file=${DEBUG_INTEL_OUTPUT_FILE}
nvidia_mode=${DEBUG_NVIDIA} nvidia_mode=${DEBUG_NVIDIA}
nvidia_devices_file=${DEBUG_NVIDIA_DEVICES_FILE} nvidia_devices_file=${DEBUG_NVIDIA_DEVICES_FILE}
nvidia_output_file=${DEBUG_NVIDIA_OUTPUT_FILE} nvidia_output_file=${DEBUG_NVIDIA_OUTPUT_FILE}
@ -473,69 +538,6 @@ EOF
} }
#endregion write config #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() { main() {
# ── Welcome banner ──────────────────────────────────────────────────────── # ── Welcome banner ────────────────────────────────────────────────────────
local _version local _version
@ -578,6 +580,7 @@ main() {
PVE_TRIGGER_ENABLED=0 PVE_TRIGGER_ENABLED=0
DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json" 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=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=0; DEBUG_NVIDIA_OUTPUT_FILE="/tmp/nvidia-smi-output.csv"
DEBUG_NVIDIA_DEVICES_FILE="/tmp/nvidia-smi-devices.csv" DEBUG_NVIDIA_DEVICES_FILE="/tmp/nvidia-smi-devices.csv"
DEBUG_AMD=0; DEBUG_AMD_FILE="/tmp/amd-gpu-devices.json" DEBUG_AMD=0; DEBUG_AMD_FILE="/tmp/amd-gpu-devices.json"
@ -590,40 +593,59 @@ main() {
# this run, and all other options retain their previously saved values. # this run, and all other options retain their previously saved values.
msgb "\n=== pve-mod Module Selection ===" msgb "\n=== pve-mod Module Selection ==="
echo "Available options (select one):" echo "Available options (select one):"
echo " [1] Node Info — sensor readings, GPU stats, UPS, system information" echo -e " [1] Node Info $(mod_status "$MOD_NODE_INFO") — sensor readings, GPU stats, UPS, system information"
echo " [2] Nag Screen — remove Proxmox subscription nag screen" echo -e " [2] Nag Screen $(mod_status "$MOD_NAG_SCREEN") — remove Proxmox subscription nag screen"
echo " [3] Auto re-patch on PVE upgrade — automatically re-apply patches when" 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 " pve-manager is upgraded"
echo " [n] None / cancel" echo " [n] None / cancel"
local modChoice local modChoice selected_mod="" patching_needed=0
modChoice=$(ask "Select an option to enable (1/2/3/n)") modChoice=$(ask "Select an option (1/2/3/n)")
case "$modChoice" in case "$modChoice" in
1) 1)
MOD_NODE_INFO=1 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 ===" msgb "\n=== Node Info Configuration ==="
configure_node_info configure_node_info
fi
;; ;;
2) 2)
MOD_NAG_SCREEN=1 selected_mod="nag_screen"
msgb "\n=== 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." info "Subscription nag screen removal will be applied."
fi
;; ;;
3) 3)
PVE_TRIGGER_ENABLED=1 if [[ "$PVE_TRIGGER_ENABLED" == "1" ]]; then
info "Auto re-patching on PVE upgrade enabled." 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 ;; [nN]) info "No option selected. Exiting."; exit 0 ;;
*) warn "Invalid selection. Exiting."; exit 0 ;; *) warn "Invalid selection. Exiting."; exit 0 ;;
esac esac
# ── Debug mode ──────────────────────────────────────────────────────────── # ── Write config and apply/revert ─────────────────────────────────────────
configure_debug # For mods with patches: apply if being enabled, revert if being disabled.
# Option 3 (pve_trigger) has no patches — write_config is sufficient.
# ── Write config and apply ──────────────────────────────────────────────── if [[ "$patching_needed" == "1" ]]; then
write_config
msgb "\n=== Applying patches ===" msgb "\n=== Applying patches ==="
"$APPLY_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=== Done ===" msgb "\n=== Done ==="
info "pve-mod is configured and active." info "pve-mod is configured and active."

View File

@ -39,7 +39,7 @@ revert_one_patch() {
fi fi
warn " $(basename "$patch") could not be reverted cleanly; manual cleanup may be needed" 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 patch -R -p1 -F0 -f --dry-run --verbose -d "$PVE_MOD_ROOT" < "$patch" >&2 || true
return 1 return 2
} }
# ── main ────────────────────────────────────────────────────────────────────── # ── main ──────────────────────────────────────────────────────────────────────
@ -51,10 +51,13 @@ fi
[[ -d "$PATCHES_DIR" ]] || exit 0 [[ -d "$PATCHES_DIR" ]] || exit 0
CHANGED=false 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 [[ -d "$mod_dir" ]] || continue
mod="$(basename "$mod_dir")"
manifest="$mod_dir/patches.list" manifest="$mod_dir/patches.list"
mod_conf="$CONFD_DIR/$mod.conf" mod_conf="$CONFD_DIR/$mod.conf"
@ -88,9 +91,13 @@ for mod_dir in "$PATCHES_DIR"/*/; do
patch_name="${patches[$i]}" patch_name="${patches[$i]}"
patch_file="$mod_dir/$patch_name" patch_file="$mod_dir/$patch_name"
[[ -f "$patch_file" ]] || continue [[ -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" info " reverted $patch_name"
CHANGED=true CHANGED=true
elif [[ "$rc" -eq 2 ]]; then
FAILED=true
fi fi
done done
done done
@ -99,4 +106,6 @@ if [[ "$CHANGED" == "true" ]]; then
info "Restarting pveproxy..." info "Restarting pveproxy..."
systemctl restart pveproxy 2>/dev/null || true systemctl restart pveproxy 2>/dev/null || true
fi fi
[[ "$FAILED" == "false" ]] || exit 1
exit 0 exit 0

View File

@ -5,7 +5,7 @@ use warnings;
use Exporter 'import'; use Exporter 'import';
use PVE::PVEMod::Config qw(%config $process_type $pve_mod_working_dir); 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); use PVE::PVEMod::Store qw(update_intel_gpu_rrd);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
@ -20,6 +20,23 @@ our @EXPORT_OK = qw(
sub get_intel_gpu_devices { sub get_intel_gpu_devices {
my @devices = (); my @devices = ();
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 => $dev->{card},
name => $dev->{name},
path => $dev->{path},
drm_path => $dev->{drm_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}");
}
} else {
debug(__LINE__, "Getting Intel GPU devices"); debug(__LINE__, "Getting Intel GPU devices");
if (open my $fh, '-|', 'intel_gpu_top -L') { if (open my $fh, '-|', 'intel_gpu_top -L') {
while (<$fh>) { while (<$fh>) {
@ -41,6 +58,7 @@ sub get_intel_gpu_devices {
} else { } else {
debug(__LINE__, "Failed to run intel_gpu_top -L: $!"); debug(__LINE__, "Failed to run intel_gpu_top -L: $!");
} }
}
return @devices; return @devices;
} }
@ -132,6 +150,41 @@ sub collector_for_intel_device {
if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0; if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0;
}); });
my $node_name = "node0";
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*$/;
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;
} 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"); debug(__LINE__, "About to open pipe to intel_gpu_top");
my $intel_pull_interval = $config{intervals}{data_pull} * 1000; # milliseconds my $intel_pull_interval = $config{intervals}{data_pull} * 1000; # milliseconds
$intel_gpu_top_pid = open(my $fh, '-|', $intel_gpu_top_pid = open(my $fh, '-|',
@ -144,8 +197,6 @@ sub collector_for_intel_device {
debug(__LINE__, "Pipe opened successfully, PID=$intel_gpu_top_pid"); debug(__LINE__, "Pipe opened successfully, PID=$intel_gpu_top_pid");
my $node_name = "node0";
while (my $line = <$fh>) { while (my $line = <$fh>) {
last if $shutdown; last if $shutdown;
chomp $line; chomp $line;
@ -172,6 +223,8 @@ sub collector_for_intel_device {
} }
close $fh; close $fh;
}
debug(__LINE__, "Collector for $device->{card} shutting down"); debug(__LINE__, "Collector for $device->{card} shutting down");
exit 0; exit 0;
} }

View File

@ -6,7 +6,7 @@ use Exporter 'import';
use JSON; 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); use PVE::PVEMod::Utils qw(debug setup_collector_signals);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
@ -40,7 +40,7 @@ sub collector_for_ups {
debug(__LINE__, "Error writing UPS data: $@"); 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"); debug(__LINE__, "UPS collector shutting down");
@ -56,7 +56,17 @@ sub _get_ups_status {
debug(__LINE__, "Collecting UPS status for $ups_name"); 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) { unless (defined $output && length($output) > 0) {
debug(__LINE__, "No output from upsc for $ups_name"); debug(__LINE__, "No output from upsc for $ups_name");

View File

@ -44,10 +44,16 @@ sub get_system_information_data {
sub _get_system_info { sub _get_system_info {
my ($type) = @_; 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) { 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 {}; return {};
} }

View File

@ -20,6 +20,13 @@ our @EXPORT_OK = qw(
our $DEBUG_ENABLED = 1; our $DEBUG_ENABLED = 1;
our $VERSION = 'version-placeholder'; 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. # Runtime process-type tag — set to 'worker' or 'collector' after fork.
# Each forked child gets its own copy of this variable. # Each forked child gets its own copy of this variable.
our $process_type = 'main'; # 'main', 'worker', or 'collector' our $process_type = 'main'; # 'main', 'worker', or 'collector'
@ -42,6 +49,7 @@ our %config = (
lm_sensors_output_file => '/tmp/sensors-output.json', lm_sensors_output_file => '/tmp/sensors-output.json',
intel_mode => 0, intel_mode => 0,
intel_devices_file => '/tmp/intel-gpu-devices.json', intel_devices_file => '/tmp/intel-gpu-devices.json',
intel_output_file => '/tmp/intel-gpu-output.json',
nvidia_mode => 0, nvidia_mode => 0,
nvidia_devices_file => '/tmp/nvidia-smi-devices.csv', nvidia_devices_file => '/tmp/nvidia-smi-devices.csv',
nvidia_output_file => '/tmp/nvidia-smi-output.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 { sub _load_ini_file {
my $path = '/etc/pve-mod/pve-mod.conf'; my ($path) = @_;
return unless -f $path; return unless defined $path && -f $path;
open my $fh, '<', $path or return; open my $fh, '<', $path or return;
my $section = ''; my $section = '';
@ -146,6 +154,7 @@ sub _load_ini_file {
close $fh; close $fh;
} }
_load_ini_file(); _load_ini_file($CONF_FILE);
_load_ini_file($_) for glob("$CONFD_DIR/*.conf");
1; 1;

View File

@ -149,8 +149,12 @@ sub _load_graphics_data {
# API calls # API calls
# ============================================================================ # ============================================================================
sub get_graphic_info { sub get_graphics_info {
debug(__LINE__, "get_graphic_info called"); 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 # Start PVE Mod
pve_mod_starter(); pve_mod_starter();
@ -165,6 +169,10 @@ sub get_graphic_info {
sub get_sensors_info { sub get_sensors_info {
debug(__LINE__, "get_sensors_info called"); debug(__LINE__, "get_sensors_info called");
if (!$config{lm_sensors}{enabled}) {
debug(__LINE__, "LM Sensors collection is disabled");
return {};
}
# Start PVE Mod # Start PVE Mod
pve_mod_starter(); pve_mod_starter();
@ -179,6 +187,10 @@ sub get_sensors_info {
sub get_ups_info { sub get_ups_info {
debug(__LINE__, "get_ups_info called"); debug(__LINE__, "get_ups_info called");
if (!$config{ups}{enabled}) {
debug(__LINE__, "UPS collection is disabled");
return {};
}
# Start PVE Mod # Start PVE Mod
pve_mod_starter(); pve_mod_starter();

View File

@ -1000,7 +1000,21 @@ Ext.define('PVE.node.StatusView', {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return ''; 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(' | ') : '';
} }
}, },
], ],

View File

@ -8,7 +8,7 @@ use File::Path qw(make_path);
use PVE::INotify; use PVE::INotify;
use RRDs; 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); use PVE::PVEMod::Utils qw(debug);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
@ -40,6 +40,8 @@ sub gpu_rrd_path {
# ============================================================================ # ============================================================================
sub _ensure_intel_gpu_rrd { sub _ensure_intel_gpu_rrd {
return unless $config{gpu}{gpu_history};
my ($card) = @_; my ($card) = @_;
my $path = gpu_rrd_path($card); my $path = gpu_rrd_path($card);
return if -f $path; return if -f $path;
@ -104,6 +106,8 @@ sub update_intel_gpu_rrd {
# ============================================================================ # ============================================================================
sub _ensure_nvidia_gpu_rrd { sub _ensure_nvidia_gpu_rrd {
return unless $config{gpu}{gpu_history};
my ($index) = @_; my ($index) = @_;
my $card = "nvidia$index"; my $card = "nvidia$index";
my $path = gpu_rrd_path($card); my $path = gpu_rrd_path($card);

View File

@ -215,6 +215,10 @@ sub safe_write_json {
my ($filepath, $data, $pretty) = @_; my ($filepath, $data, $pretty) = @_;
$pretty //= 1; $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 { eval {
open my $fh, '>', $filepath or die "Failed to open $filepath: $!"; open my $fh, '>', $filepath or die "Failed to open $filepath: $!";
my $json = $pretty ? JSON->new->pretty->encode($data) : encode_json($data); my $json = $pretty ? JSON->new->pretty->encode($data) : encode_json($data);
@ -232,6 +236,10 @@ sub safe_write_json {
sub safe_read_json { sub safe_read_json {
my ($filepath, $as_string) = @_; my ($filepath, $as_string) = @_;
# Untaint filepath
($filepath) = ($filepath =~ /^([a-zA-Z0-9_\/\-\.]+)$/)
or return;
return unless -f $filepath; return unless -f $filepath;
my $result; my $result;

View File

@ -34,6 +34,7 @@ lm_sensors_mode=0
lm_sensors_output_file=/tmp/sensors-output.json lm_sensors_output_file=/tmp/sensors-output.json
intel_mode=0 intel_mode=0
intel_devices_file=/tmp/intel-gpu-devices.json intel_devices_file=/tmp/intel-gpu-devices.json
intel_output_file=/tmp/intel-gpu-output.txt
nvidia_mode=0 nvidia_mode=0
nvidia_devices_file=/tmp/nvidia-smi-devices.csv nvidia_devices_file=/tmp/nvidia-smi-devices.csv
nvidia_output_file=/tmp/nvidia-smi-output.csv nvidia_output_file=/tmp/nvidia-smi-output.csv

View File

@ -1,6 +1,6 @@
--- a/usr/share/perl5/PVE/API2/Nodes.pm --- a/usr/share/perl5/PVE/API2/Nodes.pm
+++ b/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(); $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_JsonSensorInfo} = PVE::API2::PVEMod_SensorInfo::get_sensors_info();
+ $res->{PveMod_Version} = PVE::API2::PVEMod_SensorInfo::get_pve_mod_version(); + $res->{PveMod_Version} = PVE::API2::PVEMod_SensorInfo::get_pve_mod_version();
+ $res->{PveMod_upsInfo} = PVE::API2::PVEMod_SensorInfo::get_ups_info(); + $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 my $dinfo = df('/', 1); # output is bytes
$res->{rootfs} = { $res->{rootfs} = {