change all text files to use LF (#205)

* test t

* change all files to use LF

* g

---------

Co-authored-by: Meliox <na>
This commit is contained in:
Meliox 2026-06-11 22:34:25 +02:00 committed by GitHub
parent 0ab8a67152
commit e05ff6f2fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 4628 additions and 4626 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# All files must use LF
* text=auto eol=lf

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
# pve-mod :: nag_screen file manifest # pve-mod :: nag_screen file manifest
# Maps files in this directory to their installation destinations. # Maps files in this directory to their installation destinations.
# Format: <source> <destination> [permission] # Format: <source> <destination> [permission]
# source - path relative to this files/ directory # source - path relative to this files/ directory
# destination - path relative to the package root (no leading slash) # destination - path relative to the package root (no leading slash)
# permission - octal mode, optional (defaults to 644) # permission - octal mode, optional (defaults to 644)
# Read by src/gen-rules.sh to generate the per-module debian install rules. # Read by src/gen-rules.sh to generate the per-module debian install rules.
# #
# The nag_screen mod ships no new files - it only patches existing Proxmox # The nag_screen mod ships no new files - it only patches existing Proxmox
# files - so this manifest is intentionally empty. # files - so this manifest is intentionally empty.

View File

@ -1,3 +1,3 @@
# pve-mod :: nag_screen mod configuration # pve-mod :: nag_screen mod configuration
# The nag-screen mod has no tunable settings; this file is a placeholder # The nag-screen mod has no tunable settings; this file is a placeholder
# kept for consistency with the per-mod conf.d layout. # kept for consistency with the per-mod conf.d layout.

View File

@ -1,5 +1,5 @@
# pve-mod :: nag_screen patch manifest # pve-mod :: nag_screen patch manifest
# Format: <patch-file> [section.key=value] # Format: <patch-file> [section.key=value]
01-proxmoxlib-js-nagscreen.patch 01-proxmoxlib-js-nagscreen.patch
02-index-html-tpl-mobilenag.patch 02-index-html-tpl-mobilenag.patch

View File

@ -1,33 +1,33 @@
package PVE::PVEMod::Collector::Amd; package PVE::PVEMod::Collector::Amd;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use PVE::PVEMod::Config qw($process_type); use PVE::PVEMod::Config qw($process_type);
use PVE::PVEMod::Utils qw(debug); use PVE::PVEMod::Utils qw(debug);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
get_amd_gpu_devices get_amd_gpu_devices
collector_for_amd_device collector_for_amd_device
); );
# ============================================================================ # ============================================================================
# AMD GPU — placeholders (not yet implemented) # AMD GPU — placeholders (not yet implemented)
# ============================================================================ # ============================================================================
sub get_amd_gpu_devices { sub get_amd_gpu_devices {
# TODO: Implement AMD GPU detection using rocminfo or rocm-smi # TODO: Implement AMD GPU detection using rocminfo or rocm-smi
debug(__LINE__, "AMD GPU support not yet implemented"); debug(__LINE__, "AMD GPU support not yet implemented");
return (); return ();
} }
sub collector_for_amd_device { sub collector_for_amd_device {
my ($device) = @_; my ($device) = @_;
$process_type = 'collector'; $process_type = 'collector';
# TODO: Implement AMD GPU collector # TODO: Implement AMD GPU collector
debug(__LINE__, "AMD GPU collector not yet implemented"); debug(__LINE__, "AMD GPU collector not yet implemented");
exit 0; exit 0;
} }
1; 1;

View File

@ -1,179 +1,179 @@
package PVE::PVEMod::Collector::Intel; package PVE::PVEMod::Collector::Intel;
use strict; use strict;
use warnings; 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_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(
get_intel_gpu_devices get_intel_gpu_devices
collector_for_intel_device collector_for_intel_device
); );
# ============================================================================ # ============================================================================
# Intel GPU — device discovery # Intel GPU — device discovery
# ============================================================================ # ============================================================================
sub get_intel_gpu_devices { sub get_intel_gpu_devices {
my @devices = (); my @devices = ();
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>) {
chomp; chomp;
# Parse: "card0 Intel Alderlake_n (Gen12) pci:vendor=8086,device=46D0,card=0" # Parse: "card0 Intel Alderlake_n (Gen12) pci:vendor=8086,device=46D0,card=0"
# or: "card0 Intel Alderlake_n (Gen12) pci:0000:00:02.0" # or: "card0 Intel Alderlake_n (Gen12) pci:0000:00:02.0"
if (/^(card\d+)\s+(.+?)\s+(pci:[^\s]+)/) { if (/^(card\d+)\s+(.+?)\s+(pci:[^\s]+)/) {
my ($card, $name, $path) = ($1, $2, $3); my ($card, $name, $path) = ($1, $2, $3);
push @devices, { push @devices, {
card => $card, card => $card,
name => $name, name => $name,
path => $path, path => $path,
drm_path => "/dev/dri/$card", drm_path => "/dev/dri/$card",
}; };
debug(__LINE__, "Found Intel device: $card -> $name ($path)"); debug(__LINE__, "Found Intel device: $card -> $name ($path)");
} }
} }
close $fh; close $fh;
} else { } else {
debug(__LINE__, "Failed to run intel_gpu_top -L: $!"); debug(__LINE__, "Failed to run intel_gpu_top -L: $!");
} }
return @devices; return @devices;
} }
# ============================================================================ # ============================================================================
# Intel GPU — data parsing # Intel GPU — data parsing
# ============================================================================ # ============================================================================
sub _parse_intel_gpu_line { sub _parse_intel_gpu_line {
my ($line) = @_; my ($line) = @_;
# Expected format (whitespace-aligned columns): # Expected format (whitespace-aligned columns):
# Freq MHz IRQ RC6 Power W RCS BCS VCS VECS # Freq MHz IRQ RC6 Power W RCS BCS VCS VECS
# req act /s % gpu pkg % se wa % se wa % se wa % se wa # req act /s % gpu pkg % se wa % se wa % se wa % se wa
# 0 0 0 0 0.00 7.47 0.00 0 0 0.00 0 0 0.00 0 0 0.00 0 0 # 0 0 0 0 0.00 7.47 0.00 0 0 0.00 0 0 0.00 0 0 0.00 0 0
$line =~ s/^\s+|\s+$//g; $line =~ s/^\s+|\s+$//g;
my @values = grep { $_ ne '' } split(/\s+/, $line); my @values = grep { $_ ne '' } split(/\s+/, $line);
return unless @values >= 18; return unless @values >= 18;
return { return {
frequency => { frequency => {
requested => $values[0] + 0.0, requested => $values[0] + 0.0,
actual => $values[1] + 0.0, actual => $values[1] + 0.0,
unit => "MHz", unit => "MHz",
}, },
interrupts => { interrupts => {
count => $values[2] + 0.0, count => $values[2] + 0.0,
unit => "irq/s", unit => "irq/s",
}, },
rc6 => { rc6 => {
value => $values[3] + 0.0, value => $values[3] + 0.0,
unit => "%", unit => "%",
}, },
power => { power => {
GPU => $values[4] + 0.0, GPU => $values[4] + 0.0,
Package => $values[5] + 0.0, Package => $values[5] + 0.0,
unit => "W", unit => "W",
}, },
engines => { engines => {
'Render/3D' => { 'Render/3D' => {
busy => $values[6] + 0.0, busy => $values[6] + 0.0,
sema => $values[7] + 0.0, sema => $values[7] + 0.0,
wait => $values[8] + 0.0, wait => $values[8] + 0.0,
unit => "%", unit => "%",
}, },
Blitter => { Blitter => {
busy => $values[9] + 0.0, busy => $values[9] + 0.0,
sema => $values[10] + 0.0, sema => $values[10] + 0.0,
wait => $values[11] + 0.0, wait => $values[11] + 0.0,
unit => "%", unit => "%",
}, },
Video => { Video => {
busy => $values[12] + 0.0, busy => $values[12] + 0.0,
sema => $values[13] + 0.0, sema => $values[13] + 0.0,
wait => $values[14] + 0.0, wait => $values[14] + 0.0,
unit => "%", unit => "%",
}, },
VideoEnhance => { VideoEnhance => {
busy => $values[15] + 0.0, busy => $values[15] + 0.0,
sema => $values[16] + 0.0, sema => $values[16] + 0.0,
wait => $values[17] + 0.0, wait => $values[17] + 0.0,
unit => "%", unit => "%",
}, },
}, },
clients => {}, clients => {},
}; };
} }
# ============================================================================ # ============================================================================
# Intel GPU — long-running collector # Intel GPU — long-running collector
# ============================================================================ # ============================================================================
sub collector_for_intel_device { sub collector_for_intel_device {
my ($device) = @_; my ($device) = @_;
$process_type = 'collector'; $process_type = 'collector';
$0 = "collector-gpu-intel-$device->{card}"; $0 = "collector-gpu-intel-$device->{card}";
my $drm_dev = "drm:/dev/dri/$device->{card}"; my $drm_dev = "drm:/dev/dri/$device->{card}";
my $intel_gpu_top_pid = undef; my $intel_gpu_top_pid = undef;
my $device_state_file = "$pve_mod_working_dir/stats-$device->{card}.json"; my $device_state_file = "$pve_mod_working_dir/stats-$device->{card}.json";
debug(__LINE__, "Collector started for device: $drm_dev, writing to $device_state_file"); debug(__LINE__, "Collector started for device: $drm_dev, writing to $device_state_file");
my $shutdown = 0; my $shutdown = 0;
setup_collector_signals($device->{card}, \$shutdown, sub { setup_collector_signals($device->{card}, \$shutdown, sub {
kill 'TERM', $intel_gpu_top_pid kill 'TERM', $intel_gpu_top_pid
if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0; if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0;
}); });
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, '-|',
"intel_gpu_top -d $drm_dev -s $intel_pull_interval -l 2>&1"); "intel_gpu_top -d $drm_dev -s $intel_pull_interval -l 2>&1");
unless (defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0) { unless (defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0) {
debug(__LINE__, "Failed to run intel_gpu_top for $drm_dev: $!"); debug(__LINE__, "Failed to run intel_gpu_top for $drm_dev: $!");
exit 1; exit 1;
} }
debug(__LINE__, "Pipe opened successfully, PID=$intel_gpu_top_pid"); debug(__LINE__, "Pipe opened successfully, PID=$intel_gpu_top_pid");
my $node_name = "node0"; my $node_name = "node0";
while (my $line = <$fh>) { while (my $line = <$fh>) {
last if $shutdown; last if $shutdown;
chomp $line; 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\.]+$/) { if ($line =~ /^\s*[\d\s\.]+$/) {
my $stats = _parse_intel_gpu_line($line); my $stats = _parse_intel_gpu_line($line);
if ($stats) { if ($stats) {
my $device_data = { my $device_data = {
$node_name => { $node_name => {
name => $device->{name}, name => $device->{name},
device_path => $device->{path}, device_path => $device->{path},
drm_path => $device->{drm_path}, drm_path => $device->{drm_path},
stats => $stats, stats => $stats,
} }
}; };
safe_write_json($device_state_file, $device_data); safe_write_json($device_state_file, $device_data);
update_intel_gpu_rrd($device->{card}, $stats); update_intel_gpu_rrd($device->{card}, $stats);
} }
} }
} }
close $fh; close $fh;
debug(__LINE__, "Collector for $device->{card} shutting down"); debug(__LINE__, "Collector for $device->{card} shutting down");
exit 0; exit 0;
} }
1; 1;

View File

