PVE-mods/src/PVENodeInfo/PVEMod_Collectors/LmSensors.pm
2026-06-07 11:17:27 +02:00

418 lines
15 KiB
Perl

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