From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 82C2D1FF17A for ; Tue, 14 Oct 2025 23:22:00 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 29026CCBC; Tue, 14 Oct 2025 23:22:13 +0200 (CEST) To: pve-devel@lists.proxmox.com Date: Tue, 14 Oct 2025 21:36:01 +0200 In-Reply-To: <20251014193601.34285-1-davide.guerri@gmail.com> References: <20251014193601.34285-1-davide.guerri@gmail.com> MIME-Version: 1.0 Message-ID: List-Id: Proxmox VE development discussion List-Post: From: Davide Guerri via pve-devel Precedence: list Cc: Davide Guerri X-Mailman-Version: 2.1.29 X-BeenThere: pve-devel@lists.proxmox.com List-Subscribe: , List-Unsubscribe: , List-Archive: Reply-To: Proxmox VE development discussion List-Help: Subject: [pve-devel] [PATCH container 1/1] sensors: add hardware sensor module for CPUs, disks, and fans Content-Type: multipart/mixed; boundary="===============6050815948427227828==" Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" --===============6050815948427227828== Content-Type: message/rfc822 Content-Disposition: inline Return-Path: X-Original-To: pve-devel@lists.proxmox.com Delivered-To: pve-devel@lists.proxmox.com Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id F3356DD7AB for ; Tue, 14 Oct 2025 23:22:11 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CE29DCBA6 for ; Tue, 14 Oct 2025 23:21:41 +0200 (CEST) Received: from mail-wr1-x42e.google.com (mail-wr1-x42e.google.com [IPv6:2a00:1450:4864:20::42e]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 14 Oct 2025 23:21:40 +0200 (CEST) Received: by mail-wr1-x42e.google.com with SMTP id ffacd0b85a97d-426f1574a14so584910f8f.3 for ; Tue, 14 Oct 2025 14:21:40 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1760476894; x=1761081694; darn=lists.proxmox.com; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=TtgdyV4mC7u8nSW4+7ZiElhtB555rHnUupWnHzZYlOg=; b=akX67PbytSYl8Y+2SkLVuV83OjQhSMyUnCZhqD0TSRZTEao8mp27O43Ac5tDBY6eIG kawT0oRV0FZhSF9zaGe8Cu0X3I2HjD0PsbVybdOu6ocJ3PZmY1byeELllt/hDggLjFdY bUXytebcZ+TTxLi/iQiVMVum+HM1UNuAz7fC0R/sBrDz+drwJX52EZ3j+3LNER/k/Kr1 UXJel44qo5OPfSmEHbLdPfbwsS9pTV2aYVFB7qKYMy1oYGY7PyRh3u+fwQOeZ+RosyEs AiOwVOECmNKoYB8jhXV0IKhYaOI5qdFQOkRYfFdFtxON0VtUC144lGSWRoMJLL0ieyw1 gGAQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1760476894; x=1761081694; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=TtgdyV4mC7u8nSW4+7ZiElhtB555rHnUupWnHzZYlOg=; b=iIKIGrxZR6rm3/jymUAo6IAN5bJV8uG9MqkR9Yj2Ks/SgQ84sRXwjQo2BaXoPy893G xG+I2jFptO4tANeg8QfWvWUug2BzdOZI9SslDIGg00joQX37cElbDEDJUr7O1GfLhV/L EriiUIaNQmF4yAixzzAc1T+Zbs472HB+jIF/mbBJVXblNSC8J8qTF9yHqCKOHWNaEpZo SHCPwPXicmXzfmOkETaqJckWDKT/F98CDGhFe4OfsPCGw1tEJ9eVupREqGe74OIaXt+G uZdXmKvw2ufY3CPA+Q5PZARF+Ycw2H68BE09kNSBFrvEua054h7fdJ1LqkYSPHm0onuu r5Xg== X-Gm-Message-State: AOJu0Yy94fg83o405YmeRPw7WiUaobfaKkCsXaqadCxDnICaWyyWzYl2 OA9M31f0y7zE2EtOPHqpDvELGjf9CA3eWUED++NifgX2Mmxh6fInVVqcdFufGzvK X-Gm-Gg: ASbGncuUFW7qFN4KuZ9/oCSxNXP4+KZL4GNf9DkScQHkPtNYCxStPjiJAG2jYYO3zLC GtdpFVJ2FBT2ybMcKoiuQg15rfwIseei+p0de9//vkbfIfCSgKCtX20gjv66xAZe04w5KkJi2w3 WvNu30zPDHL9jE5rMS7iRn5HIqAqQmKaemFqnS/1/pTCRXZReM12EcPKKIrCLxd0InTNQohidCR GUeWAmLkFRpcZxBSmMXSuTMeYs1eT9K6Cks1fmRFzZw6oBOInbDnNI7HzLhtLh7pU2grNSFIuhc ztZgPAY4MJjAZLM2v5GBmWFi+mN00pIs5E9rAopxzKzQ0fcTebzo0CnDmU0ojRXwKXeKWZsyQs9 38p93f5usf5xoZ5GcNZwxEokzsYI1Qolni8bKF0AQ831gkxuBFgG2SvygsyNNuHWrsSYylbDb1l SjBCUn3TZAFQnf7f03CqY6raET8n9/EGaxLR81OIAQDYQ= X-Google-Smtp-Source: AGHT+IGep5XXKbUXChQPCKsIlv8B+tbMBL/a8XFSPB+GD4NpKhY1ORQUIIcFMFjoAL9nsrTGJ1tt+Q== X-Received: by 2002:a05:6000:2305:b0:3e9:9f7f:6c36 with SMTP id ffacd0b85a97d-4266e8db5f6mr16348065f8f.54.1760476893448; Tue, 14 Oct 2025 14:21:33 -0700 (PDT) Received: from lechuck (host-79-43-235-239.retail.telecomitalia.it. [79.43.235.239]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-426ce57d49bsm26618687f8f.10.2025.10.14.14.21.29 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Tue, 14 Oct 2025 14:21:31 -0700 (PDT) Received: by lechuck.localdomain (Postfix, from userid 502) id CC29C7527B1F; Tue, 14 Oct 2025 21:36:05 +0200 (CEST) From: Davide Guerri To: pve-devel@lists.proxmox.com Subject: [PATCH container 1/1] sensors: add hardware sensor module for CPUs, disks, and fans Date: Tue, 14 Oct 2025 21:36:01 +0200 Message-ID: <20251014193601.34285-2-davide.guerri@gmail.com> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20251014193601.34285-1-davide.guerri@gmail.com> References: <20251014193601.34285-1-davide.guerri@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.925 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's domain DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from domain DMARC_PASS -0.1 DMARC pass policy FREEMAIL_FROM 0.001 Sender email is commonly abused enduser mail provider KAM_MAILER 2 Automated Mailer Tag Left in Email RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Add new PVE::SensorInfo.pm module providing functionality to read temperature, fan, and other sensor data from /sys/class/hwmon. Includes driver mappings for various hardware (Intel, AMD, ASUS, etc.) and CPU core topology integration for accurate per-core temperature reporting. Signed-off-by: Davide Guerri --- src/Makefile | 1 + src/PVE/SensorInfo.pm | 385 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 src/PVE/SensorInfo.pm diff --git a/src/Makefile b/src/Makefile index 0ca817b..388ebbb 100644 --- a/src/Makefile +++ b/src/Makefile @@ -32,6 +32,7 @@ LIB_SOURCES = \ RESTHandler.pm \ SafeSyslog.pm \ SectionConfig.pm \ + SensorInfo.pm \ SysFSTools.pm \ Syscall.pm \ Systemd.pm \ diff --git a/src/PVE/SensorInfo.pm b/src/PVE/SensorInfo.pm new file mode 100644 index 0000000..3938a4e --- /dev/null +++ b/src/PVE/SensorInfo.pm @@ -0,0 +1,385 @@ +package PVE::SensorInfo; + +use v5.36; + +use PVE::Tools qw(file_read_firstline dir_glob_foreach); + +my $hwmon_base = "/sys/class/hwmon"; +my $cpu_base = "/sys/devices/system/cpu"; + +# CPU sensor driver names +my %cpu_drivers = ( + 'coretemp' => 'Intel', + 'k10temp' => 'AMD', + 'k8temp' => 'AMD K8', + 'zenpower' => 'AMD Ryzen', + 'fam15h_power' => 'AMD', + 'atk0110' => 'ASUS', + 'w83627ehf' => 'Winbond', + 'nct6775' => 'Nuvoton', + 'it87' => 'ITE', + 'hp_wmi' => 'HPE', + 'asus_wmi' => 'ASUS WMI', +); + +# Disk sensor driver names +my %disk_drivers = ( + 'drivetemp' => 'SATA/SAS', + 'nvme' => 'NVMe', +); + +# Fan sensor driver names (motherboard and chassis fans) +my %fan_drivers = ( + 'nct6775' => 'Nuvoton', + 'it87' => 'ITE', + 'w83627ehf' => 'Winbond', + 'asus_wmi_sensors' => 'ASUS WMI', + 'atk0110' => 'ASUS', + 'dell_smm' => 'Dell', + 'gigabyte_waterforce' => 'Gigabyte', + 'corsairpsu' => 'Corsair PSU', + 'nzxt_smart2' => 'NZXT', + 'drivetemp' => 'Drive', +); + +my $classify_temperature_sensor = sub { + my ($hwmon, $name) = @_; + + return 'cpu' if exists $cpu_drivers{$name}; + return 'disk' if exists $disk_drivers{$name}; + + return 'other'; +}; + +my $build_core_mapping = sub { + my $core_map = {}; + my $seen_cores = {}; + my $logical_num = 0; + + dir_glob_foreach( + $cpu_base, + 'cpu\d+', + sub { + my ($cpu) = @_; + + my $cpu_path = "$cpu_base/$cpu"; + my $core_id_file = "$cpu_path/topology/core_id"; + my $package_id_file = "$cpu_path/topology/physical_package_id"; + + return if !-r $core_id_file; + + my $physical_core = file_read_firstline($core_id_file); + my $package = file_read_firstline($package_id_file); + $package = 0 if !defined($package); + + return if !defined($physical_core); + + my $key = "${package}:${physical_core}"; + + if (!exists $seen_cores->{$key}) { + my $info = { + logical_num => $logical_num, + physical_id => $physical_core, + package => $package, + }; + + $core_map->{$key} = $info; + $seen_cores->{$key} = 1; + $logical_num++; + } + }, + ); + + return $core_map; +}; + +my $get_disk_device_from_hwmon = sub { + my ($hwmon_path, $sensor_name) = @_; + + # For NVMe devices: /sys/class/hwmon/hwmonX/device -> ../../nvmeY + # The nvme controller directory contains nvmeYn1 subdirectory + # (namespace) + if ($sensor_name eq 'nvme') { + if (-l "$hwmon_path/device") { + my $device_link = readlink("$hwmon_path/device"); + if (defined($device_link) && $device_link =~ m/(nvme\d+)/) { + my $nvme_ctrl = $1; + # Look for the first namespace (typically nvmeXn1) + my $device_path = "$hwmon_path/device"; + if (opendir(my $dh, $device_path)) { + my @namespaces = (); + while (my $entry = readdir($dh)) { + if ($entry =~ m/^(nvme\d+n\d+)$/) { + push @namespaces, $entry; + } + } + closedir($dh); + if (@namespaces) { + # Return the first namespace, sorted (usually nvmeXn1) + return (sort @namespaces)[0]; + } + } + } + } + } + + # For drivetemp devices: name format is "drivetemp" + # Device path: /sys/class/hwmon/hwmonX/device -> ../../X:Y:Z:W/ + # (SCSI host:channel:id:lun) + if ($sensor_name eq 'drivetemp') { + if (-l "$hwmon_path/device") { + my $device_link = readlink("$hwmon_path/device"); + if (defined($device_link)) { + # Extract SCSI address (e.g., "../../6:0:0:0/") + if ($device_link =~ m/(\d+):(\d+):(\d+):(\d+)/) { + my ($host, $channel, $id, $lun) = ($1, $2, $3, $4); + + my $scsi_device_path = + "/sys/class/scsi_disk/" . "${host}:${channel}:${id}:${lun}/device"; + + # Look for the block device + my $scsi_block_path = "${scsi_device_path}/block"; + if (-d $scsi_block_path) { + # Find the block device name (e.g., "sda") + opendir(my $dh, $scsi_block_path) or return undef; + while (my $entry = readdir($dh)) { + next if $entry =~ /^\./; + closedir($dh); + return $entry; + } + closedir($dh); + } + } + } + } + } + + return undef; +}; + +# returns hashref of current TEMPERATURE readings for given sensor type +# type can be 'cpu', 'disk', 'other', or 'all' (default) +# +# return format: +# { +# 'coretemp/Core 0' => { +# temperature => 45.0, # in degrees Celsius +# unit => 'celsius', +# type => 'cpu', +# driver => 'Intel', +# max => 100.0, # optional +# critical => 100.0, # optional +# logical_core => 0, # optional, for CPU cores +# physical_core => 0, # optional, for CPU cores +# package => 0, # optional, for CPU cores +# }, +# 'hwmon4/Sensor 1' => { +# temperature => 35.0, # in degrees Celsius +# unit => 'celsius', +# type => 'disk', +# driver => 'SATA/SAS', +# device => 'sda', # block device name (for disks) +# }, +# 'hwmon1/Composite' => { +# temperature => 42.0, # in degrees Celsius +# unit => 'celsius', +# type => 'disk', +# driver => 'NVMe', +# device => 'nvme0n1', # block device name (for disks) +# }, +# 'hwmon2/Composite' => { +# temperature => 43.0, # in degrees Celsius +# unit => 'celsius', +# type => 'disk', +# driver => 'NVMe', +# device => 'nvme1n1', # block device name (for disks) +# }, +# ... +# } +# +sub read_temperatures { + my ($type) = @_; + + $type = 'all' if !defined($type); + my @types = ($type eq 'all') ? qw(cpu disk other) : ($type); + + my $results = {}; + return $results if !-d $hwmon_base; + + # Build core mapping once for all sensors + my $core_mapping = $build_core_mapping->(); + + dir_glob_foreach( + $hwmon_base, + 'hwmon\d+', + sub { + my ($hwmon) = @_; + + my $hwmon_path = "$hwmon_base/$hwmon"; + my $sensor_name = file_read_firstline("$hwmon_path/name"); + return if !defined($sensor_name); + + # Determine sensor type and skip if not requested + my $sensor_type = $classify_temperature_sensor->($hwmon_path, $sensor_name); + return if !grep { $_ eq $sensor_type } @types; + + my $driver = $cpu_drivers{$sensor_name} || $disk_drivers{$sensor_name} || 'Unknown'; + my $is_cpu_sensor = exists $cpu_drivers{$sensor_name}; + my $is_disk_sensor = exists $disk_drivers{$sensor_name}; + + # Get disk device information for disk sensors + my $disk_device; + if ($is_disk_sensor) { + $disk_device = $get_disk_device_from_hwmon->($hwmon_path, $sensor_name); + } + + # Read all temperature sensors for this hwmon device + dir_glob_foreach( + $hwmon_path, + 'temp\d+_input', + sub { + my ($temp_input) = @_; + my ($num) = $temp_input =~ /temp(\d+)_input$/; + return if !defined($num); + + # Read current temperature + my $raw_temp = file_read_firstline("$hwmon_path/$temp_input"); + return if !defined($raw_temp); + + my $temp_c = $raw_temp / 1000.0; + + # Get label + my $label = file_read_firstline("$hwmon_path/temp${num}_label"); + $label = "Sensor $num" if !defined($label); + + # Create unique key: use hwmon ID for disk sensors, sensor name for others + my $key = $is_disk_sensor ? "$hwmon/$label" : "$sensor_name/$label"; + + $results->{$key} = { + temperature => $temp_c, + unit => 'celsius', + type => $sensor_type, + driver => $driver, + }; + + # Add CPU topology information + if ($is_cpu_sensor && $label =~ /^Core (\d+)$/) { + my $physical_id = $1; + foreach my $map_key (keys %$core_mapping) { + my $map = $core_mapping->{$map_key}; + if ($map->{physical_id} eq $physical_id) { + $results->{$key}->{logical_core} = $map->{logical_num}; + $results->{$key}->{physical_core} = $physical_id; + $results->{$key}->{package} = $map->{package}; + last; + } + } + } + + # Add disk device information + if ($is_disk_sensor && defined($disk_device)) { + $results->{$key}->{device} = $disk_device; + } + + # Add temperature limits + my $max = file_read_firstline("$hwmon_path/temp${num}_max"); + $results->{$key}->{max} = $max / 1000.0 if defined($max); + + my $crit = file_read_firstline("$hwmon_path/temp${num}_crit"); + $results->{$key}->{critical} = $crit / 1000.0 if defined($crit); + }, + ); + }, + ); + + return $results; +} + +# returns hashref of current FAN SPEED readings +# +# return format: +# { +# 'nct6775/CPU Fan' => { +# speed => 1500, # in RPM +# unit => 'rpm', +# driver => 'Nuvoton', +# min => 0, # optional +# max => 2000, # optional +# target => 1500, # optional +# alarm => 0, # optional, 0 or 1 +# }, +# ... +# } +# +sub read_fan_speeds { + my $results = {}; + return $results if !-d $hwmon_base; + + dir_glob_foreach( + $hwmon_base, + 'hwmon\d+', + sub { + my ($hwmon) = @_; + + my $hwmon_path = "$hwmon_base/$hwmon"; + my $sensor_name = file_read_firstline("$hwmon_path/name"); + return if !defined($sensor_name); + + my $driver = $fan_drivers{$sensor_name} || 'Unknown'; + + # Read all fan sensors for this hwmon device + dir_glob_foreach( + $hwmon_path, + 'fan\d+_input', + sub { + my ($fan_input) = @_; + my ($num) = $fan_input =~ /fan(\d+)_input$/; + return if !defined($num); + + # Read current fan speed + my $raw_speed = file_read_firstline("$hwmon_path/$fan_input"); + return if !defined($raw_speed); + + # Get label + my $label = file_read_firstline("$hwmon_path/fan${num}_label"); + $label = "Fan $num" if !defined($label); + + my $key = "$sensor_name/$label"; + + $results->{$key} = { + speed => int($raw_speed), + unit => 'rpm', + driver => $driver, + }; + + # Add optional fan metadata + my $min = file_read_firstline("$hwmon_path/fan${num}_min"); + $results->{$key}->{min} = int($min) if defined($min); + + my $max = file_read_firstline("$hwmon_path/fan${num}_max"); + $results->{$key}->{max} = int($max) if defined($max); + + my $target = file_read_firstline("$hwmon_path/fan${num}_target"); + $results->{$key}->{target} = int($target) if defined($target); + + my $alarm = file_read_firstline("$hwmon_path/fan${num}_alarm"); + $results->{$key}->{alarm} = int($alarm) if defined($alarm); + }, + ); + }, + ); + + return $results; +} + +# convenience function: read CPU temperatures only +sub read_cpu_temps { + return read_temperatures('cpu'); +} + +# convenience function: read disk temperatures only +sub read_disk_temps { + return read_temperatures('disk'); +} + +1; -- 2.50.1 (Apple Git-155) --===============6050815948427227828== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel --===============6050815948427227828==--