@ -1,417 +1,417 @@
package PVE::PVEMod::Collector::LmSensors; package PVE::PVEMod::Collector::LmSensors;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use JSON; use JSON;
use PVE::PVEMod::Config qw(%config $process_type $sensors_state_file); use PVE::PVEMod::Config qw(%config $process_type $sensors_state_file);
use PVE::PVEMod::Utils qw(debug check_executable setup_collector_signals read_sysfs); use PVE::PVEMod::Utils qw(debug check_executable setup_collector_signals read_sysfs);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
collector_for_temperature_sensors collector_for_temperature_sensors
); );
# ============================================================================ # ============================================================================
# Temperature Sensors — long-running collector # Temperature Sensors — long-running collector
# ============================================================================ # ============================================================================
sub collector_for_temperature_sensors { sub collector_for_temperature_sensors {
my ($device) = @_; my ($device) = @_;
$process_type = 'collector'; $process_type = 'collector';
$0 = "collector-temperature-sensors"; $0 = "collector-temperature-sensors";
my %cache; my %cache;
my $shutdown = 0; my $shutdown = 0;
setup_collector_signals('temperature-sensors', \$shutdown); setup_collector_signals('temperature-sensors', \$shutdown);
while (!$shutdown) { while (!$shutdown) {
my $sensors_data = _get_temperature_sensors(\%cache); my $sensors_data = _get_temperature_sensors(\%cache);
eval { eval {
open my $ofh, '>', $sensors_state_file open my $ofh, '>', $sensors_state_file
or die "Failed to open $sensors_state_file: $!"; or die "Failed to open $sensors_state_file: $!";
print $ofh $sensors_data; print $ofh $sensors_data;
close $ofh; close $ofh;
debug(__LINE__, "Wrote temperature sensor data to $sensors_state_file"); debug(__LINE__, "Wrote temperature sensor data to $sensors_state_file");
}; };
if ($@) { if ($@) {
debug(__LINE__, "Error writing temperature sensor data: $@"); debug(__LINE__, "Error writing temperature sensor data: $@");
} }
sleep $config{intervals}{data_pull} unless $shutdown; sleep $config{intervals}{data_pull} unless $shutdown;
} }
debug(__LINE__, "Temperature sensor collector shutting down"); debug(__LINE__, "Temperature sensor collector shutting down");
exit 0; exit 0;
} }
# ============================================================================ # ============================================================================
# Temperature Sensors — pipeline # Temperature Sensors — pipeline
# ============================================================================ # ============================================================================
sub _get_temperature_sensors { sub _get_temperature_sensors {
my ($cache_ref) = @_; my ($cache_ref) = @_;
my $sensors_output; my $sensors_output;
if ($config{debug}{lm_sensors_mode} && -f $config{debug}{lm_sensors_output_file}) { if ($config{debug}{lm_sensors_mode} && -f $config{debug}{lm_sensors_output_file}) {
debug(__LINE__, "Debug mode: reading lm-sensors data from $config{debug}{lm_sensors_output_file}"); debug(__LINE__, "Debug mode: reading lm-sensors data from $config{debug}{lm_sensors_output_file}");
if (open my $fh, '<', $config{debug}{lm_sensors_output_file}) { if (open my $fh, '<', $config{debug}{lm_sensors_output_file}) {
local $/; local $/;
$sensors_output = <$fh>; $sensors_output = <$fh>;
close $fh; close $fh;
debug(__LINE__, "Read lm-sensors data from debug file, length: " debug(__LINE__, "Read lm-sensors data from debug file, length: "
. length($sensors_output) . " bytes"); . length($sensors_output) . " bytes");
} else { } else {
debug(__LINE__, "Failed to open debug file $config{debug}{lm_sensors_output_file}: $!"); debug(__LINE__, "Failed to open debug file $config{debug}{lm_sensors_output_file}: $!");
$sensors_output = '{}'; $sensors_output = '{}';
} }
} else { } else {
$sensors_output = `sensors -j 2>/dev/null | python3 -m json.tool`; $sensors_output = `sensors -j 2>/dev/null | python3 -m json.tool`;
debug(__LINE__, "Raw lm-sensors output collected from command"); debug(__LINE__, "Raw lm-sensors output collected from command");
} }
debug(__LINE__, "Raw lm-sensors output collected"); debug(__LINE__, "Raw lm-sensors output collected");
my $data = _sanitize_sensors($sensors_output); my $data = _sanitize_sensors($sensors_output);
debug(__LINE__, "Sanitized lm-sensors output"); debug(__LINE__, "Sanitized lm-sensors output");
$data = _get_drive_names($data, $cache_ref); $data = _get_drive_names($data, $cache_ref);
debug(__LINE__, "Translated drive names in lm-sensors output"); debug(__LINE__, "Translated drive names in lm-sensors output");
$data = _get_cpu_name($data, $cache_ref); $data = _get_cpu_name($data, $cache_ref);
debug(__LINE__, "Translated CPU names in lm-sensors output"); debug(__LINE__, "Translated CPU names in lm-sensors output");
# Wrap in top-level key # Wrap in top-level key
my $sensors_json; my $sensors_json;
eval { $sensors_json = decode_json($data); }; eval { $sensors_json = decode_json($data); };
if ($@) { if ($@) {
debug(__LINE__, "Failed to parse final lm-sensors JSON: $@"); debug(__LINE__, "Failed to parse final lm-sensors JSON: $@");
return $data; return $data;
} }
$data = JSON->new->pretty->encode({ "PVE MOD lm-sensors Enhanced" => $sensors_json }); $data = JSON->new->pretty->encode({ "PVE MOD lm-sensors Enhanced" => $sensors_json });
return $data; return $data;
} }
# ============================================================================ # ============================================================================
# Sanitize raw lm-sensors JSON # Sanitize raw lm-sensors JSON
# ============================================================================ # ============================================================================
sub _sanitize_sensors { sub _sanitize_sensors {
my ($sensors_output) = @_; my ($sensors_output) = @_;
$sensors_output =~ s/ERROR:.+\s(\w+):\s(.+)/\"$1\": 0.000,/g; $sensors_output =~ s/ERROR:.+\s(\w+):\s(.+)/\"$1\": 0.000,/g;
$sensors_output =~ s/ERROR:.+\s(\w+)!/\"$1\": 0.000,/g; $sensors_output =~ s/ERROR:.+\s(\w+)!/\"$1\": 0.000,/g;
$sensors_output =~ s/,\s*(})/$1/g; $sensors_output =~ s/,\s*(})/$1/g;
$sensors_output =~ s/\bNaN\b/null/g; $sensors_output =~ s/\bNaN\b/null/g;
# Fix duplicate SODIMM keys: "SODIMM":{"temp3_input":34.0} → "SODIMM3":{...} # Fix duplicate SODIMM keys: "SODIMM":{"temp3_input":34.0} → "SODIMM3":{...}
$sensors_output =~ $sensors_output =~
s/\"SODIMM\":\{\"temp(\d+)_input\"/\"SODIMM$1\":\{\"temp$1_input\"/g; s/\"SODIMM\":\{\"temp(\d+)_input\"/\"SODIMM$1\":\{\"temp$1_input\"/g;
return $sensors_output; return $sensors_output;
} }
# ============================================================================ # ============================================================================
# Enrich lm-sensors data with drive device info # Enrich lm-sensors data with drive device info
# ============================================================================ # ============================================================================
sub _get_drive_names { sub _get_drive_names {
my ($sensors_output, $cache_ref) = @_; my ($sensors_output, $cache_ref) = @_;
$cache_ref //= {}; $cache_ref //= {};
my $sensors_data; my $sensors_data;
eval { $sensors_data = decode_json($sensors_output); }; eval { $sensors_data = decode_json($sensors_output); };
if ($@) { if ($@) {
debug(__LINE__, "Failed to parse sensors JSON: $@"); debug(__LINE__, "Failed to parse sensors JSON: $@");
return $sensors_output; return $sensors_output;
} }
my @entries = grep { my @entries = grep {
/^drivetemp-scsi-/ || /^drivetemp-nvme-/ || /^nvme-pci-/ /^drivetemp-scsi-/ || /^drivetemp-nvme-/ || /^nvme-pci-/
} keys %{$sensors_data}; } keys %{$sensors_data};
debug(__LINE__, "Found " . scalar(@entries) . " drive entries in lm-sensors output"); debug(__LINE__, "Found " . scalar(@entries) . " drive entries in lm-sensors output");
my @drive_names; my @drive_names;
foreach my $entry (@entries) { foreach my $entry (@entries) {
my ($dev_path, $model, $serial) = ("unknown", "unknown", "unknown"); my ($dev_path, $model, $serial) = ("unknown", "unknown", "unknown");
if (exists $cache_ref->{$entry}) { if (exists $cache_ref->{$entry}) {
my $cached = $cache_ref->{$entry}; my $cached = $cache_ref->{$entry};
$dev_path = $cached->{device_path}; $dev_path = $cached->{device_path};
$model = $cached->{model}; $model = $cached->{model};
$serial = $cached->{serial}; $serial = $cached->{serial};
debug(__LINE__, "Using cached drive info for $entry"); debug(__LINE__, "Using cached drive info for $entry");
} else { } else {
# ----- SCSI/SATA ----- # ----- SCSI/SATA -----
if ($entry =~ /^drivetemp-scsi-(\d+)-(\d+)/) { if ($entry =~ /^drivetemp-scsi-(\d+)-(\d+)/) {
my ($host, $id) = ($1, $2); my ($host, $id) = ($1, $2);
my $scsi_path = "/sys/class/scsi_disk/$host:$id:0:0/device/block"; my $scsi_path = "/sys/class/scsi_disk/$host:$id:0:0/device/block";
if (opendir(my $sdh, $scsi_path)) { if (opendir(my $sdh, $scsi_path)) {
my @devs = grep { /^sd/ } readdir($sdh); my @devs = grep { /^sd/ } readdir($sdh);
closedir($sdh); closedir($sdh);
if (@devs) { if (@devs) {
$dev_path = "/dev/$devs[0]"; $dev_path = "/dev/$devs[0]";
$model = read_sysfs("/sys/class/block/$devs[0]/device/model"); $model = read_sysfs("/sys/class/block/$devs[0]/device/model");
$serial = read_sysfs("/sys/class/block/$devs[0]/device/serial"); $serial = read_sysfs("/sys/class/block/$devs[0]/device/serial");
} }
} }
# ----- Numeric NVMe ----- # ----- Numeric NVMe -----
} elsif ($entry =~ /^drivetemp-nvme-(\d+)/) { } elsif ($entry =~ /^drivetemp-nvme-(\d+)/) {
my $nvme_index = $1; my $nvme_index = $1;
$dev_path = "/dev/nvme${nvme_index}n1"; $dev_path = "/dev/nvme${nvme_index}n1";
if (-e $dev_path) { if (-e $dev_path) {
$model = read_sysfs("/sys/class/block/nvme${nvme_index}n1/device/model"); $model = read_sysfs("/sys/class/block/nvme${nvme_index}n1/device/model");
$serial = read_sysfs("/sys/class/block/nvme${nvme_index}n1/device/serial"); $serial = read_sysfs("/sys/class/block/nvme${nvme_index}n1/device/serial");
} }
# ----- PCI-style NVMe ----- # ----- PCI-style NVMe -----
} elsif ($entry =~ /^nvme-pci-(\w+)/) { } elsif ($entry =~ /^nvme-pci-(\w+)/) {
my $pci_addr = $1; my $pci_addr = $1;
# Convert short PCI address (e.g. "0600") to pattern (e.g. "0000:06:00") # Convert short PCI address (e.g. "0600") to pattern (e.g. "0000:06:00")
my $pci_pattern; my $pci_pattern;
if ($pci_addr =~ /^([0-9a-f]{2})([0-9a-f]{2})$/i) { if ($pci_addr =~ /^([0-9a-f]{2})([0-9a-f]{2})$/i) {
my ($bus, $dev) = ($1, $2); my ($bus, $dev) = ($1, $2);
$pci_pattern = sprintf("%04x:%02x:%02x", 0, hex($bus), hex($dev)); $pci_pattern = sprintf("%04x:%02x:%02x", 0, hex($bus), hex($dev));
debug(__LINE__, "Converted PCI address $pci_addr to pattern $pci_pattern"); debug(__LINE__, "Converted PCI address $pci_addr to pattern $pci_pattern");
} else { } else {
$pci_pattern = $pci_addr; $pci_pattern = $pci_addr;
} }
my $found = 0; my $found = 0;
my $nvme_dir = "/sys/class/nvme"; my $nvme_dir = "/sys/class/nvme";
debug(__LINE__, debug(__LINE__,
"Searching for NVMe devices in $nvme_dir matching PCI pattern $pci_pattern"); "Searching for NVMe devices in $nvme_dir matching PCI pattern $pci_pattern");
if (opendir(my $ndh, $nvme_dir)) { if (opendir(my $ndh, $nvme_dir)) {
my @nvme_devs = my @nvme_devs =
grep { /^nvme\d+$/ && -d "$nvme_dir/$_" } readdir($ndh); grep { /^nvme\d+$/ && -d "$nvme_dir/$_" } readdir($ndh);
closedir($ndh); closedir($ndh);
debug(__LINE__, "Found NVMe devices: " . join(", ", @nvme_devs)); debug(__LINE__, "Found NVMe devices: " . join(", ", @nvme_devs));
foreach my $nvme_dev (@nvme_devs) { foreach my $nvme_dev (@nvme_devs) {
my $device_link = readlink("$nvme_dir/$nvme_dev/device"); my $device_link = readlink("$nvme_dir/$nvme_dev/device");
if ($device_link && $device_link =~ /$pci_pattern/) { if ($device_link && $device_link =~ /$pci_pattern/) {
debug(__LINE__, debug(__LINE__,
"NVMe device $nvme_dev matches PCI pattern $pci_pattern"); "NVMe device $nvme_dev matches PCI pattern $pci_pattern");
$dev_path = "/dev/${nvme_dev}n1"; $dev_path = "/dev/${nvme_dev}n1";
$model = read_sysfs("$nvme_dir/$nvme_dev/model"); $model = read_sysfs("$nvme_dir/$nvme_dev/model");
$serial = read_sysfs("$nvme_dir/$nvme_dev/serial"); $serial = read_sysfs("$nvme_dir/$nvme_dev/serial");
$found = 1; $found = 1;
debug(__LINE__, debug(__LINE__,
"Found NVMe device via /sys/class/nvme: $dev_path"); "Found NVMe device via /sys/class/nvme: $dev_path");
last; last;
} }
debug(__LINE__, debug(__LINE__,
"NVMe device $nvme_dev did not match PCI pattern $pci_pattern"); "NVMe device $nvme_dev did not match PCI pattern $pci_pattern");
} }
} }
# Fallback: scan /sys/class/block # Fallback: scan /sys/class/block
if (!$found && opendir(my $bdh, "/sys/class/block")) { if (!$found && opendir(my $bdh, "/sys/class/block")) {
my @block_devs = grep { /^nvme\d+n\d+$/ } readdir($bdh); my @block_devs = grep { /^nvme\d+n\d+$/ } readdir($bdh);
closedir($bdh); closedir($bdh);
foreach my $block_dev (@block_devs) { foreach my $block_dev (@block_devs) {
my $device_link = my $device_link =
readlink("/sys/class/block/$block_dev/device"); readlink("/sys/class/block/$block_dev/device");
if ($device_link && $device_link =~ /$pci_pattern/) { if ($device_link && $device_link =~ /$pci_pattern/) {
$dev_path = "/dev/$block_dev"; $dev_path = "/dev/$block_dev";
(my $nvme_ctrl = $block_dev) =~ s/n\d+$//; (my $nvme_ctrl = $block_dev) =~ s/n\d+$//;
$model = read_sysfs("/sys/class/nvme/$nvme_ctrl/model"); $model = read_sysfs("/sys/class/nvme/$nvme_ctrl/model");
$serial = read_sysfs("/sys/class/nvme/$nvme_ctrl/serial"); $serial = read_sysfs("/sys/class/nvme/$nvme_ctrl/serial");
$found = 1; $found = 1;
debug(__LINE__, debug(__LINE__,
"Found NVMe device via /sys/class/block: $dev_path"); "Found NVMe device via /sys/class/block: $dev_path");
last; last;
} }
} }
} }
unless ($found) { unless ($found) {
debug(__LINE__, debug(__LINE__,
"Could not find device for nvme-pci-$pci_addr (pattern: $pci_pattern)"); "Could not find device for nvme-pci-$pci_addr (pattern: $pci_pattern)");
} }
} else { } else {
next; next;
} }
$cache_ref->{$entry} = { $cache_ref->{$entry} = {
device_path => $dev_path, device_path => $dev_path,
model => $model, model => $model,
serial => $serial, serial => $serial,
}; };
debug(__LINE__, "Drive: $entry -> $dev_path (Model: $model, Serial: $serial)"); debug(__LINE__, "Drive: $entry -> $dev_path (Model: $model, Serial: $serial)");
} }
push @drive_names, [$entry, $dev_path, $model, $serial]; push @drive_names, [$entry, $dev_path, $model, $serial];
} }
foreach my $drive_entry (@drive_names) { foreach my $drive_entry (@drive_names) {
my ($original_name, $dev_path, $model, $serial) = @$drive_entry; my ($original_name, $dev_path, $model, $serial) = @$drive_entry;
if (exists $sensors_data->{$original_name}) { if (exists $sensors_data->{$original_name}) {
$sensors_data->{$original_name}->{device_path} = $dev_path; $sensors_data->{$original_name}->{device_path} = $dev_path;
$sensors_data->{$original_name}->{model} = $model; $sensors_data->{$original_name}->{model} = $model;
$sensors_data->{$original_name}->{serial} = $serial; $sensors_data->{$original_name}->{serial} = $serial;
debug(__LINE__, "Enhanced $original_name with drive info"); debug(__LINE__, "Enhanced $original_name with drive info");
} }
} }
return JSON->new->pretty->canonical->encode($sensors_data); return JSON->new->pretty->canonical->encode($sensors_data);
} }
# ============================================================================ # ============================================================================
# Enrich lm-sensors data with CPU model info # Enrich lm-sensors data with CPU model info
# ============================================================================ # ============================================================================
sub _get_cpu_name { sub _get_cpu_name {
my ($sensors_output, $cache_ref) = @_; my ($sensors_output, $cache_ref) = @_;
$cache_ref //= {}; $cache_ref //= {};
my $sensors_data; my $sensors_data;
eval { $sensors_data = decode_json($sensors_output); }; eval { $sensors_data = decode_json($sensors_output); };
if ($@) { if ($@) {
debug(__LINE__, "Failed to parse sensors JSON: $@"); debug(__LINE__, "Failed to parse sensors JSON: $@");
return $sensors_output; return $sensors_output;
} }
my @entries = my @entries =
grep { /^coretemp-isa-/ || /^k10temp-pci-/ } keys %{$sensors_data}; grep { /^coretemp-isa-/ || /^k10temp-pci-/ } keys %{$sensors_data};
debug(__LINE__, "Found " . scalar(@entries) . " CPU entries in sensors output"); debug(__LINE__, "Found " . scalar(@entries) . " CPU entries in sensors output");
foreach my $entry (@entries) { foreach my $entry (@entries) {
my ($cpu_model, $pkg) = ("unknown", "unknown"); my ($cpu_model, $pkg) = ("unknown", "unknown");
if (exists $cache_ref->{$entry}) { if (exists $cache_ref->{$entry}) {
my $cached = $cache_ref->{$entry}; my $cached = $cache_ref->{$entry};
$cpu_model = $cached->{model}; $cpu_model = $cached->{model};
$pkg = $cached->{package}; $pkg = $cached->{package};
debug(__LINE__, "Using cached CPU info for $entry"); debug(__LINE__, "Using cached CPU info for $entry");
} else { } else {
# ----- Intel coretemp ----- # ----- Intel coretemp -----
if ($entry =~ /^coretemp-isa-(\d+)/) { if ($entry =~ /^coretemp-isa-(\d+)/) {
for my $hwmon (glob "/sys/class/hwmon/hwmon*") { for my $hwmon (glob "/sys/class/hwmon/hwmon*") {
my $name = read_sysfs("$hwmon/name"); my $name = read_sysfs("$hwmon/name");
next unless $name eq 'coretemp'; next unless $name eq 'coretemp';
my $dev = readlink("$hwmon/device"); my $dev = readlink("$hwmon/device");
next unless $dev; next unless $dev;
if ($dev =~ /\.([0-9]+)$/) { if ($dev =~ /\.([0-9]+)$/) {
$pkg = $1; $pkg = $1;
$cpu_model = _cpu_model_by_package($pkg); $cpu_model = _cpu_model_by_package($pkg);
debug(__LINE__, debug(__LINE__,
"Found Intel CPU: $entry -> Package $pkg, Model: $cpu_model"); "Found Intel CPU: $entry -> Package $pkg, Model: $cpu_model");
last; last;
} }
} }
} }
# ----- AMD k10temp ----- # ----- AMD k10temp -----
elsif ($entry =~ /^k10temp-pci-(\w+)/) { elsif ($entry =~ /^k10temp-pci-(\w+)/) {
my $pci_addr = $1; my $pci_addr = $1;
my $pci_pattern = $pci_addr; my $pci_pattern = $pci_addr;
if ($pci_addr =~ /^([0-9a-f]{2})([0-9a-f]{2})$/i) { if ($pci_addr =~ /^([0-9a-f]{2})([0-9a-f]{2})$/i) {
my ($bus, $dev_func) = ($1, $2); my ($bus, $dev_func) = ($1, $2);
$pci_pattern = $pci_pattern =
sprintf("%04x:%02x:%02x", 0, hex($bus), hex($dev_func)); sprintf("%04x:%02x:%02x", 0, hex($bus), hex($dev_func));
debug(__LINE__, debug(__LINE__,
"Converted PCI address $pci_addr to pattern $pci_pattern"); "Converted PCI address $pci_addr to pattern $pci_pattern");
} }
for my $hwmon (glob "/sys/class/hwmon/hwmon*") { for my $hwmon (glob "/sys/class/hwmon/hwmon*") {
my $name = read_sysfs("$hwmon/name"); my $name = read_sysfs("$hwmon/name");
next unless $name eq 'k10temp'; next unless $name eq 'k10temp';
my $dev = readlink("$hwmon/device"); my $dev = readlink("$hwmon/device");
next unless $dev; next unless $dev;
if ($dev =~ /$pci_pattern/ || $dev =~ /$pci_addr/) { if ($dev =~ /$pci_pattern/ || $dev =~ /$pci_addr/) {
$pkg = 0; $pkg = 0;
if (opendir(my $dh, "/sys/devices/system/cpu")) { if (opendir(my $dh, "/sys/devices/system/cpu")) {
my @cpus = grep { /^cpu\d+$/ } readdir($dh); my @cpus = grep { /^cpu\d+$/ } readdir($dh);
closedir($dh); closedir($dh);
foreach my $cpu (@cpus) { foreach my $cpu (@cpus) {
my $cpu_pkg = read_sysfs( my $cpu_pkg = read_sysfs(
"/sys/devices/system/cpu/$cpu/topology/physical_package_id"); "/sys/devices/system/cpu/$cpu/topology/physical_package_id");
if ($cpu_pkg ne "unknown" && $cpu_pkg =~ /^\d+$/) { if ($cpu_pkg ne "unknown" && $cpu_pkg =~ /^\d+$/) {
$pkg = $cpu_pkg; $pkg = $cpu_pkg;
last; last;
} }
} }
} }
$cpu_model = _cpu_model_by_package($pkg); $cpu_model = _cpu_model_by_package($pkg);
debug(__LINE__, debug(__LINE__,
"Found AMD CPU: $entry -> Package $pkg, Model: $cpu_model"); "Found AMD CPU: $entry -> Package $pkg, Model: $cpu_model");
last; last;
} }
} }
} }
$cache_ref->{$entry} = { model => $cpu_model, package => $pkg }; $cache_ref->{$entry} = { model => $cpu_model, package => $pkg };
debug(__LINE__, "CPU: $entry -> Package $pkg (Model: $cpu_model)"); debug(__LINE__, "CPU: $entry -> Package $pkg (Model: $cpu_model)");
} }
if (exists $sensors_data->{$entry}) { if (exists $sensors_data->{$entry}) {
$sensors_data->{$entry}->{cpu_model} = $cpu_model; $sensors_data->{$entry}->{cpu_model} = $cpu_model;
$sensors_data->{$entry}->{cpu_package} = $pkg; $sensors_data->{$entry}->{cpu_package} = $pkg;
debug(__LINE__, "Enhanced $entry with CPU info"); debug(__LINE__, "Enhanced $entry with CPU info");
} }
} }
return JSON->new->pretty->canonical->encode($sensors_data); return JSON->new->pretty->canonical->encode($sensors_data);
} }
# ============================================================================ # ============================================================================
# CPU model lookup helper # CPU model lookup helper
# ============================================================================ # ============================================================================
sub _cpu_model_by_package { sub _cpu_model_by_package {
my ($pkg) = @_; my ($pkg) = @_;
if (open my $fh, '<', '/proc/cpuinfo') { if (open my $fh, '<', '/proc/cpuinfo') {
my $current_pkg = -1; my $current_pkg = -1;
my $model_name = "unknown"; my $model_name = "unknown";
while (my $line = <$fh>) { while (my $line = <$fh>) {
chomp $line; chomp $line;
if ($line =~ /^physical id\s+:\s+(\d+)/) { if ($line =~ /^physical id\s+:\s+(\d+)/) {
$current_pkg = $1; $current_pkg = $1;
} }
if ($line =~ /^model name\s+:\s+(.+)$/) { if ($line =~ /^model name\s+:\s+(.+)$/) {
$model_name = $1; $model_name = $1;
$model_name =~ s/^\s+|\s+$//g; $model_name =~ s/^\s+|\s+$//g;
if ($current_pkg == $pkg) { if ($current_pkg == $pkg) {
close($fh); close($fh);
return $model_name; return $model_name;
} }
} }
} }
close($fh); close($fh);
return $model_name if $model_name ne "unknown"; return $model_name if $model_name ne "unknown";
} }
return "unknown"; return "unknown";
} }
1; 1;

View File

@ -1,212 +1,212 @@
package PVE::PVEMod::Collector::Nvidia; package PVE::PVEMod::Collector::Nvidia;
use strict; use strict;
use warnings; 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 parse_csv_line); use PVE::PVEMod::Utils qw(debug check_executable setup_collector_signals safe_write_json parse_csv_line);
use PVE::PVEMod::Store qw(update_nvidia_gpu_rrd); use PVE::PVEMod::Store qw(update_nvidia_gpu_rrd);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
get_nvidia_gpu_devices get_nvidia_gpu_devices
collector_for_nvidia_devices collector_for_nvidia_devices
); );
# ============================================================================ # ============================================================================
# NVIDIA GPU — device discovery # NVIDIA GPU — device discovery
# ============================================================================ # ============================================================================
sub get_nvidia_gpu_devices { sub get_nvidia_gpu_devices {
my @devices = (); my @devices = ();
if ($config{debug}{nvidia_mode} && -f $config{debug}{nvidia_devices_file}) { if ($config{debug}{nvidia_mode} && -f $config{debug}{nvidia_devices_file}) {
debug(__LINE__, "Debug mode: reading NVIDIA GPU devices from $config{debug}{nvidia_devices_file}"); debug(__LINE__, "Debug mode: reading NVIDIA GPU devices from $config{debug}{nvidia_devices_file}");
if (open my $fh, '<', $config{debug}{nvidia_devices_file}) { if (open my $fh, '<', $config{debug}{nvidia_devices_file}) {
my $line_num = 0; my $line_num = 0;
while (<$fh>) { while (<$fh>) {
chomp; chomp;
$line_num++; $line_num++;
next if $line_num == 1 || /^\s*$/; next if $line_num == 1 || /^\s*$/;
my @values = parse_csv_line($_, 2); my @values = parse_csv_line($_, 2);
if (@values) { if (@values) {
push @devices, { index => $values[0], name => $values[1] }; push @devices, { index => $values[0], name => $values[1] };
debug(__LINE__, "Found NVIDIA GPU device (debug): $values[1] (index: $values[0])"); debug(__LINE__, "Found NVIDIA GPU device (debug): $values[1] (index: $values[0])");
} }
} }
close $fh; close $fh;
} else { } else {
debug(__LINE__, "Failed to open debug file $config{debug}{nvidia_devices_file}: $!"); debug(__LINE__, "Failed to open debug file $config{debug}{nvidia_devices_file}: $!");
} }
} else { } else {
if (open my $fh, '-|', 'nvidia-smi --query-gpu=index,name --format=csv') { if (open my $fh, '-|', 'nvidia-smi --query-gpu=index,name --format=csv') {
my $line_num = 0; my $line_num = 0;
while (<$fh>) { while (<$fh>) {
chomp; chomp;
$line_num++; $line_num++;
next if $line_num == 1 || /^\s*$/; next if $line_num == 1 || /^\s*$/;
my @values = parse_csv_line($_, 2); my @values = parse_csv_line($_, 2);
if (@values) { if (@values) {
push @devices, { index => $values[0], name => $values[1] }; push @devices, { index => $values[0], name => $values[1] };
debug(__LINE__, "Found NVIDIA GPU device: $values[1] (index: $values[0])"); debug(__LINE__, "Found NVIDIA GPU device: $values[1] (index: $values[0])");
} }
} }
close $fh; close $fh;
} }
} }
return @devices; return @devices;
} }
# ============================================================================ # ============================================================================
# NVIDIA GPU — data parsing # NVIDIA GPU — data parsing
# ============================================================================ # ============================================================================
sub _parse_nvidia_gpu_line { sub _parse_nvidia_gpu_line {
my ($line) = @_; my ($line) = @_;
# Expected CSV format: # Expected CSV format:
# index, name, temperature.gpu, utilization.gpu, utilization.memory, # index, name, temperature.gpu, utilization.gpu, utilization.memory,
# memory.used, memory.total, power.draw, power.limit, fan.speed # memory.used, memory.total, power.draw, power.limit, fan.speed
my @values = parse_csv_line($line, 10); my @values = parse_csv_line($line, 10);
return unless @values; return unless @values;
return { return {
index => $values[0] + 0, index => $values[0] + 0,
name => $values[1], name => $values[1],
temperature => { temperature => {
gpu => $values[2] + 0.0, gpu => $values[2] + 0.0,
unit => "°C", unit => "°C",
}, },
utilization => { utilization => {
gpu => $values[3] + 0.0, gpu => $values[3] + 0.0,
memory => $values[4] + 0.0, memory => $values[4] + 0.0,
unit => "%", unit => "%",
}, },
memory => { memory => {
used => $values[5] + 0.0, used => $values[5] + 0.0,
total => $values[6] + 0.0, total => $values[6] + 0.0,
unit => "MiB", unit => "MiB",
}, },
power => { power => {
draw => $values[7] + 0.0, draw => $values[7] + 0.0,
limit => $values[8] + 0.0, limit => $values[8] + 0.0,
unit => "W", unit => "W",
}, },
fan => { fan => {
speed => $values[9] + 0.0, speed => $values[9] + 0.0,
unit => "%", unit => "%",
}, },
}; };
} }
# ============================================================================ # ============================================================================
# NVIDIA GPU — stat collection and write # NVIDIA GPU — stat collection and write
# ============================================================================ # ============================================================================
sub _get_and_write_nvidia_stats { sub _get_and_write_nvidia_stats {
my ($devices) = @_; my ($devices) = @_;
my @all_stats; my @all_stats;
if ($config{debug}{nvidia_mode} && -f $config{debug}{nvidia_output_file}) { if ($config{debug}{nvidia_mode} && -f $config{debug}{nvidia_output_file}) {
debug(__LINE__, "Debug mode: reading NVIDIA GPU stats from $config{debug}{nvidia_output_file}"); debug(__LINE__, "Debug mode: reading NVIDIA GPU stats from $config{debug}{nvidia_output_file}");
if (open my $fh, '<', $config{debug}{nvidia_output_file}) { if (open my $fh, '<', $config{debug}{nvidia_output_file}) {
my $line_num = 0; my $line_num = 0;
while (<$fh>) { while (<$fh>) {
chomp; chomp;
$line_num++; $line_num++;
next if $line_num == 1 || /^\s*$/; next if $line_num == 1 || /^\s*$/;
my $stats = _parse_nvidia_gpu_line($_); my $stats = _parse_nvidia_gpu_line($_);
push @all_stats, $stats if $stats; push @all_stats, $stats if $stats;
} }
close $fh; close $fh;
} else { } else {
debug(__LINE__, "Failed to open debug file $config{debug}{nvidia_output_file}: $!"); debug(__LINE__, "Failed to open debug file $config{debug}{nvidia_output_file}: $!");
} }
} else { } else {
unless (check_executable('/usr/bin/nvidia-smi', 'NVIDIA')) { unless (check_executable('/usr/bin/nvidia-smi', 'NVIDIA')) {
debug(__LINE__, "nvidia-smi not available, cannot collect stats"); debug(__LINE__, "nvidia-smi not available, cannot collect stats");
return 0; return 0;
} }
my $query = 'index,name,temperature.gpu,utilization.gpu,utilization.memory,' my $query = 'index,name,temperature.gpu,utilization.gpu,utilization.memory,'
. 'memory.used,memory.total,power.draw,power.limit,fan.speed'; . 'memory.used,memory.total,power.draw,power.limit,fan.speed';
my $cmd = "nvidia-smi --query-gpu=$query --format=csv,nounits"; my $cmd = "nvidia-smi --query-gpu=$query --format=csv,nounits";
if (open my $fh, '-|', $cmd) { if (open my $fh, '-|', $cmd) {
my $line_num = 0; my $line_num = 0;
while (<$fh>) { while (<$fh>) {
chomp; chomp;
$line_num++; $line_num++;
next if $line_num == 1 || /^\s*$/; next if $line_num == 1 || /^\s*$/;
my $stats = _parse_nvidia_gpu_line($_); my $stats = _parse_nvidia_gpu_line($_);
push @all_stats, $stats if $stats; push @all_stats, $stats if $stats;
} }
close $fh; close $fh;
} }
} }
foreach my $stats (@all_stats) { foreach my $stats (@all_stats) {
my $device_index = $stats->{index}; my $device_index = $stats->{index};
unless ($device_index =~ /^(\d+)$/) { unless ($device_index =~ /^(\d+)$/) {
debug(__LINE__, "Invalid device index: $device_index, skipping"); debug(__LINE__, "Invalid device index: $device_index, skipping");
next; next;
} }
$device_index = $1; # untainted $device_index = $1; # untainted
my $node_name = "gpu$device_index"; my $node_name = "gpu$device_index";
my $device_state_file = "$pve_mod_working_dir/stats-nvidia$device_index.json"; my $device_state_file = "$pve_mod_working_dir/stats-nvidia$device_index.json";
my $device_name = $stats->{name}; my $device_name = $stats->{name};
foreach my $dev (@$devices) { foreach my $dev (@$devices) {
if ($dev->{index} == $device_index) { if ($dev->{index} == $device_index) {
$device_name = $dev->{name}; $device_name = $dev->{name};
last; last;
} }
} }
my $device_data = { my $device_data = {
$node_name => { $node_name => {
name => $device_name, name => $device_name,
index => $device_index, index => $device_index,
stats => $stats, stats => $stats,
} }
}; };
safe_write_json($device_state_file, $device_data); safe_write_json($device_state_file, $device_data);
update_nvidia_gpu_rrd($device_index, $stats); update_nvidia_gpu_rrd($device_index, $stats);
} }
unless (@all_stats) { unless (@all_stats) {
debug(__LINE__, "No valid NVIDIA GPU stats collected"); debug(__LINE__, "No valid NVIDIA GPU stats collected");
} }
return scalar(@all_stats); return scalar(@all_stats);
} }
# ============================================================================ # ============================================================================
# NVIDIA GPU — long-running collector (all devices in one process) # NVIDIA GPU — long-running collector (all devices in one process)
# ============================================================================ # ============================================================================
sub collector_for_nvidia_devices { sub collector_for_nvidia_devices {
my ($devices) = @_; my ($devices) = @_;
$process_type = 'collector'; $process_type = 'collector';
$0 = "collector-gpu-nvidia-all"; $0 = "collector-gpu-nvidia-all";
debug(__LINE__, "NVIDIA collector started for " . scalar(@$devices) . " GPU(s)"); debug(__LINE__, "NVIDIA collector started for " . scalar(@$devices) . " GPU(s)");
my $shutdown = 0; my $shutdown = 0;
setup_collector_signals('nvidia-all', \$shutdown); setup_collector_signals('nvidia-all', \$shutdown);
while (!$shutdown) { while (!$shutdown) {
_get_and_write_nvidia_stats($devices); _get_and_write_nvidia_stats($devices);
sleep $config{intervals}{data_pull} unless $shutdown; sleep $config{intervals}{data_pull} unless $shutdown;
} }
debug(__LINE__, "NVIDIA collector shutting down"); debug(__LINE__, "NVIDIA collector shutting down");
exit 0; exit 0;
} }
1; 1;

View File

@ -1,115 +1,115 @@
package PVE::PVEMod::Collector::Ups; package PVE::PVEMod::Collector::Ups;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use JSON; use JSON;
use PVE::PVEMod::Config qw($process_type $ups_state_file); use PVE::PVEMod::Config qw($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(
collector_for_ups collector_for_ups
); );
# ============================================================================ # ============================================================================
# UPS — long-running collector # UPS — long-running collector
# ============================================================================ # ============================================================================
sub collector_for_ups { sub collector_for_ups {
my ($device) = @_; my ($device) = @_;
$process_type = 'collector'; $process_type = 'collector';
$0 = "collector-ups-$device->{ups_name}"; $0 = "collector-ups-$device->{ups_name}";
debug(__LINE__, "UPS collector started"); debug(__LINE__, "UPS collector started");
my $shutdown = 0; my $shutdown = 0;
setup_collector_signals("ups-$device->{ups_name}", \$shutdown); setup_collector_signals("ups-$device->{ups_name}", \$shutdown);
while (!$shutdown) { while (!$shutdown) {
my $ups_data = _get_ups_status($device->{ups_name}); my $ups_data = _get_ups_status($device->{ups_name});
eval { eval {
open my $ofh, '>', $ups_state_file open my $ofh, '>', $ups_state_file
or die "Failed to open $ups_state_file: $!"; or die "Failed to open $ups_state_file: $!";
print $ofh $ups_data; print $ofh $ups_data;
close $ofh; close $ofh;
debug(__LINE__, "Wrote UPS data to $ups_state_file"); debug(__LINE__, "Wrote UPS data to $ups_state_file");
}; };
if ($@) { if ($@) {
debug(__LINE__, "Error writing UPS data: $@"); debug(__LINE__, "Error writing UPS data: $@");
} }
sleep 1 unless $shutdown; # $config{intervals}{data_pull} sleep 1 unless $shutdown; # $config{intervals}{data_pull}
} }
debug(__LINE__, "UPS collector shutting down"); debug(__LINE__, "UPS collector shutting down");
exit 0; exit 0;
} }
# ============================================================================ # ============================================================================
# UPS — status query # UPS — status query
# ============================================================================ # ============================================================================
sub _get_ups_status { sub _get_ups_status {
my ($ups_name) = @_; my ($ups_name) = @_;
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 = `/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");
return encode_json({ error => "No data from UPS $ups_name" }); return encode_json({ error => "No data from UPS $ups_name" });
} }
my $ups_data = _parse_upsc_output($output); my $ups_data = _parse_upsc_output($output);
unless (keys %$ups_data) { unless (keys %$ups_data) {
debug(__LINE__, "No data received from upsc for $ups_name"); debug(__LINE__, "No data received from upsc for $ups_name");
return encode_json({ error => "No data from UPS $ups_name" }); return encode_json({ error => "No data from UPS $ups_name" });
} }
return JSON->new->pretty->canonical->encode({ $ups_name => $ups_data }); return JSON->new->pretty->canonical->encode({ $ups_name => $ups_data });
} }
# ============================================================================ # ============================================================================
# UPS — output parser # UPS — output parser
# ============================================================================ # ============================================================================
sub _parse_upsc_output { sub _parse_upsc_output {
my ($output) = @_; my ($output) = @_;
my $ups_data = {}; my $ups_data = {};
debug(__LINE__, "Parsing upsc output"); debug(__LINE__, "Parsing upsc output");
eval { eval {
foreach my $line (split /\n/, $output) { foreach my $line (split /\n/, $output) {
next if $line =~ /^\s*$/; next if $line =~ /^\s*$/;
next if $line =~ /^Init SSL/; next if $line =~ /^Init SSL/;
if ($line =~ /^([^:]+):\s*(.*)$/) { if ($line =~ /^([^:]+):\s*(.*)$/) {
my ($key, $value) = ($1, $2); my ($key, $value) = ($1, $2);
$key =~ s/^\s+|\s+$//g; $key =~ s/^\s+|\s+$//g;
$value =~ s/^\s+|\s+$//g; $value =~ s/^\s+|\s+$//g;
# Coerce numeric values # Coerce numeric values
if ($value =~ /^-?\d+\.?\d*$/) { if ($value =~ /^-?\d+\.?\d*$/) {
$ups_data->{$key} = $value + 0; $ups_data->{$key} = $value + 0;
} else { } else {
$ups_data->{$key} = $value; $ups_data->{$key} = $value;
} }
} }
} }
}; };
if ($@) { if ($@) {
debug(__LINE__, "Error parsing upsc output: $@"); debug(__LINE__, "Error parsing upsc output: $@");
} }
debug(__LINE__, "Completed parsing upsc output"); debug(__LINE__, "Completed parsing upsc output");
return $ups_data; return $ups_data;
} }
1; 1;

View File

@ -1,96 +1,96 @@
package PVE::PVEMod::Collector::SystemInformation; package PVE::PVEMod::Collector::SystemInformation;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use PVE::PVEMod::Config qw(%config); use PVE::PVEMod::Config qw(%config);
use PVE::PVEMod::Utils qw(debug); use PVE::PVEMod::Utils qw(debug);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
get_system_information_data get_system_information_data
); );
# ============================================================================ # ============================================================================
# System Information — one-time dmidecode call # System Information — one-time dmidecode call
# ============================================================================ # ============================================================================
sub get_system_information_data { sub get_system_information_data {
unless ($config{system_info}{enabled}) { unless ($config{system_info}{enabled}) {
debug(__LINE__, "System information collection is disabled"); debug(__LINE__, "System information collection is disabled");
return {}; return {};
} }
my $raw_type = $config{system_info}{type}; my $raw_type = $config{system_info}{type};
# Taint-safe: only allow type 1 (System) or 2 (Baseboard/Motherboard) # Taint-safe: only allow type 1 (System) or 2 (Baseboard/Motherboard)
my $type; my $type;
if (defined $raw_type && $raw_type =~ /^([12])$/) { if (defined $raw_type && $raw_type =~ /^([12])$/) {
$type = $1; $type = $1;
} else { } else {
debug(__LINE__, "Invalid system_info type '${\($raw_type // 'undef')}', defaulting to 1"); debug(__LINE__, "Invalid system_info type '${\($raw_type // 'undef')}', defaulting to 1");
$type = 1; $type = 1;
} }
debug(__LINE__, "Collecting system information via dmidecode -t $type"); debug(__LINE__, "Collecting system information via dmidecode -t $type");
return _get_system_info($type); return _get_system_info($type);
} }
# ============================================================================ # ============================================================================
# Internal — run dmidecode and parse output # Internal — run dmidecode and parse output
# ============================================================================ # ============================================================================
sub _get_system_info { sub _get_system_info {
my ($type) = @_; my ($type) = @_;
my $output = `/usr/sbin/dmidecode -t $type 2>/dev/null`; my $output = `/usr/sbin/dmidecode -t $type 2>/dev/null`;
unless (defined $output && length($output) > 0) { unless (defined $output && length($output) > 0) {
debug(__LINE__, "No output from dmidecode -t $type"); debug(__LINE__, "No output from dmidecode -t $type");
return {}; return {};
} }
my %fields; my %fields;
my @field_order; my @field_order;
for my $line (split /\n/, $output) { for my $line (split /\n/, $output) {
if ($line =~ /^\s+(Manufacturer|Product Name|Serial Number):\s*(.+)$/) { if ($line =~ /^\s+(Manufacturer|Product Name|Serial Number):\s*(.+)$/) {
my ($key, $value) = ($1, $2); my ($key, $value) = ($1, $2);
$value =~ s/^\s+|\s+$//g; $value =~ s/^\s+|\s+$//g;
my $field_key = lc($key); my $field_key = lc($key);
$field_key =~ s/ /_/g; $field_key =~ s/ /_/g;
unless (exists $fields{$field_key}) { unless (exists $fields{$field_key}) {
push @field_order, $field_key; push @field_order, $field_key;
$fields{$field_key} = $value; $fields{$field_key} = $value;
} }
} }
} }
unless (%fields) { unless (%fields) {
debug(__LINE__, "No recognised fields found in dmidecode output"); debug(__LINE__, "No recognised fields found in dmidecode output");
return {}; return {};
} }
# Build display string: "Manufacturer: X | Product Name: Y | Serial Number: Z" # Build display string: "Manufacturer: X | Product Name: Y | Serial Number: Z"
my %pretty_key = ( my %pretty_key = (
manufacturer => 'Manufacturer', manufacturer => 'Manufacturer',
product_name => 'Product Name', product_name => 'Product Name',
serial_number => 'Serial Number', serial_number => 'Serial Number',
); );
my @parts; my @parts;
for my $key (@field_order) { for my $key (@field_order) {
my $label = $pretty_key{$key} // $key; my $label = $pretty_key{$key} // $key;
push @parts, "$label: $fields{$key}"; push @parts, "$label: $fields{$key}";
} }
$fields{display_string} = join(' | ', @parts); $fields{display_string} = join(' | ', @parts);
debug(__LINE__, "System information: $fields{display_string}"); debug(__LINE__, "System information: $fields{display_string}");
return \%fields; return \%fields;
} }
1; 1;

View File

@ -1,151 +1,151 @@
package PVE::PVEMod::Config; package PVE::PVEMod::Config;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
%config %config
$DEBUG_ENABLED $VERSION $process_type $DEBUG_ENABLED $VERSION $process_type
$pve_mod_working_dir $stats_dir $state_file $pve_mod_working_dir $stats_dir $state_file
$sensors_state_file $ups_state_file $sensors_state_file $ups_state_file
$pve_mod_worker_lock $startup_lock $pve_mod_worker_lock $startup_lock
$RRD_SOCKET $RRD_BASE $RRD_SOCKET $RRD_BASE
); );
# ============================================================================ # ============================================================================
# Debug / Version # Debug / Version
# ============================================================================ # ============================================================================
our $DEBUG_ENABLED = 1; our $DEBUG_ENABLED = 1;
our $VERSION = 'version-placeholder'; our $VERSION = 'version-placeholder';
# 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'
# ============================================================================ # ============================================================================
# Configuration # Configuration
# ============================================================================ # ============================================================================
our %config = ( our %config = (
gpu => { gpu => {
intel_enabled => 0, intel_enabled => 0,
amd_enabled => 0, amd_enabled => 0,
nvidia_enabled => 0, nvidia_enabled => 0,
gpu_history => 0, gpu_history => 0,
}, },
debug => { debug => {
log_enabled => 0, log_enabled => 0,
log_file => '/tmp/pve-mod-debug.log', log_file => '/tmp/pve-mod-debug.log',
lm_sensors_mode => 0, 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',
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',
amd_mode => 0, amd_mode => 0,
amd_devices_file => '/tmp/amd-gpu-devices.json', amd_devices_file => '/tmp/amd-gpu-devices.json',
ups_mode => 0, ups_mode => 0,
ups_output_file => '/tmp/ups-output.json', ups_output_file => '/tmp/ups-output.json',
}, },
intervals => { intervals => {
data_pull => 1, # seconds between data pulls data_pull => 1, # seconds between data pulls
collector_timeout => 10, # stop collectors after N seconds of inactivity collector_timeout => 10, # stop collectors after N seconds of inactivity
}, },
lm_sensors => { lm_sensors => {
enabled => 0, enabled => 0,
enable_cpu => 0, enable_cpu => 0,
cpu_temp_target => 'Core', cpu_temp_target => 'Core',
enable_ram_temp => 0, enable_ram_temp => 0,
enable_hdd_temp => 0, enable_hdd_temp => 0,
enable_nvme_temp => 0, enable_nvme_temp => 0,
enable_fan_speed => 0, enable_fan_speed => 0,
display_zero_speed_fans => 0, display_zero_speed_fans => 0,
temp_unit => 'C', temp_unit => 'C',
}, },
ups => { ups => {
enabled => 0, enabled => 0,
device_name => 'ups@localhost', device_name => 'ups@localhost',
}, },
system_info => { system_info => {
enabled => 0, enabled => 0,
type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2) type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2)
}, },
paths => { paths => {
working_dir => '/run/pveproxy/pve-mod', working_dir => '/run/pveproxy/pve-mod',
}, },
); );
# ============================================================================ # ============================================================================
# Derived paths # Derived paths
# ============================================================================ # ============================================================================
our $pve_mod_working_dir = $config{paths}{working_dir}; our $pve_mod_working_dir = $config{paths}{working_dir};
our $stats_dir = $pve_mod_working_dir; our $stats_dir = $pve_mod_working_dir;
our $state_file = "$pve_mod_working_dir/stats.json"; our $state_file = "$pve_mod_working_dir/stats.json";
our $sensors_state_file = "$pve_mod_working_dir/sensors.json"; our $sensors_state_file = "$pve_mod_working_dir/sensors.json";
our $ups_state_file = "$pve_mod_working_dir/ups.json"; our $ups_state_file = "$pve_mod_working_dir/ups.json";
our $pve_mod_worker_lock = "$pve_mod_working_dir/pve_mod_worker.lock"; our $pve_mod_worker_lock = "$pve_mod_working_dir/pve_mod_worker.lock";
our $startup_lock = "$pve_mod_working_dir/startup.lock"; our $startup_lock = "$pve_mod_working_dir/startup.lock";
# ============================================================================ # ============================================================================
# RRD paths # RRD paths
# ============================================================================ # ============================================================================
our $RRD_SOCKET = '/var/run/rrdcached.sock'; our $RRD_SOCKET = '/var/run/rrdcached.sock';
our $RRD_BASE = '/var/lib/rrdcached/db/pve-mod-gpu'; our $RRD_BASE = '/var/lib/rrdcached/db/pve-mod-gpu';
# ============================================================================ # ============================================================================
# Load configuration from /etc/pve-mod/pve-mod.conf (INI format). # Load configuration from /etc/pve-mod/pve-mod.conf (INI format).
# Merges file values into %config, overriding compiled-in defaults. # Merges file values into %config, overriding compiled-in defaults.
# Safe to call multiple times; silently skips missing file or unknown keys. # Safe to call multiple times; silently skips missing file or unknown keys.
# ============================================================================ # ============================================================================
sub _load_ini_file { sub _load_ini_file {
my $path = '/etc/pve-mod/pve-mod.conf'; my $path = '/etc/pve-mod/pve-mod.conf';
return unless -f $path; return unless -f $path;
open my $fh, '<', $path or return; open my $fh, '<', $path or return;
my $section = ''; my $section = '';
while (my $line = <$fh>) { while (my $line = <$fh>) {
chomp $line; chomp $line;
$line =~ s/#.*//; # strip inline comments $line =~ s/#.*//; # strip inline comments
$line =~ s/^\s+|\s+$//g; # trim whitespace $line =~ s/^\s+|\s+$//g; # trim whitespace
next unless length $line; next unless length $line;
if ($line =~ /^\[([^\]]+)\]$/) { if ($line =~ /^\[([^\]]+)\]$/) {
$section = $1; $section = $1;
next; next;
} }
if ($line =~ /^([^=]+)=(.*)$/) { if ($line =~ /^([^=]+)=(.*)$/) {
my ($key, $val) = ($1, $2); my ($key, $val) = ($1, $2);
$key =~ s/^\s+|\s+$//g; $key =~ s/^\s+|\s+$//g;
$val =~ s/^\s+|\s+$//g; $val =~ s/^\s+|\s+$//g;
if ($section eq 'gpu' && exists $config{gpu}{$key}) { if ($section eq 'gpu' && exists $config{gpu}{$key}) {
$config{gpu}{$key} = $val; $config{gpu}{$key} = $val;
} }
elsif ($section eq 'lm_sensors' && exists $config{lm_sensors}{$key}) { elsif ($section eq 'lm_sensors' && exists $config{lm_sensors}{$key}) {
$config{lm_sensors}{$key} = $val; $config{lm_sensors}{$key} = $val;
} }
elsif ($section eq 'ups' && exists $config{ups}{$key}) { elsif ($section eq 'ups' && exists $config{ups}{$key}) {
$config{ups}{$key} = $val; $config{ups}{$key} = $val;
} }
elsif ($section eq 'system_info' && exists $config{system_info}{$key}) { elsif ($section eq 'system_info' && exists $config{system_info}{$key}) {
$config{system_info}{$key} = $val; $config{system_info}{$key} = $val;
} }
elsif ($section eq 'debug' && exists $config{debug}{$key}) { elsif ($section eq 'debug' && exists $config{debug}{$key}) {
$config{debug}{$key} = $val; $config{debug}{$key} = $val;
} }
} }
} }
close $fh; close $fh;
} }
_load_ini_file(); _load_ini_file();
1; 1;

View File

@ -1,469 +1,469 @@
package PVE::PVEMod::ProcessManager; package PVE::PVEMod::ProcessManager;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use POSIX qw(WNOHANG); use POSIX qw(WNOHANG);
use File::Path qw(remove_tree); use File::Path qw(remove_tree);
use PVE::PVEMod::Config qw( use PVE::PVEMod::Config qw(
%config $process_type %config $process_type
$pve_mod_working_dir $state_file $pve_mod_working_dir $state_file
$pve_mod_worker_lock $startup_lock $pve_mod_worker_lock $startup_lock
); );
use PVE::PVEMod::Utils qw( use PVE::PVEMod::Utils qw(
debug is_process_alive read_lock_pid debug is_process_alive read_lock_pid
acquire_exclusive_lock ensure_pve_mod_directory_exists acquire_exclusive_lock ensure_pve_mod_directory_exists
check_executable startup_message check_executable startup_message
); );
use PVE::PVEMod::Collector::Intel qw(get_intel_gpu_devices collector_for_intel_device); use PVE::PVEMod::Collector::Intel qw(get_intel_gpu_devices collector_for_intel_device);
use PVE::PVEMod::Collector::Nvidia qw(get_nvidia_gpu_devices collector_for_nvidia_devices); use PVE::PVEMod::Collector::Nvidia qw(get_nvidia_gpu_devices collector_for_nvidia_devices);
use PVE::PVEMod::Collector::Amd qw(get_amd_gpu_devices collector_for_amd_device); use PVE::PVEMod::Collector::Amd qw(get_amd_gpu_devices collector_for_amd_device);
use PVE::PVEMod::Collector::LmSensors qw(collector_for_temperature_sensors); use PVE::PVEMod::Collector::LmSensors qw(collector_for_temperature_sensors);
use PVE::PVEMod::Collector::Ups qw(collector_for_ups); use PVE::PVEMod::Collector::Ups qw(collector_for_ups);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
pve_mod_starter pve_mod_starter
notify_pve_mod_worker notify_pve_mod_worker
); );
# Collector registry — only populated inside the worker process. # Collector registry — only populated inside the worker process.
# Each forked child has its own copy; the parent never accesses this after forking. # Each forked child has its own copy; the parent never accesses this after forking.
my %collectors = (); my %collectors = ();
# ============================================================================ # ============================================================================
# Public API (called from SensorInfo) # Public API (called from SensorInfo)
# ============================================================================ # ============================================================================
# Ensures the worker is running. Starts it if necessary (double-checked locking). # Ensures the worker is running. Starts it if necessary (double-checked locking).
sub pve_mod_starter { sub pve_mod_starter {
debug(__LINE__, "Checking if pve_mod_worker is already running"); debug(__LINE__, "Checking if pve_mod_worker is already running");
if (_worker_lock_file_exists()) { if (_worker_lock_file_exists()) {
debug(__LINE__, "pve_mod_worker process already running, system is already started"); debug(__LINE__, "pve_mod_worker process already running, system is already started");
return "pve_mod_worker process already running, system is already started"; return "pve_mod_worker process already running, system is already started";
} }
debug(__LINE__, "PVE mod worker is not running. PVE Mod will be started."); debug(__LINE__, "PVE mod worker is not running. PVE Mod will be started.");
startup_message(); startup_message();
ensure_pve_mod_directory_exists(); ensure_pve_mod_directory_exists();
debug(__LINE__, "Trying to acquire startup lock: $startup_lock"); debug(__LINE__, "Trying to acquire startup lock: $startup_lock");
my $startup_fh = acquire_exclusive_lock($startup_lock, 'startup lock'); my $startup_fh = acquire_exclusive_lock($startup_lock, 'startup lock');
return unless $startup_fh; return unless $startup_fh;
# Second check after acquiring lock # Second check after acquiring lock
if (_worker_lock_file_exists()) { if (_worker_lock_file_exists()) {
debug(__LINE__, "Worker started by another process while we waited for lock"); debug(__LINE__, "Worker started by another process while we waited for lock");
close($startup_fh); close($startup_fh);
unlink($startup_lock); unlink($startup_lock);
return "already running"; return "already running";
} }
print $startup_fh "$$\n"; print $startup_fh "$$\n";
$startup_fh->flush(); $startup_fh->flush();
debug(__LINE__, "Wrote PID $$ to startup lock"); debug(__LINE__, "Wrote PID $$ to startup lock");
_pve_mod_worker(); _pve_mod_worker();
unlink($startup_lock); unlink($startup_lock);
debug(__LINE__, "Released startup lock"); debug(__LINE__, "Released startup lock");
debug(__LINE__, "pve_mod_worker started successfully, returning"); debug(__LINE__, "pve_mod_worker started successfully, returning");
} }
# Sends SIGUSR1 to the worker to reset the inactivity timer. # Sends SIGUSR1 to the worker to reset the inactivity timer.
sub notify_pve_mod_worker { sub notify_pve_mod_worker {
debug(__LINE__, "notify_pve_mod_worker called"); debug(__LINE__, "notify_pve_mod_worker called");
unless (-f $pve_mod_worker_lock) { unless (-f $pve_mod_worker_lock) {
debug(__LINE__, "pve_mod_worker lock file does not exist"); debug(__LINE__, "pve_mod_worker lock file does not exist");
return; return;
} }
debug(__LINE__, "pve_mod_worker lock file exists, reading PID"); debug(__LINE__, "pve_mod_worker lock file exists, reading PID");
if (open my $fh, '<', $pve_mod_worker_lock) { if (open my $fh, '<', $pve_mod_worker_lock) {
my $pid = <$fh>; my $pid = <$fh>;
close $fh; close $fh;
chomp $pid if defined $pid; chomp $pid if defined $pid;
if (defined $pid && $pid =~ /^(\d+)$/) { if (defined $pid && $pid =~ /^(\d+)$/) {
my $clean_pid = $1; my $clean_pid = $1;
if (is_process_alive($clean_pid)) { if (is_process_alive($clean_pid)) {
debug(__LINE__, "Sending USR1 signal to pve_mod_worker PID $clean_pid"); debug(__LINE__, "Sending USR1 signal to pve_mod_worker PID $clean_pid");
my $result = kill('USR1', $clean_pid); my $result = kill('USR1', $clean_pid);
debug(__LINE__, "Signal result: $result"); debug(__LINE__, "Signal result: $result");
} else { } else {
debug(__LINE__, debug(__LINE__,
"pve_mod_worker process $clean_pid is not alive, removing stale lock"); "pve_mod_worker process $clean_pid is not alive, removing stale lock");
unlink($pve_mod_worker_lock); unlink($pve_mod_worker_lock);
} }
} else { } else {
debug(__LINE__, debug(__LINE__,
"pve_mod_worker lock is stale (PID: " . ($pid // 'undefined') . "), removing"); "pve_mod_worker lock is stale (PID: " . ($pid // 'undefined') . "), removing");
unlink($pve_mod_worker_lock); unlink($pve_mod_worker_lock);
} }
} else { } else {
debug(__LINE__, "Failed to open pve_mod_worker lock file: $!"); debug(__LINE__, "Failed to open pve_mod_worker lock file: $!");
} }
} }
# ============================================================================ # ============================================================================
# Worker process management # Worker process management
# ============================================================================ # ============================================================================
sub _worker_lock_file_exists { sub _worker_lock_file_exists {
return -f $pve_mod_worker_lock; return -f $pve_mod_worker_lock;
} }
# Forks the worker process and records its PID in the lock file. # Forks the worker process and records its PID in the lock file.
sub _pve_mod_worker { sub _pve_mod_worker {
debug(__LINE__, "_pve_mod_worker called"); debug(__LINE__, "_pve_mod_worker called");
my $pve_mod_worker_fh = my $pve_mod_worker_fh =
acquire_exclusive_lock($pve_mod_worker_lock, 'pve_mod_worker lock'); acquire_exclusive_lock($pve_mod_worker_lock, 'pve_mod_worker lock');
return unless $pve_mod_worker_fh; return unless $pve_mod_worker_fh;
print $pve_mod_worker_fh "$$\n"; print $pve_mod_worker_fh "$$\n";
close($pve_mod_worker_fh); close($pve_mod_worker_fh);
debug(__LINE__, "Forking new pve_mod_worker process"); debug(__LINE__, "Forking new pve_mod_worker process");
my $pve_mod_worker_pid = fork(); my $pve_mod_worker_pid = fork();
unless (defined $pve_mod_worker_pid) { unless (defined $pve_mod_worker_pid) {
debug(__LINE__, "Failed to fork pve_mod_worker process: $!"); debug(__LINE__, "Failed to fork pve_mod_worker process: $!");
return; return;
} }
if ($pve_mod_worker_pid == 0) { if ($pve_mod_worker_pid == 0) {
# Child # Child
$0 = "pve_mod_worker_controller"; $0 = "pve_mod_worker_controller";
debug(__LINE__, "Child process forked, calling _pve_mod_keep_alive"); debug(__LINE__, "Child process forked, calling _pve_mod_keep_alive");
_pve_mod_keep_alive(); _pve_mod_keep_alive();
exit(0); exit(0);
} }
# Parent — update lock file with real child PID # Parent — update lock file with real child PID
debug(__LINE__, "Forked pve_mod_worker process with PID $pve_mod_worker_pid"); debug(__LINE__, "Forked pve_mod_worker process with PID $pve_mod_worker_pid");
if (open my $fh, '>', $pve_mod_worker_lock) { if (open my $fh, '>', $pve_mod_worker_lock) {
print $fh "$pve_mod_worker_pid\n"; print $fh "$pve_mod_worker_pid\n";
close $fh; close $fh;
debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock"); debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock");
} else { } else {
debug(__LINE__, "Failed to write pve_mod_worker lock file: $!"); debug(__LINE__, "Failed to write pve_mod_worker lock file: $!");
kill('TERM', $pve_mod_worker_pid); kill('TERM', $pve_mod_worker_pid);
} }
debug(__LINE__, "pve_mod_worker process started successfully"); debug(__LINE__, "pve_mod_worker process started successfully");
} }
# ============================================================================ # ============================================================================
# Worker keep-alive loop # Worker keep-alive loop
# ============================================================================ # ============================================================================
sub _pve_mod_keep_alive { sub _pve_mod_keep_alive {
$process_type = 'worker'; $process_type = 'worker';
debug(__LINE__, "pve_mod_worker process started with PID $$"); debug(__LINE__, "pve_mod_worker process started with PID $$");
my $last_activity = time(); my $last_activity = time();
$SIG{USR1} = sub { $SIG{USR1} = sub {
$last_activity = time(); $last_activity = time();
debug(__LINE__, "Activity ping received"); debug(__LINE__, "Activity ping received");
}; };
$SIG{CHLD} = sub { $SIG{CHLD} = sub {
while ((my $pid = waitpid(-1, WNOHANG)) > 0) { while ((my $pid = waitpid(-1, WNOHANG)) > 0) {
my $exit_status = $? >> 8; my $exit_status = $? >> 8;
debug(__LINE__, "Child process $pid exited with status $exit_status"); debug(__LINE__, "Child process $pid exited with status $exit_status");
foreach my $name (keys %collectors) { foreach my $name (keys %collectors) {
if ($collectors{$name} == $pid) { if ($collectors{$name} == $pid) {
debug(__LINE__, debug(__LINE__,
"Collector '$name' (PID $pid) exited, removing from registry"); "Collector '$name' (PID $pid) exited, removing from registry");
delete $collectors{$name}; delete $collectors{$name};
last; last;
} }
} }
} }
}; };
$SIG{TERM} = sub { $SIG{TERM} = sub {
debug(__LINE__, "pve_mod_worker received SIGTERM, shutting down"); debug(__LINE__, "pve_mod_worker received SIGTERM, shutting down");
_stop_child_collectors(); _stop_child_collectors();
unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock; unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock;
exit(0); exit(0);
}; };
$SIG{INT} = sub { $SIG{INT} = sub {
debug(__LINE__, "pve_mod_worker received SIGINT, shutting down"); debug(__LINE__, "pve_mod_worker received SIGINT, shutting down");
_stop_child_collectors(); _stop_child_collectors();
unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock; unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock;
exit(0); exit(0);
}; };
debug(__LINE__, "Worker starting all collectors"); debug(__LINE__, "Worker starting all collectors");
_initialise_sensors_collector(); _initialise_sensors_collector();
_initialise_graphics_collectors(); _initialise_graphics_collectors();
_initialise_ups_collector(); _initialise_ups_collector();
debug(__LINE__, "All collectors started by worker"); debug(__LINE__, "All collectors started by worker");
debug(__LINE__, debug(__LINE__,
"Entering pve_mod_worker loop, timeout=$config{intervals}{collector_timeout}s"); "Entering pve_mod_worker loop, timeout=$config{intervals}{collector_timeout}s");
while (1) { while (1) {
debug(__LINE__, "pve_mod_worker loop start: checking activity"); debug(__LINE__, "pve_mod_worker loop start: checking activity");
my $idle_time = time() - $last_activity; my $idle_time = time() - $last_activity;
debug(__LINE__, debug(__LINE__,
"pve_mod_worker loop: idle_time=${idle_time}s, " "pve_mod_worker loop: idle_time=${idle_time}s, "
. "timeout=$config{intervals}{collector_timeout}s"); . "timeout=$config{intervals}{collector_timeout}s");
if ($idle_time > $config{intervals}{collector_timeout}) { if ($idle_time > $config{intervals}{collector_timeout}) {
debug(__LINE__, "Timeout reached, stopping collectors"); debug(__LINE__, "Timeout reached, stopping collectors");
_stop_child_collectors(); _stop_child_collectors();
debug(__LINE__, "Collectors stopped, exiting pve_mod_worker"); debug(__LINE__, "Collectors stopped, exiting pve_mod_worker");
unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock; unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock;
exit(0); exit(0);
} }
sleep(1); sleep(1);
} }
debug(__LINE__, "pve_mod_worker loop exited unexpectedly!"); debug(__LINE__, "pve_mod_worker loop exited unexpectedly!");
} }
# ============================================================================ # ============================================================================
# Collector startup helpers (called from worker loop) # Collector startup helpers (called from worker loop)
# ============================================================================ # ============================================================================
sub _initialise_sensors_collector { sub _initialise_sensors_collector {
return unless $config{lm_sensors}{enabled}; return unless $config{lm_sensors}{enabled};
return unless check_executable('/usr/bin/sensors', 'lm-sensors', return unless check_executable('/usr/bin/sensors', 'lm-sensors',
$config{debug}{lm_sensors_mode}, $config{debug}{lm_sensors_mode},
$config{debug}{lm_sensors_output_file}); $config{debug}{lm_sensors_output_file});
debug(__LINE__, "Starting lm-sensors collector"); debug(__LINE__, "Starting lm-sensors collector");
_start_collector('sensors', 'sensors', _start_collector('sensors', 'sensors',
\&collector_for_temperature_sensors, \&collector_for_temperature_sensors,
{ name => 'sensors' }); { name => 'sensors' });
} }
sub _initialise_ups_collector { sub _initialise_ups_collector {
unless ($config{ups}{enabled} && $config{ups}{device_name}) { unless ($config{ups}{enabled} && $config{ups}{device_name}) {
debug(__LINE__, "UPS collection disabled/invalid in config, skipping"); debug(__LINE__, "UPS collection disabled/invalid in config, skipping");
return; return;
} }
return unless check_executable('/usr/bin/upsc', 'UPS', return unless check_executable('/usr/bin/upsc', 'UPS',
$config{debug}{ups_mode}, $config{debug}{ups_mode},
$config{debug}{ups_output_file}); $config{debug}{ups_output_file});
debug(__LINE__, "Starting UPS collector: $config{ups}{device_name}"); debug(__LINE__, "Starting UPS collector: $config{ups}{device_name}");
_start_collector('ups', 'ups', \&collector_for_ups, _start_collector('ups', 'ups', \&collector_for_ups,
{ ups_name => $config{ups}{device_name} }); { ups_name => $config{ups}{device_name} });
} }
sub _initialise_graphics_collectors { sub _initialise_graphics_collectors {
unless ($config{gpu}{intel_enabled} unless ($config{gpu}{intel_enabled}
|| $config{gpu}{amd_enabled} || $config{gpu}{amd_enabled}
|| $config{gpu}{nvidia_enabled}) { || $config{gpu}{nvidia_enabled}) {
debug(__LINE__, "No GPU types enabled, skipping collector startup"); debug(__LINE__, "No GPU types enabled, skipping collector startup");
return; return;
} }
debug(__LINE__, "Starting graphics collectors"); debug(__LINE__, "Starting graphics collectors");
my (@all_devices, @all_types, @all_collector_subs); my (@all_devices, @all_types, @all_collector_subs);
my @nvidia_devices; my @nvidia_devices;
# Intel (each GPU has its own collector) # Intel (each GPU has its own collector)
if ($config{gpu}{intel_enabled} && check_executable('/usr/bin/intel_gpu_top', 'Intel', if ($config{gpu}{intel_enabled} && check_executable('/usr/bin/intel_gpu_top', 'Intel',
$config{debug}{intel_mode}, $config{debug}{intel_mode},
$config{debug}{intel_devices_file})) { $config{debug}{intel_devices_file})) {
my @intel_devices = get_intel_gpu_devices(); my @intel_devices = get_intel_gpu_devices();
for my $device (@intel_devices) { for my $device (@intel_devices) {
push @all_devices, $device; push @all_devices, $device;
push @all_types, 'intel'; push @all_types, 'intel';
push @all_collector_subs, \&collector_for_intel_device; push @all_collector_subs, \&collector_for_intel_device;
} }
} }
# AMD (each GPU has its own collector) # AMD (each GPU has its own collector)
if ($config{gpu}{amd_enabled} && check_executable('/usr/bin/rocm-smi', 'AMD', if ($config{gpu}{amd_enabled} && check_executable('/usr/bin/rocm-smi', 'AMD',
$config{debug}{amd_mode}, $config{debug}{amd_mode},
$config{debug}{amd_devices_file})) { $config{debug}{amd_devices_file})) {
my @amd_devices = get_amd_gpu_devices(); my @amd_devices = get_amd_gpu_devices();
for my $device (@amd_devices) { for my $device (@amd_devices) {
push @all_devices, $device; push @all_devices, $device;
push @all_types, 'amd'; push @all_types, 'amd';
push @all_collector_subs, \&collector_for_amd_device; push @all_collector_subs, \&collector_for_amd_device;
} }
} }
# NVIDIA (all GPUs collected together in one collector due to nvidia-smi design) # NVIDIA (all GPUs collected together in one collector due to nvidia-smi design)
if ($config{gpu}{nvidia_enabled} && check_executable('/usr/bin/nvidia-smi', 'NVIDIA', if ($config{gpu}{nvidia_enabled} && check_executable('/usr/bin/nvidia-smi', 'NVIDIA',
$config{debug}{nvidia_mode}, $config{debug}{nvidia_mode},
$config{debug}{nvidia_devices_file})) { $config{debug}{nvidia_devices_file})) {
@nvidia_devices = get_nvidia_gpu_devices(); @nvidia_devices = get_nvidia_gpu_devices();
} }
debug(__LINE__, debug(__LINE__,
"Detected: " "Detected: "
. scalar(grep { $_ eq 'intel' } @all_types) . " Intel, " . scalar(grep { $_ eq 'intel' } @all_types) . " Intel, "
. scalar(grep { $_ eq 'amd' } @all_types) . " AMD, " . scalar(grep { $_ eq 'amd' } @all_types) . " AMD, "
. scalar(@nvidia_devices) . " NVIDIA"); . scalar(@nvidia_devices) . " NVIDIA");
my $started_count = 0; my $started_count = 0;
# Start individual collectors for Intel and AMD devices # Start individual collectors for Intel and AMD devices
for (my $i = 0; $i < @all_devices; $i++) { for (my $i = 0; $i < @all_devices; $i++) {
my $device = $all_devices[$i]; my $device = $all_devices[$i];
my $type = $all_types[$i]; my $type = $all_types[$i];
my $collector_sub = $all_collector_subs[$i]; my $collector_sub = $all_collector_subs[$i];
my $device_name = $device->{card} // $device->{name} // "device$i"; my $device_name = $device->{card} // $device->{name} // "device$i";
my $pid = _start_collector($device_name, $type, $collector_sub, $device); my $pid = _start_collector($device_name, $type, $collector_sub, $device);
$started_count++ if $pid; $started_count++ if $pid;
} }
# NVIDIA — single collector for all GPUs # NVIDIA — single collector for all GPUs
if (@nvidia_devices) { if (@nvidia_devices) {
my $pid = _start_collector('nvidia-all', 'nvidia', my $pid = _start_collector('nvidia-all', 'nvidia',
\&collector_for_nvidia_devices, \&collector_for_nvidia_devices,
\@nvidia_devices); \@nvidia_devices);
$started_count++ if $pid; $started_count++ if $pid;
} }
debug(__LINE__, debug(__LINE__,
"Started/verified $started_count graphics collector(s)"); "Started/verified $started_count graphics collector(s)");
} }
# ============================================================================ # ============================================================================
# Generic collector start/stop # Generic collector start/stop
# ============================================================================ # ============================================================================
sub _start_collector { sub _start_collector {
my ($collector_name, $collector_type, $collector_sub, $device) = @_; my ($collector_name, $collector_type, $collector_sub, $device) = @_;
debug(__LINE__, "Starting $collector_type collector: $collector_name"); debug(__LINE__, "Starting $collector_type collector: $collector_name");
if (exists $collectors{$collector_name}) { if (exists $collectors{$collector_name}) {
my $pid = $collectors{$collector_name}; my $pid = $collectors{$collector_name};
if (kill(0, $pid)) { if (kill(0, $pid)) {
debug(__LINE__, debug(__LINE__,
"$collector_type collector '$collector_name' already running with PID $pid"); "$collector_type collector '$collector_name' already running with PID $pid");
return $pid; return $pid;
} else { } else {
debug(__LINE__, debug(__LINE__,
"Collector '$collector_name' PID $pid is stale, removing from registry"); "Collector '$collector_name' PID $pid is stale, removing from registry");
delete $collectors{$collector_name}; delete $collectors{$collector_name};
} }
} }
my $pid = _start_child_collector($collector_name, $collector_sub, $device); my $pid = _start_child_collector($collector_name, $collector_sub, $device);
unless ($pid) { unless ($pid) {
debug(__LINE__, "Failed to start $collector_type collector '$collector_name'"); debug(__LINE__, "Failed to start $collector_type collector '$collector_name'");
return undef; return undef;
} }
$collectors{$collector_name} = $pid; $collectors{$collector_name} = $pid;
debug(__LINE__, debug(__LINE__,
"Registered $collector_type collector '$collector_name' with PID $pid"); "Registered $collector_type collector '$collector_name' with PID $pid");
sleep 0.1; sleep 0.1;
if (kill(0, $pid)) { if (kill(0, $pid)) {
debug(__LINE__, debug(__LINE__,
"Verified $collector_type collector '$collector_name' (PID $pid) is alive"); "Verified $collector_type collector '$collector_name' (PID $pid) is alive");
return $pid; return $pid;
} else { } else {
debug(__LINE__, debug(__LINE__,
"WARNING - $collector_type collector '$collector_name' (PID $pid) died immediately!"); "WARNING - $collector_type collector '$collector_name' (PID $pid) died immediately!");
delete $collectors{$collector_name}; delete $collectors{$collector_name};
return undef; return undef;
} }
} }
sub _start_child_collector { sub _start_child_collector {
my ($collector_name, $collector_sub, $device) = @_; my ($collector_name, $collector_sub, $device) = @_;
debug(__LINE__, "Starting child collector: $collector_name"); debug(__LINE__, "Starting child collector: $collector_name");
my $pid = fork(); my $pid = fork();
unless (defined $pid) { unless (defined $pid) {
debug(__LINE__, "fork failed for $collector_name: $!"); debug(__LINE__, "fork failed for $collector_name: $!");
return undef; return undef;
} }
if ($pid == 0) { if ($pid == 0) {
$process_type = 'collector'; $process_type = 'collector';
debug(__LINE__, "In child process for $collector_name"); debug(__LINE__, "In child process for $collector_name");
$0 = "collector-$collector_name"; $0 = "collector-$collector_name";
$collector_sub->($device); $collector_sub->($device);
exit(0); exit(0);
} }
debug(__LINE__, "Forked child PID $pid for $collector_name"); debug(__LINE__, "Forked child PID $pid for $collector_name");
return $pid; return $pid;
} }
sub _stop_child_collectors { sub _stop_child_collectors {
debug(__LINE__, "Stopping all collectors"); debug(__LINE__, "Stopping all collectors");
my @pids = values %collectors; my @pids = values %collectors;
if (@pids) { if (@pids) {
debug(__LINE__, "Sending SIGTERM to " . scalar(@pids) . " collector process(es)"); debug(__LINE__, "Sending SIGTERM to " . scalar(@pids) . " collector process(es)");
foreach my $pid (@pids) { foreach my $pid (@pids) {
if (kill(0, $pid)) { if (kill(0, $pid)) {
kill('TERM', $pid); kill('TERM', $pid);
debug(__LINE__, "Sent SIGTERM to collector PID $pid"); debug(__LINE__, "Sent SIGTERM to collector PID $pid");
} }
} }
my $timeout = 2; my $timeout = 2;
my $start = time(); my $start = time();
while (time() - $start < $timeout) { while (time() - $start < $timeout) {
my $any_alive = 0; my $any_alive = 0;
foreach my $pid (@pids) { foreach my $pid (@pids) {
if (kill(0, $pid)) { $any_alive = 1; last; } if (kill(0, $pid)) { $any_alive = 1; last; }
} }
last unless $any_alive; last unless $any_alive;
select(undef, undef, undef, 0.1); select(undef, undef, undef, 0.1);
} }
foreach my $pid (@pids) { foreach my $pid (@pids) {
if (kill(0, $pid)) { if (kill(0, $pid)) {
debug(__LINE__, "Force killing collector process $pid"); debug(__LINE__, "Force killing collector process $pid");
kill('KILL', $pid); kill('KILL', $pid);
} }
} }
} }
%collectors = (); %collectors = ();
debug(__LINE__, "Cleared collector registry"); debug(__LINE__, "Cleared collector registry");
if (-f $state_file) { if (-f $state_file) {
unlink $state_file or debug(__LINE__, "Failed to remove $state_file: $!"); unlink $state_file or debug(__LINE__, "Failed to remove $state_file: $!");
} }
if (-d $pve_mod_working_dir) { if (-d $pve_mod_working_dir) {
remove_tree($pve_mod_working_dir, { error => \my $err }); remove_tree($pve_mod_working_dir, { error => \my $err });
debug(__LINE__, "Cleanup errors: @$err") if @$err; debug(__LINE__, "Cleanup errors: @$err") if @$err;
} }
debug(__LINE__, "Cleanup complete"); debug(__LINE__, "Cleanup complete");
} }
# ============================================================================ # ============================================================================
# END block — only the worker process performs cleanup # END block — only the worker process performs cleanup
# ============================================================================ # ============================================================================
END { END {
if ($process_type eq 'worker') { if ($process_type eq 'worker') {
debug(__LINE__, "PVE Mod Worker END block: cleaning up"); debug(__LINE__, "PVE Mod Worker END block: cleaning up");
_stop_child_collectors(); _stop_child_collectors();
} elsif ($process_type eq 'collector') { } elsif ($process_type eq 'collector') {
debug(__LINE__, "Collector ($0) END block: no cleanup needed"); debug(__LINE__, "Collector ($0) END block: no cleanup needed");
} else { } else {
debug(__LINE__, "Main process END block: no cleanup needed"); debug(__LINE__, "Main process END block: no cleanup needed");
} }
} }
1; 1;

View File

@ -1,218 +1,218 @@
package PVE::API2::PVEMod_SensorInfo; package PVE::API2::PVEMod_SensorInfo;
use strict; use strict;
use warnings; use warnings;
use PVE::PVEMod::Config qw(%config $VERSION $stats_dir $sensors_state_file $ups_state_file); use PVE::PVEMod::Config qw(%config $VERSION $stats_dir $sensors_state_file $ups_state_file);
use PVE::PVEMod::Utils qw(debug safe_read_json); use PVE::PVEMod::Utils qw(debug safe_read_json);
use PVE::PVEMod::ProcessManager qw(pve_mod_starter notify_pve_mod_worker); use PVE::PVEMod::ProcessManager qw(pve_mod_starter notify_pve_mod_worker);
use PVE::PVEMod::Collector::SystemInformation qw(get_system_information_data); use PVE::PVEMod::Collector::SystemInformation qw(get_system_information_data);
# Per-endpoint state caches (module-level, reset on worker restart) # Per-endpoint state caches (module-level, reset on worker restart)
my $graphics_cache = { data => {}, mtime => 0 }; my $graphics_cache = { data => {}, mtime => 0 };
my $sensors_cache = { data => '{}', mtime => 0 }; my $sensors_cache = { data => '{}', mtime => 0 };
my $ups_cache = { data => '{}', mtime => 0 }; my $ups_cache = { data => '{}', mtime => 0 };
my $system_info_cache = undef; my $system_info_cache = undef;
# ============================================================================ # ============================================================================
# Internal helpers # Internal helpers
# ============================================================================ # ============================================================================
sub _read_state_file_cached { sub _read_state_file_cached {
my ($files, $cache_ref, $reader, $empty_fallback) = @_; my ($files, $cache_ref, $reader, $empty_fallback) = @_;
# Normalize scalar path to single-element arrayref # Normalize scalar path to single-element arrayref
my @filepaths = ref($files) eq 'ARRAY' ? @$files : ($files); my @filepaths = ref($files) eq 'ARRAY' ? @$files : ($files);
# Find newest mtime across all files # Find newest mtime across all files
my $newest_mtime = 0; my $newest_mtime = 0;
my $any_exist = 0; my $any_exist = 0;
foreach my $fp (@filepaths) { foreach my $fp (@filepaths) {
my @st = stat($fp); my @st = stat($fp);
if (@st) { if (@st) {
$any_exist = 1; $any_exist = 1;
$newest_mtime = $st[9] if $st[9] > $newest_mtime; $newest_mtime = $st[9] if $st[9] > $newest_mtime;
} }
} }
unless ($any_exist) { unless ($any_exist) {
debug(__LINE__, "No state files exist: " . join(', ', @filepaths)); debug(__LINE__, "No state files exist: " . join(', ', @filepaths));
return $cache_ref->{data} // $empty_fallback; return $cache_ref->{data} // $empty_fallback;
} }
if ($newest_mtime == $cache_ref->{mtime} && defined $cache_ref->{data}) { if ($newest_mtime == $cache_ref->{mtime} && defined $cache_ref->{data}) {
debug(__LINE__, "State files unchanged, returning cached data"); debug(__LINE__, "State files unchanged, returning cached data");
return $cache_ref->{data}; return $cache_ref->{data};
} }
my $data; my $data;
if (ref($reader) eq 'CODE') { if (ref($reader) eq 'CODE') {
$data = $reader->(\@filepaths); $data = $reader->(\@filepaths);
} else { } else {
$data = safe_read_json($filepaths[0], $reader); $data = safe_read_json($filepaths[0], $reader);
} }
if (!defined $data) { if (!defined $data) {
debug(__LINE__, "Failed to read state file(s): " . join(', ', @filepaths)); debug(__LINE__, "Failed to read state file(s): " . join(', ', @filepaths));
return $cache_ref->{data} // $empty_fallback; return $cache_ref->{data} // $empty_fallback;
} }
$cache_ref->{data} = $data; $cache_ref->{data} = $data;
$cache_ref->{mtime} = $newest_mtime; $cache_ref->{mtime} = $newest_mtime;
return $cache_ref->{data}; return $cache_ref->{data};
} }
sub _merge_graphics_files { sub _merge_graphics_files {
my ($filepaths) = @_; my ($filepaths) = @_;
my $merged = { my $merged = {
Graphics => { Graphics => {
Intel => {}, Intel => {},
NVIDIA => {}, NVIDIA => {},
AMD => {}, AMD => {},
} }
}; };
foreach my $filepath (@$filepaths) { foreach my $filepath (@$filepaths) {
my ($file) = $filepath =~ m{([^/]+)$}; my ($file) = $filepath =~ m{([^/]+)$};
debug(__LINE__, "Reading device file: $filepath"); debug(__LINE__, "Reading device file: $filepath");
my $device_data = safe_read_json($filepath, 0); my $device_data = safe_read_json($filepath, 0);
if (!$device_data) { if (!$device_data) {
debug(__LINE__, "Failed to read/parse $filepath"); debug(__LINE__, "Failed to read/parse $filepath");
next; next;
} }
my $device_type = ($file =~ /^stats-card/) ? 'Intel' my $device_type = ($file =~ /^stats-card/) ? 'Intel'
: ($file =~ /^stats-nvidia/) ? 'NVIDIA' : ($file =~ /^stats-nvidia/) ? 'NVIDIA'
: 'AMD'; : 'AMD';
foreach my $node_name (keys %$device_data) { foreach my $node_name (keys %$device_data) {
$merged->{Graphics}->{$device_type}->{$node_name} = $device_data->{$node_name}; $merged->{Graphics}->{$device_type}->{$node_name} = $device_data->{$node_name};
debug(__LINE__, "Merged $device_type node '$node_name' from $file"); debug(__LINE__, "Merged $device_type node '$node_name' from $file");
} }
} }
return $merged; return $merged;
} }
sub _load_graphics_data { sub _load_graphics_data {
# Build filename patterns for enabled GPU types # Build filename patterns for enabled GPU types
my @patterns; my @patterns;
push @patterns, 'card\d+' if $config{gpu}{intel_enabled}; push @patterns, 'card\d+' if $config{gpu}{intel_enabled};
push @patterns, 'nvidia\d+' if $config{gpu}{nvidia_enabled}; push @patterns, 'nvidia\d+' if $config{gpu}{nvidia_enabled};
push @patterns, 'amd\d+' if $config{gpu}{amd_enabled}; push @patterns, 'amd\d+' if $config{gpu}{amd_enabled};
unless (@patterns) { unless (@patterns) {
debug(__LINE__, "No GPU types enabled in config"); debug(__LINE__, "No GPU types enabled in config");
return $graphics_cache->{data}; return $graphics_cache->{data};
} }
my $pattern = join('|', @patterns); my $pattern = join('|', @patterns);
# Find device stat files for enabled GPU types # Find device stat files for enabled GPU types
my $dh; my $dh;
unless (opendir($dh, $stats_dir)) { unless (opendir($dh, $stats_dir)) {
debug(__LINE__, "Failed to open stats directory: $stats_dir: $!"); debug(__LINE__, "Failed to open stats directory: $stats_dir: $!");
return $graphics_cache->{data}; return $graphics_cache->{data};
} }
my @stat_files = grep { /^stats-(?:$pattern)\.json$/ } readdir($dh); my @stat_files = grep { /^stats-(?:$pattern)\.json$/ } readdir($dh);
closedir($dh); closedir($dh);
unless (@stat_files) { unless (@stat_files) {
debug(__LINE__, "No device stat files found in $stats_dir"); debug(__LINE__, "No device stat files found in $stats_dir");
return $graphics_cache->{data}; return $graphics_cache->{data};
} }
debug(__LINE__, "Found " . scalar(@stat_files) . " device stat file(s): " . join(', ', @stat_files)); debug(__LINE__, "Found " . scalar(@stat_files) . " device stat file(s): " . join(', ', @stat_files));
my @filepaths = map { "$stats_dir/$_" } @stat_files; my @filepaths = map { "$stats_dir/$_" } @stat_files;
my $data = _read_state_file_cached( my $data = _read_state_file_cached(
\@filepaths, \@filepaths,
$graphics_cache, $graphics_cache,
\&_merge_graphics_files, \&_merge_graphics_files,
{ Graphics => { Intel => {}, NVIDIA => {}, AMD => {} } } { Graphics => { Intel => {}, NVIDIA => {}, AMD => {} } }
); );
my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}}); my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}});
my $nvidia_count = scalar(keys %{$data->{Graphics}{NVIDIA} // {}}); my $nvidia_count = scalar(keys %{$data->{Graphics}{NVIDIA} // {}});
my $amd_count = scalar(keys %{$data->{Graphics}{AMD} // {}}); my $amd_count = scalar(keys %{$data->{Graphics}{AMD} // {}});
debug(__LINE__, "Returning $intel_count Intel + $nvidia_count NVIDIA + $amd_count AMD device node(s)"); debug(__LINE__, "Returning $intel_count Intel + $nvidia_count NVIDIA + $amd_count AMD device node(s)");
return $data; return $data;
} }
# ============================================================================ # ============================================================================
# API calls # API calls
# ============================================================================ # ============================================================================
sub get_graphic_info { sub get_graphic_info {
debug(__LINE__, "get_graphic_info called"); debug(__LINE__, "get_graphic_info called");
# Start PVE Mod # Start PVE Mod
pve_mod_starter(); pve_mod_starter();
my $data = _load_graphics_data(); my $data = _load_graphics_data();
# Notify pve_mod_worker of activity # Notify pve_mod_worker of activity
notify_pve_mod_worker(); notify_pve_mod_worker();
return $data; return $data;
} }
sub get_sensors_info { sub get_sensors_info {
debug(__LINE__, "get_sensors_info called"); debug(__LINE__, "get_sensors_info called");
# Start PVE Mod # Start PVE Mod
pve_mod_starter(); pve_mod_starter();
my $data = _read_state_file_cached($sensors_state_file, $sensors_cache, 1, '{}'); my $data = _read_state_file_cached($sensors_state_file, $sensors_cache, 1, '{}');
# Notify pve_mod_worker of activity # Notify pve_mod_worker of activity
notify_pve_mod_worker(); notify_pve_mod_worker();
return $data; return $data;
} }
sub get_ups_info { sub get_ups_info {
debug(__LINE__, "get_ups_info called"); debug(__LINE__, "get_ups_info called");
# Start PVE Mod # Start PVE Mod
pve_mod_starter(); pve_mod_starter();
my $data = _read_state_file_cached($ups_state_file, $ups_cache, 1, '{}'); my $data = _read_state_file_cached($ups_state_file, $ups_cache, 1, '{}');
# Notify pve_mod_worker of activity # Notify pve_mod_worker of activity
notify_pve_mod_worker(); notify_pve_mod_worker();
return $data; return $data;
} }
sub get_pve_mod_version { sub get_pve_mod_version {
debug(__LINE__, "get_pve_mod_version called"); debug(__LINE__, "get_pve_mod_version called");
# Notify pve_mod_worker of activity # Notify pve_mod_worker of activity
notify_pve_mod_worker(); notify_pve_mod_worker();
debug(__LINE__, "Returning version: $VERSION"); debug(__LINE__, "Returning version: $VERSION");
return $VERSION; return $VERSION;
} }
sub get_system_information { sub get_system_information {
debug(__LINE__, "get_system_information called"); debug(__LINE__, "get_system_information called");
if (defined $system_info_cache) { if (defined $system_info_cache) {
debug(__LINE__, "Returning cached system information"); debug(__LINE__, "Returning cached system information");
return $system_info_cache; return $system_info_cache;
} }
$system_info_cache = get_system_information_data(); $system_info_cache = get_system_information_data();
return $system_info_cache; return $system_info_cache;
} }
1; 1;

File diff suppressed because it is too large Load Diff

View File

@ -1,166 +1,166 @@
package PVE::PVEMod::Store; package PVE::PVEMod::Store;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use File::Path qw(make_path); 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($RRD_SOCKET $RRD_BASE);
use PVE::PVEMod::Utils qw(debug); use PVE::PVEMod::Utils qw(debug);
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
get_nodename get_nodename
gpu_rrd_path gpu_rrd_path
update_intel_gpu_rrd update_intel_gpu_rrd
update_nvidia_gpu_rrd update_nvidia_gpu_rrd
); );
# ============================================================================ # ============================================================================
# Node name # Node name
# ============================================================================ # ============================================================================
sub get_nodename { sub get_nodename {
return PVE::INotify::nodename(); return PVE::INotify::nodename();
} }
# ============================================================================ # ============================================================================
# RRD path helper # RRD path helper
# ============================================================================ # ============================================================================
sub gpu_rrd_path { sub gpu_rrd_path {
my ($card) = @_; my ($card) = @_;
return "$RRD_BASE/" . get_nodename() . "/$card"; return "$RRD_BASE/" . get_nodename() . "/$card";
} }
# ============================================================================ # ============================================================================
# Intel GPU RRD # Intel GPU RRD
# ============================================================================ # ============================================================================
sub _ensure_intel_gpu_rrd { sub _ensure_intel_gpu_rrd {
my ($card) = @_; my ($card) = @_;
my $path = gpu_rrd_path($card); my $path = gpu_rrd_path($card);
return if -f $path; return if -f $path;
my $dir = "$RRD_BASE/" . get_nodename(); my $dir = "$RRD_BASE/" . get_nodename();
make_path($dir, { mode => 0755 }) unless -d $dir; make_path($dir, { mode => 0755 }) unless -d $dir;
RRDs::create( RRDs::create(
$path, $path,
'--step', '1', '--step', '1',
'DS:freq_req:GAUGE:120:0:U', 'DS:freq_req:GAUGE:120:0:U',
'DS:freq_act:GAUGE:120:0:U', 'DS:freq_act:GAUGE:120:0:U',
'DS:rc6:GAUGE:120:0:100', 'DS:rc6:GAUGE:120:0:100',
'DS:power_gpu:GAUGE:120:0:U', 'DS:power_gpu:GAUGE:120:0:U',
'DS:power_pkg:GAUGE:120:0:U', 'DS:power_pkg:GAUGE:120:0:U',
'DS:render_busy:GAUGE:120:0:100', 'DS:render_busy:GAUGE:120:0:100',
'DS:blitter_busy:GAUGE:120:0:100', 'DS:blitter_busy:GAUGE:120:0:100',
'DS:video_busy:GAUGE:120:0:100', 'DS:video_busy:GAUGE:120:0:100',
'DS:videnh_busy:GAUGE:120:0:100', 'DS:videnh_busy:GAUGE:120:0:100',
'RRA:AVERAGE:0.5:1:1440', 'RRA:AVERAGE:0.5:1:1440',
'RRA:AVERAGE:0.5:60:1440', 'RRA:AVERAGE:0.5:60:1440',
'RRA:AVERAGE:0.5:1800:1344', 'RRA:AVERAGE:0.5:1800:1344',
'RRA:AVERAGE:0.5:21600:1464', 'RRA:AVERAGE:0.5:21600:1464',
'RRA:AVERAGE:0.5:604800:520', 'RRA:AVERAGE:0.5:604800:520',
'RRA:MAX:0.5:1:1440', 'RRA:MAX:0.5:1:1440',
'RRA:MAX:0.5:60:1440', 'RRA:MAX:0.5:60:1440',
'RRA:MAX:0.5:1800:1344', 'RRA:MAX:0.5:1800:1344',
'RRA:MAX:0.5:21600:1464', 'RRA:MAX:0.5:21600:1464',
'RRA:MAX:0.5:604800:520', 'RRA:MAX:0.5:604800:520',
); );
my $err = RRDs::error(); my $err = RRDs::error();
debug(__LINE__, "Created Intel GPU RRD $path: " . ($err // 'OK')); debug(__LINE__, "Created Intel GPU RRD $path: " . ($err // 'OK'));
} }
sub update_intel_gpu_rrd { sub update_intel_gpu_rrd {
my ($card, $stats) = @_; my ($card, $stats) = @_;
_ensure_intel_gpu_rrd($card); _ensure_intel_gpu_rrd($card);
my $path = gpu_rrd_path($card); my $path = gpu_rrd_path($card);
my $freq_req = $stats->{frequency}{requested} // 'U'; my $freq_req = $stats->{frequency}{requested} // 'U';
my $freq_act = $stats->{frequency}{actual} // 'U'; my $freq_act = $stats->{frequency}{actual} // 'U';
my $rc6 = $stats->{rc6}{value} // 'U'; my $rc6 = $stats->{rc6}{value} // 'U';
my $power_gpu = $stats->{power}{GPU} // 'U'; my $power_gpu = $stats->{power}{GPU} // 'U';
my $power_pkg = $stats->{power}{Package} // 'U'; my $power_pkg = $stats->{power}{Package} // 'U';
my $render_busy = $stats->{engines}{'Render/3D'}{busy} // 'U'; my $render_busy = $stats->{engines}{'Render/3D'}{busy} // 'U';
my $blitter = $stats->{engines}{Blitter}{busy} // 'U'; my $blitter = $stats->{engines}{Blitter}{busy} // 'U';
my $video = $stats->{engines}{Video}{busy} // 'U'; my $video = $stats->{engines}{Video}{busy} // 'U';
my $videnh = $stats->{engines}{VideoEnhance}{busy} // 'U'; my $videnh = $stats->{engines}{VideoEnhance}{busy} // 'U';
my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : (); my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : ();
RRDs::update( RRDs::update(
$path, $path,
@daemon_args, @daemon_args,
"N:$freq_req:$freq_act:$rc6:$power_gpu:$power_pkg:$render_busy:$blitter:$video:$videnh", "N:$freq_req:$freq_act:$rc6:$power_gpu:$power_pkg:$render_busy:$blitter:$video:$videnh",
); );
my $err = RRDs::error(); my $err = RRDs::error();
debug(__LINE__, "RRD update intel $card: $err") if $err; debug(__LINE__, "RRD update intel $card: $err") if $err;
} }
# ============================================================================ # ============================================================================
# NVIDIA GPU RRD # NVIDIA GPU RRD
# ============================================================================ # ============================================================================
sub _ensure_nvidia_gpu_rrd { sub _ensure_nvidia_gpu_rrd {
my ($index) = @_; my ($index) = @_;
my $card = "nvidia$index"; my $card = "nvidia$index";
my $path = gpu_rrd_path($card); my $path = gpu_rrd_path($card);
return if -f $path; return if -f $path;
my $dir = "$RRD_BASE/" . get_nodename(); my $dir = "$RRD_BASE/" . get_nodename();
make_path($dir, { mode => 0755 }) unless -d $dir; make_path($dir, { mode => 0755 }) unless -d $dir;
RRDs::create( RRDs::create(
$path, $path,
'--step', '1', '--step', '1',
'DS:gpu_util:GAUGE:120:0:100', 'DS:gpu_util:GAUGE:120:0:100',
'DS:mem_util:GAUGE:120:0:100', 'DS:mem_util:GAUGE:120:0:100',
'DS:mem_used:GAUGE:120:0:U', 'DS:mem_used:GAUGE:120:0:U',
'DS:mem_total:GAUGE:120:0:U', 'DS:mem_total:GAUGE:120:0:U',
'DS:power_draw:GAUGE:120:0:U', 'DS:power_draw:GAUGE:120:0:U',
'DS:power_limit:GAUGE:120:0:U', 'DS:power_limit:GAUGE:120:0:U',
'DS:temp_gpu:GAUGE:120:0:U', 'DS:temp_gpu:GAUGE:120:0:U',
'DS:fan_speed:GAUGE:120:0:100', 'DS:fan_speed:GAUGE:120:0:100',
'RRA:AVERAGE:0.5:1:1440', 'RRA:AVERAGE:0.5:1:1440',
'RRA:AVERAGE:0.5:60:1440', 'RRA:AVERAGE:0.5:60:1440',
'RRA:AVERAGE:0.5:1800:1344', 'RRA:AVERAGE:0.5:1800:1344',
'RRA:AVERAGE:0.5:21600:1464', 'RRA:AVERAGE:0.5:21600:1464',
'RRA:AVERAGE:0.5:604800:520', 'RRA:AVERAGE:0.5:604800:520',
'RRA:MAX:0.5:1:1440', 'RRA:MAX:0.5:1:1440',
'RRA:MAX:0.5:60:1440', 'RRA:MAX:0.5:60:1440',
'RRA:MAX:0.5:1800:1344', 'RRA:MAX:0.5:1800:1344',
'RRA:MAX:0.5:21600:1464', 'RRA:MAX:0.5:21600:1464',
'RRA:MAX:0.5:604800:520', 'RRA:MAX:0.5:604800:520',
); );
my $err = RRDs::error(); my $err = RRDs::error();
debug(__LINE__, "Created NVIDIA GPU RRD $path: " . ($err // 'OK')); debug(__LINE__, "Created NVIDIA GPU RRD $path: " . ($err // 'OK'));
} }
sub update_nvidia_gpu_rrd { sub update_nvidia_gpu_rrd {
my ($index, $stats) = @_; my ($index, $stats) = @_;
_ensure_nvidia_gpu_rrd($index); _ensure_nvidia_gpu_rrd($index);
my $card = "nvidia$index"; my $card = "nvidia$index";
my $path = gpu_rrd_path($card); my $path = gpu_rrd_path($card);
my $gpu_util = $stats->{utilization}{gpu} // 'U'; my $gpu_util = $stats->{utilization}{gpu} // 'U';
my $mem_util = $stats->{utilization}{memory} // 'U'; my $mem_util = $stats->{utilization}{memory} // 'U';
my $mem_used = $stats->{memory}{used} // 'U'; my $mem_used = $stats->{memory}{used} // 'U';
my $mem_total = $stats->{memory}{total} // 'U'; my $mem_total = $stats->{memory}{total} // 'U';
my $power_draw = $stats->{power}{draw} // 'U'; my $power_draw = $stats->{power}{draw} // 'U';
my $power_limit = $stats->{power}{limit} // 'U'; my $power_limit = $stats->{power}{limit} // 'U';
my $temp_gpu = $stats->{temperature}{gpu} // 'U'; my $temp_gpu = $stats->{temperature}{gpu} // 'U';
my $fan_speed = $stats->{fan}{speed} // 'U'; my $fan_speed = $stats->{fan}{speed} // 'U';
my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : (); my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : ();
RRDs::update( RRDs::update(
$path, $path,
@daemon_args, @daemon_args,
"N:$gpu_util:$mem_util:$mem_used:$mem_total:$power_draw:$power_limit:$temp_gpu:$fan_speed", "N:$gpu_util:$mem_util:$mem_used:$mem_total:$power_draw:$power_limit:$temp_gpu:$fan_speed",
); );
my $err = RRDs::error(); my $err = RRDs::error();
debug(__LINE__, "RRD update nvidia$index: $err") if $err; debug(__LINE__, "RRD update nvidia$index: $err") if $err;
} }
1; 1;

View File

@ -1,274 +1,274 @@
package PVE::PVEMod::Utils; package PVE::PVEMod::Utils;
use strict; use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use JSON; use JSON;
use Fcntl qw(O_CREAT O_EXCL O_WRONLY); use Fcntl qw(O_CREAT O_EXCL O_WRONLY);
use PVE::PVEMod::Config qw($DEBUG_ENABLED $VERSION $pve_mod_working_dir %config); use PVE::PVEMod::Config qw($DEBUG_ENABLED $VERSION $pve_mod_working_dir %config);
my $debug_log_fh; my $debug_log_fh;
our @EXPORT_OK = qw( our @EXPORT_OK = qw(
debug debug
read_sysfs read_sysfs
is_process_alive is_process_alive
read_lock_pid read_lock_pid
acquire_exclusive_lock acquire_exclusive_lock
ensure_pve_mod_directory_exists ensure_pve_mod_directory_exists
check_executable check_executable
startup_message startup_message
setup_collector_signals setup_collector_signals
safe_write_json safe_write_json
safe_read_json safe_read_json
parse_csv_line parse_csv_line
); );
# ============================================================================ # ============================================================================
# Debug # Debug
# ============================================================================ # ============================================================================
# debug function showing line number and call chain # debug function showing line number and call chain
# Usage: debug(__LINE__, "message") # Usage: debug(__LINE__, "message")
sub debug { sub debug {
return unless $DEBUG_ENABLED; return unless $DEBUG_ENABLED;
my ($line, $message) = @_; my ($line, $message) = @_;
my @caller1 = caller(1); # who called debug() my @caller1 = caller(1); # who called debug()
my @caller2 = caller(2); # parent of caller my @caller2 = caller(2); # parent of caller
my $sub1 = $caller1[3] || 'main'; my $sub1 = $caller1[3] || 'main';
my $sub2 = $caller2[3]; my $sub2 = $caller2[3];
$sub1 =~ s/.*:://; $sub1 =~ s/.*:://;
my $output; my $output;
if (defined $sub2) { if (defined $sub2) {
$sub2 =~ s/.*:://; $sub2 =~ s/.*:://;
$output = "[$sub2 -> $sub1:$line] $message\n"; $output = "[$sub2 -> $sub1:$line] $message\n";
} else { } else {
$output = "[$sub1:$line] $message\n"; $output = "[$sub1:$line] $message\n";
} }
warn $output; warn $output;
if ($config{debug}{log_enabled} && !defined $debug_log_fh) { if ($config{debug}{log_enabled} && !defined $debug_log_fh) {
if (open(my $fh, '>>', $config{debug}{log_file})) { if (open(my $fh, '>>', $config{debug}{log_file})) {
$fh->autoflush(1); $fh->autoflush(1);
$debug_log_fh = $fh; $debug_log_fh = $fh;
} else { } else {
warn "[debug] Failed to open log file $config{debug}{log_file}: $!\n"; warn "[debug] Failed to open log file $config{debug}{log_file}: $!\n";
} }
} }
print $debug_log_fh $output if defined $debug_log_fh; print $debug_log_fh $output if defined $debug_log_fh;
} }
# ============================================================================ # ============================================================================
# File / Process helpers # File / Process helpers
# ============================================================================ # ============================================================================
sub read_sysfs { sub read_sysfs {
my ($path) = @_; my ($path) = @_;
return "unknown" unless defined $path && -f $path; return "unknown" unless defined $path && -f $path;
if (open my $fh, '<', $path) { if (open my $fh, '<', $path) {
my $value = <$fh>; my $value = <$fh>;
close $fh; close $fh;
if (defined $value) { if (defined $value) {
chomp $value; chomp $value;
$value =~ s/^\s+|\s+$//g; $value =~ s/^\s+|\s+$//g;
return $value ne '' ? $value : "unknown"; return $value ne '' ? $value : "unknown";
} }
} }
return "unknown"; return "unknown";
} }
sub is_process_alive { sub is_process_alive {
my ($pid) = @_; my ($pid) = @_;
return -d "/proc/$pid"; return -d "/proc/$pid";
} }
sub read_lock_pid { sub read_lock_pid {
my ($lock_path) = @_; my ($lock_path) = @_;
return undef unless open(my $fh, '<', $lock_path); return undef unless open(my $fh, '<', $lock_path);
my $pid = <$fh>; my $pid = <$fh>;
close($fh); close($fh);
chomp $pid if defined $pid; chomp $pid if defined $pid;
return $pid; return $pid;
} }
sub acquire_exclusive_lock { sub acquire_exclusive_lock {
my ($lock_path, $purpose) = @_; my ($lock_path, $purpose) = @_;
$purpose //= 'lock'; $purpose //= 'lock';
my $fh; my $fh;
if (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) { if (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) {
debug(__LINE__, "Acquired $purpose on first try"); debug(__LINE__, "Acquired $purpose on first try");
return $fh; return $fh;
} }
debug(__LINE__, ucfirst($purpose) . " exists, checking if stale"); debug(__LINE__, ucfirst($purpose) . " exists, checking if stale");
my $lock_pid = read_lock_pid($lock_path); my $lock_pid = read_lock_pid($lock_path);
if (!defined $lock_pid) { if (!defined $lock_pid) {
debug(__LINE__, "Could not read $purpose file: $!"); debug(__LINE__, "Could not read $purpose file: $!");
return undef; return undef;
} }
if ($lock_pid eq '' || $lock_pid !~ /^\d+$/) { if ($lock_pid eq '' || $lock_pid !~ /^\d+$/) {
debug(__LINE__, "Invalid PID in $purpose: '" . ($lock_pid // 'undefined') . "', removing"); debug(__LINE__, "Invalid PID in $purpose: '" . ($lock_pid // 'undefined') . "', removing");
unlink($lock_path); unlink($lock_path);
} elsif (is_process_alive($lock_pid)) { } elsif (is_process_alive($lock_pid)) {
debug(__LINE__, ucfirst($purpose) . " holder PID $lock_pid is still alive"); debug(__LINE__, ucfirst($purpose) . " holder PID $lock_pid is still alive");
return undef; return undef;
} else { } else {
debug(__LINE__, ucfirst($purpose) . " holder PID $lock_pid is dead, removing stale lock"); debug(__LINE__, ucfirst($purpose) . " holder PID $lock_pid is dead, removing stale lock");
unlink($lock_path); unlink($lock_path);
} }
unless (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) { unless (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) {
debug(__LINE__, "Failed to acquire $purpose on retry: $!"); debug(__LINE__, "Failed to acquire $purpose on retry: $!");
return undef; return undef;
} }
debug(__LINE__, "Acquired $purpose after removing stale lock"); debug(__LINE__, "Acquired $purpose after removing stale lock");
return $fh; return $fh;
} }
sub ensure_pve_mod_directory_exists { sub ensure_pve_mod_directory_exists {
unless (-d $pve_mod_working_dir) { unless (-d $pve_mod_working_dir) {
debug(__LINE__, "Creating directory $pve_mod_working_dir"); debug(__LINE__, "Creating directory $pve_mod_working_dir");
unless (mkdir($pve_mod_working_dir, 0755)) { unless (mkdir($pve_mod_working_dir, 0755)) {
debug(__LINE__, "Failed to create $pve_mod_working_dir: $!. PVE Mod cannot start."); debug(__LINE__, "Failed to create $pve_mod_working_dir: $!. PVE Mod cannot start.");
die "Failed to create $pve_mod_working_dir: $!"; die "Failed to create $pve_mod_working_dir: $!";
} }
debug(__LINE__, "Directory $pve_mod_working_dir created"); debug(__LINE__, "Directory $pve_mod_working_dir created");
} else { } else {
debug(__LINE__, "Directory $pve_mod_working_dir already exists"); debug(__LINE__, "Directory $pve_mod_working_dir already exists");
} }
} }
# Returns 1 if executable exists, or debug mode is active with a debug file present. # Returns 1 if executable exists, or debug mode is active with a debug file present.
# Returns 0 otherwise. # Returns 0 otherwise.
sub check_executable { sub check_executable {
my ($exec_path, $type, $debug_mode_enabled, $debug_file) = @_; my ($exec_path, $type, $debug_mode_enabled, $debug_file) = @_;
if (defined $debug_mode_enabled && $debug_mode_enabled) { if (defined $debug_mode_enabled && $debug_mode_enabled) {
if (defined $debug_file && -f $debug_file) { if (defined $debug_file && -f $debug_file) {
debug(__LINE__, "Debug mode enabled for $type, using debug file: $debug_file"); debug(__LINE__, "Debug mode enabled for $type, using debug file: $debug_file");
return 1; return 1;
} elsif (defined $debug_file) { } elsif (defined $debug_file) {
debug(__LINE__, "Debug mode enabled for $type but debug file missing: $debug_file"); debug(__LINE__, "Debug mode enabled for $type but debug file missing: $debug_file");
return 0; return 0;
} else { } else {
debug(__LINE__, "Debug mode enabled for $type, skipping executable check for $exec_path"); debug(__LINE__, "Debug mode enabled for $type, skipping executable check for $exec_path");
return 1; return 1;
} }
} }
unless (-x $exec_path) { unless (-x $exec_path) {
debug(__LINE__, "$type executable not found or not executable: $exec_path"); debug(__LINE__, "$type executable not found or not executable: $exec_path");
return 0; return 0;
} }
debug(__LINE__, "$type executable found: $exec_path"); debug(__LINE__, "$type executable found: $exec_path");
return 1; return 1;
} }
sub startup_message { sub startup_message {
debug(__LINE__, "PVE Mod is being started. Version $VERSION"); debug(__LINE__, "PVE Mod is being started. Version $VERSION");
} }
# Setup common TERM/INT signal handlers for collector processes. # Setup common TERM/INT signal handlers for collector processes.
# $shutdown_ref is a scalar ref that will be set to 1 on signal. # $shutdown_ref is a scalar ref that will be set to 1 on signal.
sub setup_collector_signals { sub setup_collector_signals {
my ($name, $shutdown_ref, $extra_cleanup) = @_; my ($name, $shutdown_ref, $extra_cleanup) = @_;
$SIG{TERM} = sub { $SIG{TERM} = sub {
debug(__LINE__, "Collector $name received SIGTERM"); debug(__LINE__, "Collector $name received SIGTERM");
$$shutdown_ref = 1; $$shutdown_ref = 1;
$extra_cleanup->() if $extra_cleanup; $extra_cleanup->() if $extra_cleanup;
}; };
$SIG{INT} = sub { $SIG{INT} = sub {
debug(__LINE__, "Collector $name received SIGINT"); debug(__LINE__, "Collector $name received SIGINT");
$$shutdown_ref = 1; $$shutdown_ref = 1;
$extra_cleanup->() if $extra_cleanup; $extra_cleanup->() if $extra_cleanup;
}; };
} }
# ============================================================================ # ============================================================================
# JSON helpers # JSON helpers
# ============================================================================ # ============================================================================
sub safe_write_json { sub safe_write_json {
my ($filepath, $data, $pretty) = @_; my ($filepath, $data, $pretty) = @_;
$pretty //= 1; $pretty //= 1;
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);
print $fh $json; print $fh $json;
close $fh; close $fh;
debug(__LINE__, "Wrote JSON to $filepath"); debug(__LINE__, "Wrote JSON to $filepath");
}; };
if ($@) { if ($@) {
debug(__LINE__, "Error writing to $filepath: $@"); debug(__LINE__, "Error writing to $filepath: $@");
return 0; return 0;
} }
return 1; return 1;
} }
sub safe_read_json { sub safe_read_json {
my ($filepath, $as_string) = @_; my ($filepath, $as_string) = @_;
return unless -f $filepath; return unless -f $filepath;
my $result; my $result;
eval { eval {
open my $fh, '<', $filepath or die "Failed to open $filepath: $!"; open my $fh, '<', $filepath or die "Failed to open $filepath: $!";
local $/; local $/;
my $json = <$fh>; my $json = <$fh>;
close $fh; close $fh;
if ($as_string) { if ($as_string) {
$result = $json; $result = $json;
} else { } else {
$result = decode_json($json); $result = decode_json($json);
} }
debug(__LINE__, "Read JSON from $filepath"); debug(__LINE__, "Read JSON from $filepath");
}; };
if ($@) { if ($@) {
debug(__LINE__, "Error reading $filepath: $@"); debug(__LINE__, "Error reading $filepath: $@");
return; return;
} }
return $result; return $result;
} }
# ============================================================================ # ============================================================================
# CSV helper # CSV helper
# ============================================================================ # ============================================================================
sub parse_csv_line { sub parse_csv_line {
my ($line, $expected_fields) = @_; my ($line, $expected_fields) = @_;
return unless $line; return unless $line;
$line =~ s/^\s+|\s+$//g; $line =~ s/^\s+|\s+$//g;
my @values = map { s/^\s+|\s+$//gr } split(/,/, $line); my @values = map { s/^\s+|\s+$//gr } split(/,/, $line);
return unless !$expected_fields || @values >= $expected_fields; return unless !$expected_fields || @values >= $expected_fields;
return @values; return @values;
} }
1; 1;

View File

@ -1,27 +1,27 @@
# pve-mod :: node_info file manifest # pve-mod :: node_info file manifest
# Maps files in this directory to their installation destinations. # Maps files in this directory to their installation destinations.
# Format: <source> <destination> [permission] # Format: <source> <destination> [permission]
# source - path relative to this files/ directory # source - path relative to this files/ directory
# destination - path relative to the package root (no leading slash) # destination - path relative to the package root (no leading slash)
# permission - octal mode, optional (defaults to 644) # permission - octal mode, optional (defaults to 644)
# Read by src/gen-rules.sh to generate the per-module debian install rules. # Read by src/gen-rules.sh to generate the per-module debian install rules.
# PVE API2 facade # PVE API2 facade
PveMod_SensorInfo.pm usr/share/perl5/PVE/API2/PVEMod_SensorInfo.pm PveMod_SensorInfo.pm usr/share/perl5/PVE/API2/PVEMod_SensorInfo.pm
# PVEMod core modules # PVEMod core modules
Config.pm usr/share/perl5/PVE/PVEMod/Config.pm Config.pm usr/share/perl5/PVE/PVEMod/Config.pm
Utils.pm usr/share/perl5/PVE/PVEMod/Utils.pm Utils.pm usr/share/perl5/PVE/PVEMod/Utils.pm
Store.pm usr/share/perl5/PVE/PVEMod/Store.pm Store.pm usr/share/perl5/PVE/PVEMod/Store.pm
ProcessManager.pm usr/share/perl5/PVE/PVEMod/ProcessManager.pm ProcessManager.pm usr/share/perl5/PVE/PVEMod/ProcessManager.pm
# Collector plugins # Collector plugins
Collector/Intel.pm usr/share/perl5/PVE/PVEMod/Collector/Intel.pm Collector/Intel.pm usr/share/perl5/PVE/PVEMod/Collector/Intel.pm
Collector/Nvidia.pm usr/share/perl5/PVE/PVEMod/Collector/Nvidia.pm Collector/Nvidia.pm usr/share/perl5/PVE/PVEMod/Collector/Nvidia.pm
Collector/Amd.pm usr/share/perl5/PVE/PVEMod/Collector/Amd.pm Collector/Amd.pm usr/share/perl5/PVE/PVEMod/Collector/Amd.pm
Collector/LmSensors.pm usr/share/perl5/PVE/PVEMod/Collector/LmSensors.pm Collector/LmSensors.pm usr/share/perl5/PVE/PVEMod/Collector/LmSensors.pm
Collector/Ups.pm usr/share/perl5/PVE/PVEMod/Collector/Ups.pm Collector/Ups.pm usr/share/perl5/PVE/PVEMod/Collector/Ups.pm
Collector/systemInformation.pm usr/share/perl5/PVE/PVEMod/Collector/SystemInformation.pm Collector/systemInformation.pm usr/share/perl5/PVE/PVEMod/Collector/SystemInformation.pm
# JS module (rename to match loader reference) # JS module (rename to match loader reference)
PveMod_pvemanagerlib.js usr/share/pve-manager/js/PveMod_PveNodeStatusView.js PveMod_pvemanagerlib.js usr/share/pve-manager/js/PveMod_PveNodeStatusView.js

View File

@ -1,45 +1,45 @@
# pve-mod :: node_info mod configuration # pve-mod :: node_info mod configuration
# Settings for the node-info / sensor-monitoring mod. # Settings for the node-info / sensor-monitoring mod.
# Managed by pve-mod-configure. Re-run to update. # Managed by pve-mod-configure. Re-run to update.
[gpu] [gpu]
intel_enabled=0 intel_enabled=0
nvidia_enabled=0 nvidia_enabled=0
amd_enabled=0 amd_enabled=0
gpu_history=0 gpu_history=0
[lm_sensors] [lm_sensors]
enabled=0 enabled=0
enable_cpu=0 enable_cpu=0
cpu_temp_target=Core cpu_temp_target=Core
enable_ram_temp=0 enable_ram_temp=0
enable_hdd_temp=0 enable_hdd_temp=0
enable_nvme_temp=0 enable_nvme_temp=0
enable_fan_speed=0 enable_fan_speed=0
display_zero_speed_fans=0 display_zero_speed_fans=0
temp_unit=C temp_unit=C
[ups] [ups]
enabled=0 enabled=0
device_name=ups@localhost device_name=ups@localhost
[system_info] [system_info]
enabled=0 enabled=0
type=1 type=1
# Debug mode: when a collector's mode is 1, the real tool is not required. # Debug mode: when a collector's mode is 1, the real tool is not required.
# Data is read from the file path instead. Useful for development/testing. # Data is read from the file path instead. Useful for development/testing.
[debug] [debug]
lm_sensors_mode=0 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
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
amd_mode=0 amd_mode=0
amd_devices_file=/tmp/amd-gpu-devices.json amd_devices_file=/tmp/amd-gpu-devices.json
ups_mode=0 ups_mode=0
ups_output_file=/tmp/ups-output.json ups_output_file=/tmp/ups-output.json
log_enabled=0 log_enabled=0
log_file=/tmp/pve-mod-debug.log log_file=/tmp/pve-mod-debug.log

View File

@ -1,9 +1,9 @@
# pve-mod :: node_info patch manifest # pve-mod :: node_info patch manifest
# Format: <patch-file> [section.key=value] # Format: <patch-file> [section.key=value]
# Patches are applied top-to-bottom. An optional condition (read from this mod's # Patches are applied top-to-bottom. An optional condition (read from this mod's
# conf.d file, /etc/pve-mod/conf.d/node_info.conf) gates a patch; it is applied # conf.d file, /etc/pve-mod/conf.d/node_info.conf) gates a patch; it is applied
# only when the key equals the given value. # only when the key equals the given value.
01-nodes-pm-sensors.patch 01-nodes-pm-sensors.patch
02-nodes-pm-GPU-RRD-history.patch gpu.gpu_history=1 02-nodes-pm-GPU-RRD-history.patch gpu.gpu_history=1
03-pvemanager-js-sensors.patch 03-pvemanager-js-sensors.patch

View File

@ -1,3 +1,3 @@
## Draft code ## Draft code
This version of PVEMod is as draft version and may or may not be fully functional. This version of PVEMod is as draft version and may or may not be fully functional.
The installer is currently not working and is work in progress. The installer is currently not working and is work in progress.

View File

@ -1,19 +1,19 @@
# pve-mod main configuration file # pve-mod main configuration file
# Run 'pve-mod-configure' to set values interactively. # Run 'pve-mod-configure' to set values interactively.
# #
# This file only declares which mods are enabled. Each mod keeps its own # This file only declares which mods are enabled. Each mod keeps its own
# settings in /etc/pve-mod/conf.d/<mod>.conf # settings in /etc/pve-mod/conf.d/<mod>.conf
# #
# When a mod flag below is 1, its patches are (re)applied on install and, # When a mod flag below is 1, its patches are (re)applied on install and,
# if [pve_trigger] enabled=1, after every pve-manager upgrade. # if [pve_trigger] enabled=1, after every pve-manager upgrade.
[modules] [modules]
node_info=0 node_info=0
nag_screen=0 nag_screen=0
# Re-apply patches automatically after a pve-manager upgrade (dpkg trigger). # Re-apply patches automatically after a pve-manager upgrade (dpkg trigger).
[pve_trigger] [pve_trigger]
enabled=0 enabled=0
[service] [service]
mode=embedded mode=embedded

View File

@ -1 +1 @@
trigger workflow test 1, 2, 3 trigger workflow test 1, 2, 3, 4