From 7bb24044f2ce3d24ba560bac565104549ced6a33 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 28 Aug 2026 13:52:47 +0200 Subject: [PATCH] Add new Hardware Information module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Add a read-only module that summarizes the system, mainboard, boot firmware, Secure Boot and TPM state, usable memory and processors, and inventories PCI, USB, storage, network and hardware-monitor devices with their bound kernel drivers and modules. All data is read directly from "sysfs" and "procfs" without external commands, with friendly device names loaded from the standard "pci.ids" and "usb.ids" databases when installed. https://forum.virtualmin.com/t/show-server-information/137862/ --- CHANGELOG.md | 1 + hardware-info/hardware-info-lib.pl | 935 +++++++++++++++++++++++++++++ hardware-info/help/intro.html | 11 + hardware-info/images/icon.gif | Bin 0 -> 914 bytes hardware-info/images/icon.svg | 11 + hardware-info/index.cgi | 320 ++++++++++ hardware-info/install_check.pl | 19 + hardware-info/lang/en | 288 +++++++++ hardware-info/module.cgi | 57 ++ hardware-info/module.info | 6 + hardware-info/t/perlcritic.t | 60 ++ hardware-info/t/run-tests.t | 421 +++++++++++++ hardware-info/view.cgi | 112 ++++ mod_core_list.txt | 2 +- mod_full_list.txt | 2 +- 15 files changed, 2243 insertions(+), 2 deletions(-) create mode 100644 hardware-info/hardware-info-lib.pl create mode 100644 hardware-info/help/intro.html create mode 100644 hardware-info/images/icon.gif create mode 100644 hardware-info/images/icon.svg create mode 100755 hardware-info/index.cgi create mode 100644 hardware-info/install_check.pl create mode 100644 hardware-info/lang/en create mode 100755 hardware-info/module.cgi create mode 100644 hardware-info/module.info create mode 100644 hardware-info/t/perlcritic.t create mode 100644 hardware-info/t/run-tests.t create mode 100755 hardware-info/view.cgi diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0ab5600..baa7b8fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Changelog #### 2.661 (September, 2026) +* Add new Hardware Information module for inspecting system, firmware, security, PCI, USB, storage, network, processor, sensor, driver, and kernel module details * Add options to send Webmin and Usermin errors to the systemd journal [forum.virtualmin.com/t/136562](https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562) * Add webserver logging controls to Usermin Configuration module * Add option to rotate Webmin and Usermin webserver logs using `logrotate` instead of periodically clearing them [#2821](https://github.com/webmin/webmin/pull/2821) diff --git a/hardware-info/hardware-info-lib.pl b/hardware-info/hardware-info-lib.pl new file mode 100644 index 000000000..85aaae1df --- /dev/null +++ b/hardware-info/hardware-info-lib.pl @@ -0,0 +1,935 @@ +#!/usr/local/bin/perl +# Functions for collecting read-only Linux hardware information. + +use strict; +use warnings; +use POSIX qw(uname); +use Cwd qw(abs_path); + +BEGIN { push(@INC, ".."); } +use WebminCore; + +init_config(); + +our (%config, %gconfig, %text); +our $hardware_sys_root; +our $hardware_proc_root; +our $hardware_pci_ids_file; +our $hardware_usb_ids_file; +our $hardware_modinfo_command; +our %hardware_ids_cache; + +$hardware_sys_root ||= "/sys"; +$hardware_proc_root ||= "/proc"; + +# hardware_device_types() +# Returns the device groups shown by this module, in display order. +sub hardware_device_types +{ +return qw(pci usb storage network cpu sensor); +} + +# list_hardware_devices(type) +# Returns all devices in one of the supported inventory groups. +sub list_hardware_devices +{ +my ($type) = @_; +return list_pci_devices() if ($type eq 'pci'); +return list_usb_devices() if ($type eq 'usb'); +return list_storage_devices() if ($type eq 'storage'); +return list_network_devices() if ($type eq 'network'); +return list_cpu_devices() if ($type eq 'cpu'); +return list_sensor_devices() if ($type eq 'sensor'); +return ( ); +} + +# get_hardware_device(type, id) +# Finds a device by its exact enumerated ID. The ID is never used as a path. +sub get_hardware_device +{ +my ($type, $id) = @_; +return undef if (!defined($id)); +foreach my $device (list_hardware_devices($type)) { + return $device if ($device->{'id'} eq $id); + } +return undef; +} + +# hardware_system_summary([processors]) +# Returns the host, kernel, memory, processor and DMI report-card fields. +sub hardware_system_summary +{ +my ($processors) = @_; +my ($sysname, $nodename, $release, $version, $machine) = uname(); +my $os_type = $gconfig{'real_os_type'} || $gconfig{'os_type'} || $sysname; +my $os_version = $gconfig{'real_os_version'} || + $gconfig{'os_version'} || ""; +my $memory = hardware_memtotal(); +my @cpus = $processors ? @$processors : list_cpu_devices(); +my %models; +my %sockets; +foreach my $cpu (@cpus) { + $models{$cpu->{'model'}}++ if ($cpu->{'model'}); + $sockets{$cpu->{'socket'}}++ if (defined($cpu->{'socket'}) && + $cpu->{'socket'} ne ""); + } + +my %summary = ( + 'hostname' => get_system_hostname() || $nodename, + 'os' => join(" ", grep { defined($_) && $_ ne "" } + ($os_type, $os_version)), + 'kernel' => join(" ", grep { defined($_) && $_ ne "" } + ($sysname, $release)), + 'architecture' => $machine, + 'memory' => $memory, + 'cpu_count' => scalar(@cpus), + 'cpu_models' => [ sort keys(%models) ], + 'cpu_sockets' => scalar(keys(%sockets)), + ); + +# Fixed sysfs locations expose boot firmware and TPM presence without tools. +$summary{'firmware_mode'} = -d "$hardware_sys_root/firmware/efi" ? 'uefi' : + $machine =~ /^(?:i.86|x86_64)$/i ? 'bios' : 'other'; +$summary{'secure_boot'} = hardware_secure_boot_status() + if ($summary{'firmware_mode'} eq 'uefi'); +my @tpms = grep { /^tpm\d+$/ } + hardware_directory_entries("$hardware_sys_root/class/tpm"); +$summary{'tpms'} = \@tpms; + +# DMI exposes the system, board, chassis and firmware identity without tools. +my %dmi_fields = ( + 'system_vendor' => 'sys_vendor', + 'system_product' => 'product_name', + 'system_version' => 'product_version', + 'system_serial' => 'product_serial', + 'system_uuid' => 'product_uuid', + 'board_vendor' => 'board_vendor', + 'board_name' => 'board_name', + 'board_version' => 'board_version', + 'board_serial' => 'board_serial', + 'chassis_vendor' => 'chassis_vendor', + 'chassis_type' => 'chassis_type', + 'chassis_version' => 'chassis_version', + 'chassis_serial' => 'chassis_serial', + 'bios_vendor' => 'bios_vendor', + 'bios_version' => 'bios_version', + 'bios_date' => 'bios_date', + ); +foreach my $key (keys(%dmi_fields)) { + my $value = hardware_read_value( + "$hardware_sys_root/class/dmi/id/$dmi_fields{$key}"); + $summary{$key} = $value if (defined($value) && $value ne ""); + } +return \%summary; +} + +# hardware_memtotal() +# Returns total usable memory in bytes, or undef when procfs is unavailable. +sub hardware_memtotal +{ +my $data = hardware_read_file("$hardware_proc_root/meminfo"); +return undef if (!defined($data)); +return $1 * 1024 if ($data =~ /^MemTotal:\s+(\d+)\s+kB/im); +return undef; +} + +# hardware_secure_boot_status() +# Returns the UEFI Secure Boot flag, or undef when firmware does not expose it. +sub hardware_secure_boot_status +{ +my $efivars = "$hardware_sys_root/firmware/efi/efivars"; +foreach my $entry (hardware_directory_entries($efivars, 1)) { + next if ($entry !~ /^SecureBoot-[0-9a-f-]+$/i); + my $value = hardware_read_file("$efivars/$entry"); + next if (!defined($value) || length($value) < 5); + return ord(substr($value, 4, 1)) ? 1 : 0; + } +return undef; +} + +# list_pci_devices() +# Enumerates PCI functions from sysfs, including their bound driver and module. +sub list_pci_devices +{ +my $root = "$hardware_sys_root/bus/pci/devices"; +my @devices; +foreach my $id (hardware_directory_entries($root)) { + my $path = "$root/$id"; + my $vendor = hardware_normalize_id(hardware_read_value("$path/vendor")); + my $device = hardware_normalize_id(hardware_read_value("$path/device")); + next if (!$vendor || !$device); + my $subvendor = hardware_normalize_id( + hardware_read_value("$path/subsystem_vendor")); + my $subdevice = hardware_normalize_id( + hardware_read_value("$path/subsystem_device")); + my $class_id = hardware_normalize_id(hardware_read_value("$path/class")); + my $names = hardware_id_names('pci', $vendor, $device, + $subvendor, $subdevice); + my $uevent = hardware_read_uevent("$path/uevent"); + my ($driver, $module) = hardware_driver_info($path); + $driver ||= $uevent->{'DRIVER'}; + my $name = $names->{'device'} || "PCI $vendor:$device"; + $name = "$names->{'vendor'} $name" if ($names->{'vendor'} && + $name !~ /^\Q$names->{'vendor'}\E\b/i); + my $class = hardware_pci_class($class_id); + my @details = ( + [ 'address', $id ], + [ 'class', $class ], + [ 'class_id', $class_id ], + [ 'vendor', hardware_id_label($names->{'vendor'}, $vendor) ], + [ 'device', hardware_id_label($names->{'device'}, $device) ], + [ 'subsystem_vendor', + hardware_id_label($names->{'subvendor'}, $subvendor) ], + [ 'subsystem_device', + hardware_id_label($names->{'subdevice'}, $subdevice) ], + [ 'revision', hardware_read_value("$path/revision") ], + [ 'irq', hardware_read_value("$path/irq") ], + [ 'numa_node', hardware_read_value("$path/numa_node") ], + [ 'iommu_group', hardware_link_name("$path/iommu_group") ], + [ 'local_cpus', hardware_read_value("$path/local_cpulist") ], + [ 'enabled', hardware_read_value("$path/enable"), 'yesno' ], + ); + push(@devices, { + 'id' => $id, + 'name' => $name, + 'class' => $class, + 'vendor' => $names->{'vendor'} || uc($vendor), + 'driver' => $driver, + 'module' => $module, + 'modules' => $module ? [ $module ] : [ ], + 'modalias' => $uevent->{'MODALIAS'}, + 'details' => \@details, + 'properties' => $uevent, + }); + } +return sort { $a->{'id'} cmp $b->{'id'} } @devices; +} + +# list_usb_devices() +# Enumerates physical USB devices and aggregates drivers from their interfaces. +sub list_usb_devices +{ +my $root = "$hardware_sys_root/bus/usb/devices"; +my @entries = hardware_directory_entries($root); +my @devices; +foreach my $id (@entries) { + my $path = "$root/$id"; + my $vendor = hardware_normalize_id(hardware_read_value("$path/idVendor")); + my $product = hardware_normalize_id(hardware_read_value("$path/idProduct")); + next if (!$vendor || !$product); + my $names = hardware_id_names('usb', $vendor, $product); + my $manufacturer = hardware_read_value("$path/manufacturer"); + my $product_name = hardware_read_value("$path/product"); + my $name = join(" ", grep { defined($_) && $_ ne "" } + ($manufacturer, $product_name)); + $name = join(" ", grep { defined($_) && $_ ne "" } + ($names->{'vendor'}, $names->{'device'})) if (!$name); + $name ||= "USB $vendor:$product"; + + # USB drivers bind to interfaces in most cases, not the parent device. + my @interfaces; + foreach my $interface_id (grep { /^\Q$id\E:\d+\.\d+$/ } @entries) { + my $interface_path = "$root/$interface_id"; + my ($driver, $module) = hardware_driver_info($interface_path); + my $class_id = hardware_normalize_id( + hardware_read_value("$interface_path/bInterfaceClass")); + push(@interfaces, { + 'id' => $interface_id, + 'name' => hardware_read_value("$interface_path/interface"), + 'class' => hardware_usb_class($class_id), + 'class_id' => $class_id, + 'driver' => $driver, + 'module' => $module, + }); + } + my ($parent_driver, $parent_module) = hardware_driver_info($path); + my @drivers = hardware_unique(grep { defined($_) && $_ ne "" } + ($parent_driver, map { $_->{'driver'} } @interfaces)); + my @modules = hardware_unique(grep { defined($_) && $_ ne "" } + ($parent_module, map { $_->{'module'} } @interfaces)); + my $uevent = hardware_read_uevent("$path/uevent"); + my $class_id = hardware_normalize_id( + hardware_read_value("$path/bDeviceClass")); + my @details = ( + [ 'location', $id ], + [ 'bus_number', hardware_read_value("$path/busnum") ], + [ 'device_number', hardware_read_value("$path/devnum") ], + [ 'vendor', hardware_id_label( + $manufacturer || $names->{'vendor'}, $vendor) ], + [ 'device', hardware_id_label( + $product_name || $names->{'device'}, $product) ], + [ 'serial', hardware_read_value("$path/serial") ], + [ 'usb_version', hardware_read_value("$path/version") ], + [ 'speed', hardware_read_value("$path/speed"), 'usb_speed' ], + [ 'class', hardware_usb_class($class_id) ], + [ 'class_id', $class_id ], + [ 'protocol', hardware_read_value("$path/bDeviceProtocol") ], + [ 'removable', hardware_read_value("$path/removable") ], + [ 'authorized', hardware_read_value("$path/authorized"), 'yesno' ], + ); + push(@devices, { + 'id' => $id, + 'name' => $name, + 'bus_number' => hardware_read_value("$path/busnum"), + 'device_number' => hardware_read_value("$path/devnum"), + 'speed' => hardware_read_value("$path/speed"), + 'driver' => join(", ", @drivers), + 'module' => @modules == 1 ? $modules[0] : undef, + 'modules' => \@modules, + 'modalias' => $uevent->{'MODALIAS'}, + 'interfaces' => \@interfaces, + 'details' => \@details, + 'properties' => $uevent, + }); + } +return sort { + ($a->{'bus_number'} || 0) <=> ($b->{'bus_number'} || 0) || + ($a->{'device_number'} || 0) <=> ($b->{'device_number'} || 0) || + $a->{'id'} cmp $b->{'id'} + } @devices; +} + +# list_storage_devices() +# Enumerates whole block devices, leaving partitions to disk-management modules. +sub list_storage_devices +{ +my $root = "$hardware_sys_root/class/block"; +my @devices; +foreach my $id (hardware_directory_entries($root)) { + my $path = "$root/$id"; + next if (-r "$path/partition"); + my $uevent = hardware_read_uevent("$path/uevent"); + my $sectors = hardware_read_value("$path/size"); + my $size = defined($sectors) && $sectors =~ /^\d+$/ ? + $sectors * 512 : undef; + my $vendor = hardware_read_value("$path/device/vendor"); + my $model = hardware_read_value("$path/device/model"); + my $serial = hardware_read_value("$path/device/serial"); + my $name = join(" ", grep { defined($_) && $_ ne "" } + ($vendor, $model)); + $name ||= $id; + my $target = readlink($path); + my $virtual = defined($target) && $target =~ m{/virtual/} ? 1 : 0; + my $removable = hardware_read_value("$path/removable"); + my $rotational = hardware_read_value("$path/queue/rotational"); + my $kind = $virtual ? 'virtual' : + $id =~ /^sr\d+$/ ? 'optical' : + $removable ? 'removable' : + defined($rotational) && !$rotational ? 'ssd' : 'disk'; + my ($driver, $module) = hardware_driver_info("$path/device"); + my $bus_device = hardware_link_name("$path/device"); + my $transport = hardware_link_name("$path/device/subsystem"); + my $firmware = hardware_read_value("$path/device/firmware_rev") || + hardware_read_value("$path/device/rev"); + my $wwid = hardware_read_value("$path/wwid") || + hardware_read_value("$path/device/wwid"); + my $discard_max = hardware_read_value("$path/queue/discard_max_bytes"); + my $discard = defined($discard_max) && $discard_max =~ /^\d+$/ ? + ($discard_max > 0 ? 1 : 0) : undef; + my @details = ( + [ 'device_name', "/dev/$id" ], + [ 'type', $kind, 'storage_type' ], + [ 'capacity', $size, 'size' ], + [ 'vendor', $vendor ], + [ 'model', $model ], + [ 'serial', $serial ], + [ 'firmware_revision', $firmware ], + [ 'wwid', $wwid ], + [ 'transport', $transport ], + [ 'bus_device', $bus_device ], + [ 'state', hardware_read_value("$path/device/state") ], + [ 'logical_block_size', + hardware_read_value("$path/queue/logical_block_size"), 'size' ], + [ 'physical_block_size', + hardware_read_value("$path/queue/physical_block_size"), 'size' ], + [ 'rotational', $rotational, 'yesno' ], + [ 'removable', $removable, 'yesno' ], + [ 'read_only', hardware_read_value("$path/ro"), 'yesno' ], + [ 'discard', $discard, 'yesno' ], + [ 'scheduler', hardware_read_value("$path/queue/scheduler") ], + ); + push(@devices, { + 'id' => $id, + 'name' => $name, + 'model' => $name ne $id ? $name : undef, + 'device' => "/dev/$id", + 'size' => $size, + 'kind' => $kind, + 'driver' => $driver, + 'module' => $module, + 'modules' => $module ? [ $module ] : [ ], + 'modalias' => $uevent->{'MODALIAS'}, + 'details' => \@details, + 'properties' => $uevent, + }); + } +return sort { $a->{'id'} cmp $b->{'id'} } @devices; +} + +# list_network_devices() +# Enumerates physical and virtual network interfaces and current link details. +sub list_network_devices +{ +my $root = "$hardware_sys_root/class/net"; +my @devices; +foreach my $id (hardware_directory_entries($root)) { + my $path = "$root/$id"; + my $uevent = hardware_read_uevent("$path/uevent"); + my ($driver, $module) = hardware_driver_info("$path/device"); + my $bus_device = hardware_link_name("$path/device"); + my $target = readlink($path); + my $virtual = !-e "$path/device" || + (defined($target) && $target =~ m{/virtual/}) ? 1 : 0; + my $wireless = -d "$path/wireless" ? 1 : 0; + my $type_id = hardware_read_value("$path/type"); + my $kind = $wireless ? 'wireless' : + $id eq 'lo' ? 'loopback' : + $virtual ? 'virtual' : + defined($type_id) && $type_id == 1 ? 'ethernet' : 'other'; + my $speed = hardware_read_value("$path/speed"); + $speed = undef if (defined($speed) && $speed !~ /^\d+$/); + my @details = ( + [ 'interface', $id ], + [ 'type', $kind, 'network_type' ], + [ 'address', hardware_read_value("$path/address") ], + [ 'state', hardware_read_value("$path/operstate") ], + [ 'carrier', hardware_read_value("$path/carrier"), 'yesno' ], + [ 'speed', $speed, 'network_speed' ], + [ 'duplex', hardware_read_value("$path/duplex") ], + [ 'mtu', hardware_read_value("$path/mtu") ], + [ 'bus_device', $bus_device ], + [ 'rx_bytes', hardware_read_value( + "$path/statistics/rx_bytes"), 'size' ], + [ 'tx_bytes', hardware_read_value( + "$path/statistics/tx_bytes"), 'size' ], + [ 'rx_packets', hardware_read_value( + "$path/statistics/rx_packets") ], + [ 'tx_packets', hardware_read_value( + "$path/statistics/tx_packets") ], + [ 'rx_errors', hardware_read_value( + "$path/statistics/rx_errors") ], + [ 'tx_errors', hardware_read_value( + "$path/statistics/tx_errors") ], + [ 'rx_dropped', hardware_read_value( + "$path/statistics/rx_dropped") ], + [ 'tx_dropped', hardware_read_value( + "$path/statistics/tx_dropped") ], + ); + push(@devices, { + 'id' => $id, + 'name' => $id, + 'kind' => $kind, + 'address' => hardware_read_value("$path/address"), + 'state' => hardware_read_value("$path/operstate"), + 'speed' => $speed, + 'driver' => $driver, + 'module' => $module, + 'modules' => $module ? [ $module ] : [ ], + 'modalias' => $uevent->{'MODALIAS'}, + 'details' => \@details, + 'properties' => $uevent, + }); + } +return sort { $a->{'id'} eq 'lo' ? -1 : + $b->{'id'} eq 'lo' ? 1 : $a->{'id'} cmp $b->{'id'} } @devices; +} + +# list_sensor_devices() +# Enumerates standard hwmon readings without invoking the sensors command. +sub list_sensor_devices +{ +my $root = "$hardware_sys_root/class/hwmon"; +my @devices; +foreach my $hwmon (hardware_directory_entries($root)) { + my $path = "$root/$hwmon"; + my $chip = hardware_read_value("$path/name") || $hwmon; + my ($driver, $module) = hardware_driver_info("$path/device"); + my $uevent = hardware_read_uevent("$path/device/uevent"); + foreach my $entry (hardware_directory_entries($path, 1)) { + next if ($entry !~ /^(temp|fan|in|curr|power)(\d+)_input$/); + my ($kind, $number) = ($1, $2); + my $raw = hardware_read_value("$path/$entry"); + my $reading = hardware_sensor_reading($kind, $raw); + next if (!defined($reading)); + my $label = hardware_read_value( + "$path/${kind}${number}_label") || + text('sensor_numbered', $text{"sensor_$kind"}, $number); + my $id = "$hwmon:$kind$number"; + my @details = ( + [ 'sensor_chip', $chip ], + [ 'sensor_type', $text{"sensor_$kind"} ], + [ 'sensor_reading', $reading ], + [ 'location', $id ], + ); + push(@devices, { + 'id' => $id, + 'name' => $label, + 'chip' => $chip, + 'kind' => $kind, + 'reading' => $reading, + 'driver' => $driver, + 'module' => $module, + 'modules' => $module ? [ $module ] : [ ], + 'modalias' => $uevent->{'MODALIAS'}, + 'details' => \@details, + 'properties' => $uevent, + }); + } + } +return sort { $a->{'chip'} cmp $b->{'chip'} || $a->{'id'} cmp $b->{'id'} } + @devices; +} + +# hardware_sensor_reading(kind, raw-value) +# Converts Linux hwmon base units into concise human-readable readings. +sub hardware_sensor_reading +{ +my ($kind, $raw) = @_; +return undef if (!defined($raw) || $raw !~ /^-?\d+$/); +my %scale = ( + 'temp' => [ 1000, 'format_celsius' ], + 'fan' => [ 1, 'format_rpm' ], + 'in' => [ 1000, 'format_volts' ], + 'curr' => [ 1000, 'format_amps' ], + 'power' => [ 1000000, 'format_watts' ], + ); +return undef if (!$scale{$kind}); +my $divisor = $scale{$kind}->[0]; +my $half = int($divisor / 2); +my $scaled = $raw * 100 + ($raw < 0 ? -$half : $half); +my $hundredths = int($scaled / $divisor); +my $absolute = abs($hundredths); +my $number = ($hundredths < 0 ? "-" : ""). + int($absolute / 100).".".sprintf("%02d", $absolute % 100); +$number =~ s/\.?0+$//; +return text($scale{$kind}->[1], $number); +} + +# list_cpu_devices() +# Enumerates logical processors from procfs and augments them with sysfs state. +sub list_cpu_devices +{ +my $data = hardware_read_file("$hardware_proc_root/cpuinfo"); +my @records; +my %shared; +if (defined($data)) { + foreach my $block (split(/\n\s*\n/, $data)) { + my %record; + foreach my $line (split(/\r?\n/, $block)) { + if ($line =~ /^([^:]+?)\s*:\s*(.*)$/) { + my ($key, $value) = (lc($1), $2); + $key =~ s/^\s+|\s+$//g; + $record{$key} = $value; + } + } + if (defined($record{'processor'}) && + $record{'processor'} =~ /^\d+$/) { + push(@records, \%record); + } + else { + %shared = (%shared, %record); + } + } + } + +# Some architectures provide sparse cpuinfo records, so sysfs is the fallback. +if (!@records) { + foreach my $entry (hardware_directory_entries( + "$hardware_sys_root/devices/system/cpu")) { + next if ($entry !~ /^cpu(\d+)$/); + push(@records, { 'processor' => $1 }); + } + } + +my @devices; +my $index = 0; +foreach my $record (@records) { + my $id = defined($record->{'processor'}) ? $record->{'processor'} : $index; + next if ($id !~ /^\d+$/); + my $path = "$hardware_sys_root/devices/system/cpu/cpu$id"; + my $model = hardware_cpu_model($record, \%shared, $id); + my $socket = defined($record->{'physical id'}) ? + $record->{'physical id'} : + hardware_read_value("$path/topology/physical_package_id"); + $socket = hardware_topology_id($socket); + my $core = defined($record->{'core id'}) ? $record->{'core id'} : + hardware_read_value("$path/topology/core_id"); + $core = hardware_topology_id($core); + my $online = hardware_read_value("$path/online"); + $online = 1 if (!defined($online)); + my $frequency = hardware_read_value( + "$path/cpufreq/scaling_cur_freq"); + $frequency = $frequency / 1000 + if (defined($frequency) && $frequency =~ /^\d+$/); + $frequency ||= $record->{'cpu mhz'}; + my $frequency_driver = hardware_read_value( + "$path/cpufreq/scaling_driver"); + my @details = ( + [ 'processor', $id ], + [ 'model', $model ], + [ 'vendor', $record->{'vendor_id'} || $record->{'cpu implementer'} ], + [ 'socket', $socket ], + [ 'core', $core ], + [ 'online', $online, 'yesno' ], + [ 'frequency', $frequency, 'cpu_frequency' ], + [ 'frequency_driver', $frequency_driver ], + [ 'cpu_family', $record->{'cpu family'} ], + [ 'model_id', $record->{'model'} ], + [ 'stepping', $record->{'stepping'} ], + [ 'microcode', $record->{'microcode'} ], + [ 'cache', $record->{'cache size'} ], + [ 'siblings', $record->{'siblings'} ], + [ 'cpu_cores', $record->{'cpu cores'} ], + [ 'bogomips', $record->{'bogomips'} ], + [ 'flags', $record->{'flags'} || $record->{'features'} ], + ); + push(@devices, { + 'id' => "$id", + 'name' => "CPU $id", + 'model' => $model, + 'socket' => $socket, + 'core' => $core, + 'online' => $online, + 'frequency' => $frequency, + 'driver' => $frequency_driver, + 'modules' => [ ], + 'details' => \@details, + 'properties' => { }, + }); + $index++; + } +return sort { $a->{'id'} <=> $b->{'id'} } @devices; +} + +# hardware_cpu_model(record, shared-record, id) +# Selects an architecture-neutral CPU name without treating a numeric ID as one. +sub hardware_cpu_model +{ +my ($record, $shared, $id) = @_; +foreach my $key ('model name', 'processor name', 'cpu model', 'cpu', 'uarch') { + my $value = $record->{$key}; + return $value if (defined($value) && $value ne "" && + $value !~ /^\d+$/); + } +if ($record->{'cpu implementer'} || $record->{'cpu part'} || + $record->{'cpu architecture'}) { + my $architecture = $record->{'cpu architecture'}; + my $model = defined($architecture) && $architecture =~ /^\d+$/ ? + "ARMv$architecture processor" : "ARM processor"; + my @ids; + push(@ids, "implementer $record->{'cpu implementer'}") + if ($record->{'cpu implementer'}); + push(@ids, "part $record->{'cpu part'}") if ($record->{'cpu part'}); + return $model.(@ids ? " (".join(", ", @ids).")" : ""); + } +foreach my $source ($record, $shared) { + foreach my $key ('model name', 'processor name', 'cpu model', 'cpu', + 'uarch', 'processor', 'hardware', 'machine', 'model') { + my $value = $source->{$key}; + return $value if (defined($value) && $value ne "" && + $value !~ /^\d+$/); + } + } +return "CPU $id"; +} + +# hardware_topology_id(value) +# Keeps usable kernel topology identifiers and drops negative unknown values. +sub hardware_topology_id +{ +my ($value) = @_; +return undef if (!defined($value) || $value !~ /^-?\d+$/ || $value < 0); +return $value; +} + +# hardware_kernel_module(name) +# Returns metadata for one loaded module. Kernel section addresses are omitted. +sub hardware_kernel_module +{ +my ($name) = @_; +return undef if (!defined($name) || $name !~ /^[A-Za-z0-9][A-Za-z0-9_+-]*$/); +my $path = "$hardware_sys_root/module/$name"; +return undef if (!-d $path); +my $module = { + 'name' => $name, + 'state' => hardware_read_value("$path/initstate"), + 'refcount' => hardware_read_value("$path/refcnt"), + 'taint' => hardware_read_value("$path/taint"), + 'version' => hardware_read_value("$path/version"), + 'srcversion' => hardware_read_value("$path/srcversion"), + 'coresize' => hardware_read_value("$path/coresize"), + 'initsize' => hardware_read_value("$path/initsize"), + }; +my @holders = hardware_directory_entries("$path/holders"); +$module->{'holders'} = \@holders; +my %parameters; +foreach my $parameter (hardware_directory_entries("$path/parameters", 1)) { + next if ($parameter !~ /^[A-Za-z0-9_+.-]+$/); + my $value = hardware_read_value("$path/parameters/$parameter"); + $parameters{$parameter} = $value if (defined($value)); + } +$module->{'parameters'} = \%parameters; + +# modinfo adds package metadata when kmod is installed, but is not required. +my $modinfo = defined($hardware_modinfo_command) ? + $hardware_modinfo_command : has_command("modinfo"); +if ($modinfo) { + my $out = backquote_command(quotemeta($modinfo)." ". + quotemeta($name)." 2>/dev/null", 1); + if (!$? && defined($out)) { + my %info; + foreach my $line (split(/\r?\n/, $out)) { + next if ($line !~ /^(\w[\w-]*):\s*(.*)$/); + my ($key, $value) = (lc($1), $2); + if (defined($info{$key}) && $info{$key} ne "") { + $info{$key} .= ", ".$value; + } + else { + $info{$key} = $value; + } + } + $module->{'modinfo'} = \%info; + } + } +return $module; +} + +# hardware_driver_info(path) +# Returns the bound driver and its loadable module, if either is exposed. +sub hardware_driver_info +{ +my ($path) = @_; +my @paths = ($path); +my $resolved = abs_path($path); +my $sysroot = abs_path($hardware_sys_root); +if ($resolved && $sysroot && + ($resolved eq $sysroot || $resolved =~ /^\Q$sysroot\E\//)) { + # Some devices, notably NVMe namespaces, inherit the useful driver from + # a controller ancestor instead of exposing a driver link themselves. + my $current = $resolved; + for (my $depth = 0; $depth < 8 && $current ne $sysroot; $depth++) { + push(@paths, $current) if ($current ne $path); + $current =~ s{/[^/]+$}{}; + } + } +foreach my $candidate (@paths) { + my $driver = hardware_link_name("$candidate/driver"); + next if (!$driver); + return ($driver, hardware_link_name("$candidate/driver/module")); + } +return (undef, undef); +} + +# hardware_link_name(path) +# Returns the last component of a symlink target. +sub hardware_link_name +{ +my ($path) = @_; +my $target = readlink($path); +return undef if (!defined($target)); +$target =~ s{/+$}{}; +return $1 if ($target =~ m{([^/]+)$}); +return undef; +} + +# hardware_read_uevent(file) +# Parses a sysfs uevent file into its uppercase key/value properties. +sub hardware_read_uevent +{ +my ($file) = @_; +my $data = hardware_read_file($file); +my %values; +return \%values if (!defined($data)); +foreach my $line (split(/\r?\n/, $data)) { + if ($line =~ /^([A-Z][A-Z0-9_]*)=(.*)$/) { + $values{$1} = $2; + } + } +return \%values; +} + +# hardware_read_file(file) +# Reads a fixed inventory file and returns undef when it is unavailable. +sub hardware_read_file +{ +my ($file) = @_; +return undef if (!-r $file || -d $file); +return read_file_contents($file); +} + +# hardware_read_value(file) +# Reads a small sysfs/procfs value and removes surrounding whitespace. +sub hardware_read_value +{ +my ($file) = @_; +my $value = hardware_read_file($file); +return undef if (!defined($value)); +$value =~ s/\0//g; +$value =~ s/^\s+|\s+$//g; +return $value; +} + +# hardware_directory_entries(directory, [include-files]) +# Returns safe directory entry names without following user-provided paths. +sub hardware_directory_entries +{ +my ($directory, $include_files) = @_; +return ( ) if (!-d $directory); +opendir(my $dir, $directory) || return ( ); +my @entries = grep { + $_ ne '.' && $_ ne '..' && + ($include_files || -d "$directory/$_") + } readdir($dir); +closedir($dir); +return sort @entries; +} + +# hardware_normalize_id(value) +# Converts sysfs hexadecimal IDs to lowercase values without the 0x prefix. +sub hardware_normalize_id +{ +my ($value) = @_; +return undef if (!defined($value)); +$value =~ s/^0x//i; +return lc($value) if ($value =~ /^[0-9a-f]+$/i); +return undef; +} + +# hardware_id_label(name, id) +# Combines a friendly database name with the hexadecimal hardware ID. +sub hardware_id_label +{ +my ($name, $id) = @_; +return undef if (!defined($id) || $id eq ""); +return defined($name) && $name ne "" ? "$name ($id)" : uc($id); +} + +# hardware_id_names(kind, vendor, device, [subvendor], [subdevice]) +# Looks up friendly names in the optional pci.ids or usb.ids database. +sub hardware_id_names +{ +my ($kind, $vendor, $device, $subvendor, $subdevice) = @_; +my $database = hardware_load_id_database($kind); +my %names = ( + 'vendor' => $database->{'vendors'}->{$vendor}, + 'device' => $database->{'devices'}->{"$vendor:$device"}, + ); +if ($kind eq 'pci' && $subvendor && $subdevice) { + $names{'subvendor'} = $database->{'vendors'}->{$subvendor}; + $names{'subdevice'} = $database->{'subdevices'}->{ + "$vendor:$device:$subvendor:$subdevice"} || + $database->{'devices'}->{"$subvendor:$subdevice"}; + } +return \%names; +} + +# hardware_load_id_database(kind) +# Loads only vendor, device and PCI subsystem names from a standard IDs file. +sub hardware_load_id_database +{ +my ($kind) = @_; +my $file = hardware_id_database_file($kind); +return { 'vendors' => { }, 'devices' => { }, 'subdevices' => { } } + if (!$file); +return $hardware_ids_cache{"$kind:$file"} + if ($hardware_ids_cache{"$kind:$file"}); +my $database = { 'vendors' => { }, 'devices' => { }, + 'subdevices' => { } }; +open(my $ids, '<', $file) || return $database; +my ($vendor, $device); +while (my $line = <$ids>) { + $line =~ s/\r?\n$//; + if ($kind eq 'pci' && $line =~ /^\t\t([0-9a-f]{4})\s+([0-9a-f]{4})\s+(.+)/i && + defined($vendor) && defined($device)) { + $database->{'subdevices'}->{lc("$vendor:$device:$1:$2")} = $3; + } + elsif ($line =~ /^\t([0-9a-f]{4})\s+(.+)/i && defined($vendor)) { + $device = lc($1); + $database->{'devices'}->{"$vendor:$device"} = $2; + } + elsif ($line =~ /^([0-9a-f]{4})\s+(.+)/i) { + $vendor = lc($1); + $device = undef; + $database->{'vendors'}->{$vendor} = $2; + } + elsif ($line !~ /^\t/) { + $vendor = undef; + $device = undef; + } + } +close($ids); +$hardware_ids_cache{"$kind:$file"} = $database; +return $database; +} + +# hardware_id_database_file(kind) +# Finds a distribution's standard PCI or USB ID database. +sub hardware_id_database_file +{ +my ($kind) = @_; +my $configured = $kind eq 'pci' ? $hardware_pci_ids_file : + $hardware_usb_ids_file; +return $configured if ($configured && -r $configured); +my @paths = $kind eq 'pci' ? + ('/usr/share/hwdata/pci.ids', '/usr/share/misc/pci.ids', + '/usr/share/pci.ids') : + ('/usr/share/hwdata/usb.ids', '/usr/share/misc/usb.ids', + '/var/lib/usbutils/usb.ids', '/usr/share/usb.ids'); +foreach my $file (@paths) { + return $file if (-r $file); + } +return undef; +} + +# hardware_pci_class(class-id) +# Returns a friendly name for the PCI base class. +sub hardware_pci_class +{ +my ($class_id) = @_; +my $base = defined($class_id) ? substr($class_id, 0, 2) : ""; +return $text{"pci_class_$base"} || $text{'pci_class_other'} || + 'Other PCI device'; +} + +# hardware_usb_class(class-id) +# Returns a friendly name for a USB device or interface class. +sub hardware_usb_class +{ +my ($class_id) = @_; +my $key = $class_id || ""; +return $text{"usb_class_$key"} || $text{'usb_class_other'} || + 'Other USB device'; +} + +# hardware_chassis_type_name(type-id) +# Translates useful SMBIOS chassis identifiers and omits unknown placeholders. +sub hardware_chassis_type_name +{ +my ($type) = @_; +return undef if (!defined($type) || $type eq "" || $type eq '1' || + $type eq '2'); +return $text{"chassis_type_$type"} || text('chassis_type_other', $type) + if ($type =~ /^\d+$/); +return $type; +} + +# hardware_number(value) +# Formats a measured decimal without unnecessary trailing zeroes. +sub hardware_number +{ +my ($value) = @_; +return "" if (!defined($value) || $value !~ /^\d+(?:\.\d+)?$/); +my $number = sprintf("%.2f", $value); +$number =~ s/\.?0+$//; +return $number; +} + +# hardware_unique(values) +# Returns non-empty values once, retaining their original order. +sub hardware_unique +{ +my %seen; +return grep { defined($_) && $_ ne "" && !$seen{$_}++ } @_; +} + +1; diff --git a/hardware-info/help/intro.html b/hardware-info/help/intro.html new file mode 100644 index 000000000..11df26931 --- /dev/null +++ b/hardware-info/help/intro.html @@ -0,0 +1,11 @@ +
Hardware Information
+ +This read-only module summarizes the system, mainboard, boot firmware, security hardware, usable memory and processors, and inventories PCI, USB, storage and network devices. Hardware-monitor readings are included when the kernel exposes them.

+ +Select a device to inspect the identifiers and properties reported by the Linux kernel, including its bound driver and loadable kernel module when available.

+ +Hardware data and sensor readings are read from the Linux /sys and /proc filesystems.

+ +Friendly PCI and USB names are loaded from the operating system's standard hardware ID databases when those files are installed. No additional command is required for the device inventory.

+ +


diff --git a/hardware-info/images/icon.gif b/hardware-info/images/icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..ebe8fd2397dc42b4faf738dd5e33a7bd30a78584 GIT binary patch literal 914 zcmZ?wbhEHbG+;1b_{IPN))T|O|NakQK-fSokkPh#7LZ(WW(!bCPv1b>EndexUOzn6 zye`rsq0FKw(t1KTP=U?FaKGshDf1#7r=@$&sg9mj5w;*dZfRM@s`}QI(XA_E9vn}M zDQpFrS=Z1MvSdcW{9{FXUd%qecI$)pK*85vzCL*T{P6KpH|{?;bM@YZr*|$syAKox zqHn+c9eDEo_zb{prWAx8J@!e)<0K>-SIIzkBlG-P89UpML!C z{L|MLpMJjj`WNJ-p#v0uvM_>Ns{XO_gg4%3yR}Jx@59GWp9I#t zZhgB(`17C7FIRo%cp5FbaK0nEU>K{6$pp#fcJ`mPH8ZjnE@%`th_MJYc(kX5VgFnv z!9^+@!Lqi;m?HyITDet|^L$ncEp(bLk<>Qh!~!*&dAds%HmxjP=3k-aH-%%CuBhi^ zjeaYSDy74Q)0Y|L^;At+)wbFx@}teI1{ZD_gMg9^0SA>D+E%{Lkel*hk^4j?r-m1; z2M_ATgcSYmeBo%}5x2KPJRx?g{vwt`69m2QB`UmGQ_lX{>0F?)Yp+a4`HBrT+H))m zpO`&p$Wd<1zHavA-Gssgnx<*X0v=qL{djBsh0X3a?zQb^>bdJ+^}8&V`NW%xZ>ET{ zH`VOSe{Ph*W1hyvmh|-WZ2SA?WLY2GyViJGAn%0u_h4C-?cZdjJNRz3=k2wB`nTr! z`HqtecNI6hIo_DU@#-lP_f*xO2i0;a9TNOH1_lR|W;85HROei?;c&bBz2!}e%n=!# k%IrN0ni`a^sCjgX&&%NKF* + + + + + + + + + + diff --git a/hardware-info/index.cgi b/hardware-info/index.cgi new file mode 100755 index 000000000..eafeb3356 --- /dev/null +++ b/hardware-info/index.cgi @@ -0,0 +1,320 @@ +#!/usr/local/bin/perl +# Display a read-only system hardware report and device inventory. + +use strict; +use warnings; + +require './hardware-info-lib.pl'; ## no critic + +our (%in, %text); + +ReadParse(); + +# Collect each group once so tab counts and rows describe the same snapshot. +my @all_types = hardware_device_types(); +my %devices; +foreach my $type (@all_types) { + my @list = list_hardware_devices($type); + $devices{$type} = \@list; + } +# Sensors are optional on virtual machines and systems without hwmon drivers. +my @types = grep { $_ ne 'sensor' || @{$devices{$_}} } @all_types; +my $summary = hardware_system_summary($devices{'cpu'}); + +ui_print_header(undef, $text{'index_title'}, "", "intro", 0, 1); + +# The report card and device groups are peers so the page opens with one +# focused view instead of stacking the summary above every inventory. +my @tabs = ([ 'system', $text{'type_system'} ], map { + [ $_, $text{"type_$_"}.' '. + ui_tag('sup', scalar(@{$devices{$_}})) ] + } @types); +my %valid_tabs = map { $_->[0], 1 } @tabs; +my $requested = defined($in{'type'}) ? $in{'type'} : ""; +my $active = $valid_tabs{$requested} ? $requested : 'system'; + +print ui_tabs_start(\@tabs, "type", $active, 1); +foreach my $tab ('system', @types) { + print ui_tabs_start_tab("type", $tab); + print ui_div($text{"index_${tab}_desc"}); + if ($tab eq 'system') { + print_system_summary($summary); + } + else { + print_device_table($tab, $devices{$tab}); + } + print ui_tabs_end_tab("type", $tab); + } +print ui_tabs_end(1); + +ui_print_footer("/", $text{'index'}); + +# print_system_summary(summary) +# Renders the system, board, firmware and resource report card. +sub print_system_summary +{ +my ($summary) = @_; +print ui_table_start(undef, "width=100%", 4, + [ "width=20%", "width=30%", "width=20%", "width=30%" ]); + +my $system = join(" ", grep { defined($_) && $_ ne "" } + ($summary->{'system_vendor'}, $summary->{'system_product'}, + $summary->{'system_version'})); +my $board = join(" ", grep { defined($_) && $_ ne "" } + ($summary->{'board_vendor'}, $summary->{'board_name'}, + $summary->{'board_version'})); +my $bios = join(" ", grep { defined($_) && $_ ne "" } + ($summary->{'bios_vendor'}, $summary->{'bios_version'}, + $summary->{'bios_date'})); +my $chassis_type = hardware_chassis_type_name($summary->{'chassis_type'}); +my $chassis = $chassis_type || $summary->{'chassis_version'} ? + join(" ", grep { defined($_) && $_ ne "" } + ($summary->{'chassis_vendor'}, $chassis_type, + $summary->{'chassis_version'})) : ""; +my $models = @{$summary->{'cpu_models'}} ? + join(", ", @{$summary->{'cpu_models'}}) : $text{'unknown'}; +my $processor_count = text( + $summary->{'cpu_count'} == 1 ? 'index_processor_count_one' : + 'index_processor_count_many', $summary->{'cpu_count'}); +my $package_count = $summary->{'cpu_sockets'} == 1 ? + $text{'index_package_count_one'} : $summary->{'cpu_sockets'} > 1 ? + text('index_package_count_many', $summary->{'cpu_sockets'}) : + $text{'index_package_count_unknown'}; +my $processors = text('index_processors', $processor_count, + $package_count, $models); +my $firmware_mode = $text{'firmware_'.$summary->{'firmware_mode'}}; +my $secure_boot = defined($summary->{'secure_boot'}) ? + ($summary->{'secure_boot'} ? $text{'secure_boot_enabled'} : + $text{'secure_boot_disabled'}) : $text{'unknown'}; +my $tpm = @{$summary->{'tpms'}} ? + text('index_tpm_detected', join(", ", @{$summary->{'tpms'}})) : + $text{'index_tpm_missing'}; + +# Missing optional DMI values are omitted instead of leaving empty labels. +my @rows = ( + [ $text{'index_hostname'}, $summary->{'hostname'} ], + [ $text{'index_os'}, $summary->{'os'} ], + [ $text{'index_kernel'}, $summary->{'kernel'} ], + [ $text{'index_architecture'}, $summary->{'architecture'} ], + [ $text{'index_system'}, $system ], + [ $text{'index_system_serial'}, $summary->{'system_serial'} ], + [ $text{'index_board'}, $board ], + [ $text{'index_board_serial'}, $summary->{'board_serial'} ], + [ $text{'index_bios'}, $bios ], + [ $text{'index_firmware_mode'}, $firmware_mode ], + [ $text{'index_secure_boot'}, + $summary->{'firmware_mode'} eq 'uefi' ? $secure_boot : undef ], + [ $text{'index_tpm'}, $tpm ], + [ $text{'index_memory'}, defined($summary->{'memory'}) ? + nice_size($summary->{'memory'}) : undef, 1 ], + [ $text{'index_processors_label'}, $processors ], + [ $text{'index_uuid'}, $summary->{'system_uuid'} ], + [ $text{'index_chassis'}, $chassis ], + [ $text{'index_chassis_serial'}, $summary->{'chassis_serial'} ], + ); +foreach my $row (@rows) { + my ($label, $value, $formatted) = @$row; + next if (!defined($value) || $value eq ""); + print ui_table_row($label, $formatted ? $value : hardware_html($value)); + } +print ui_table_end(); +} + +# print_device_table(type, devices) +# Dispatches to the compact table appropriate for an inventory group. +sub print_device_table +{ +my ($type, $list) = @_; +if (!@$list) { + print ui_alert($text{"index_empty_$type"}, 'info'); + return; + } +if ($type eq 'pci') { + print_pci_table($list); + } +elsif ($type eq 'usb') { + print_usb_table($list); + } +elsif ($type eq 'storage') { + print_storage_table($list); + } +elsif ($type eq 'network') { + print_network_table($list); + } +elsif ($type eq 'cpu') { + print_cpu_table($list); + } +else { + print_sensor_table($list); + } +} + +# print_pci_table(devices) +# Shows PCI address, class and kernel binding at a glance. +sub print_pci_table +{ +my ($list) = @_; +print ui_columns_start([ + $text{'index_device'}, $text{'index_class'}, $text{'index_location'}, + $text{'index_driver'}, $text{'index_module'}, + ]); +foreach my $device (@$list) { + print ui_columns_row([ + hardware_device_link('pci', $device), + hardware_html($device->{'class'}), + ui_tag('tt', hardware_html($device->{'id'})), + hardware_html($device->{'driver'}), + hardware_module_links($device->{'modules'}, $device->{'driver'}), + ]); + } +print ui_columns_end(); +} + +# print_usb_table(devices) +# Shows physical USB devices and aggregated interface driver bindings. +sub print_usb_table +{ +my ($list) = @_; +print ui_columns_start([ + $text{'index_device'}, $text{'index_location'}, $text{'index_speed'}, + $text{'index_driver'}, $text{'index_module'}, + ]); +foreach my $device (@$list) { + my $location = defined($device->{'bus_number'}) && + defined($device->{'device_number'}) ? + text('index_usb_location', $device->{'bus_number'}, + $device->{'device_number'}) : $device->{'id'}; + my $speed = $device->{'speed'} ? + text('format_mbps', $device->{'speed'}) : ""; + print ui_columns_row([ + hardware_device_link('usb', $device), + hardware_html($location), hardware_html($speed), + hardware_html($device->{'driver'}), + hardware_module_links($device->{'modules'}, $device->{'driver'}), + ]); + } +print ui_columns_end(); +} + +# print_storage_table(devices) +# Shows whole block devices, capacity, media kind and kernel binding. +sub print_storage_table +{ +my ($list) = @_; +print ui_columns_start([ + $text{'index_device'}, $text{'index_model'}, $text{'index_capacity'}, + $text{'index_type'}, $text{'index_driver'}, $text{'index_module'}, + ]); +foreach my $device (@$list) { + print ui_columns_row([ + hardware_device_link('storage', $device, $device->{'device'}), + hardware_html($device->{'model'}), + defined($device->{'size'}) ? nice_size($device->{'size'}) : "", + hardware_html($text{'storage_'.$device->{'kind'}}), + hardware_html($device->{'driver'}), + hardware_module_links($device->{'modules'}, $device->{'driver'}), + ]); + } +print ui_columns_end(); +} + +# print_sensor_table(devices) +# Shows only detected hwmon readings, keeping the optional tab compact. +sub print_sensor_table +{ +my ($list) = @_; +print ui_columns_start([ + $text{'index_sensor'}, $text{'index_chip'}, $text{'index_type'}, + $text{'index_reading'}, $text{'index_driver'}, $text{'index_module'}, + ]); +foreach my $device (@$list) { + print ui_columns_row([ + hardware_device_link('sensor', $device), + hardware_html($device->{'chip'}), + hardware_html($text{'sensor_'.$device->{'kind'}}), + hardware_html($device->{'reading'}), + hardware_html($device->{'driver'}), + hardware_module_links($device->{'modules'}, $device->{'driver'}), + ]); + } +print ui_columns_end(); +} + +# print_network_table(devices) +# Shows all network interfaces, including virtual interfaces, and link state. +sub print_network_table +{ +my ($list) = @_; +print ui_columns_start([ + $text{'index_interface'}, $text{'index_type'}, $text{'index_address'}, + $text{'index_state'}, $text{'index_speed'}, $text{'index_driver'}, + $text{'index_module'}, + ]); +foreach my $device (@$list) { + my $speed = $device->{'speed'} ? + text('format_mbps', $device->{'speed'}) : ""; + print ui_columns_row([ + hardware_device_link('network', $device), + hardware_html($text{'network_'.$device->{'kind'}}), + ui_tag('tt', hardware_html($device->{'address'})), + hardware_html($device->{'state'}), hardware_html($speed), + hardware_html($device->{'driver'}), + hardware_module_links($device->{'modules'}, $device->{'driver'}), + ]); + } +print ui_columns_end(); +} + +# print_cpu_table(devices) +# Shows logical processor topology and current operating state. +sub print_cpu_table +{ +my ($list) = @_; +print ui_columns_start([ + $text{'index_processor'}, $text{'index_model'}, $text{'index_socket'}, + $text{'index_core'}, $text{'index_state'}, $text{'index_frequency'}, + ]); +foreach my $device (@$list) { + my $frequency = $device->{'frequency'} ? + text('format_mhz', hardware_number($device->{'frequency'})) : ""; + print ui_columns_row([ + hardware_device_link('cpu', $device), + hardware_html($device->{'model'}), + hardware_html($device->{'socket'}), hardware_html($device->{'core'}), + $device->{'online'} ? $text{'online'} : $text{'offline'}, + hardware_html($frequency), + ]); + } +print ui_columns_end(); +} + +# hardware_device_link(type, device) +# Returns an escaped link to the exact inventory item. +sub hardware_device_link +{ +my ($type, $device, $label) = @_; +my $url = "view.cgi?type=".urlize($type)."&id=".urlize($device->{'id'}); +$label = $device->{'name'} if (!defined($label)); +return ui_link($url, hardware_html($label)); +} + +# hardware_module_links(modules, [driver]) +# Returns links to details for loaded modules associated with a device. +sub hardware_module_links +{ +my ($modules, $driver) = @_; +return $text{'module_builtin'} if ((!$modules || !@$modules) && $driver); +return "" if (!$modules || !@$modules); +return join(", ", map { + ui_link("module.cgi?name=".urlize($_), ui_tag('tt', hardware_html($_))) + } @$modules); +} + +# hardware_html(value) +# Escapes a possibly missing hardware value for safe table output. +sub hardware_html +{ +my ($value) = @_; +return "" if (!defined($value)); +return html_escape($value); +} diff --git a/hardware-info/install_check.pl b/hardware-info/install_check.pl new file mode 100644 index 000000000..5a7e50e92 --- /dev/null +++ b/hardware-info/install_check.pl @@ -0,0 +1,19 @@ +#!/usr/local/bin/perl +# Decide whether the Linux sysfs hardware inventory is available. + +use strict; +use warnings; +use lib ".."; + +use WebminCore; + +# is_installed(mode) +# Returns Webmin's install-check code when the sysfs device tree is present. +sub is_installed +{ +return 0 if (!-d "/sys" || !-d "/sys/devices"); +return $_[0] ? 2 : 1; +} + +1; + diff --git a/hardware-info/lang/en b/hardware-info/lang/en new file mode 100644 index 000000000..6748da0ea --- /dev/null +++ b/hardware-info/lang/en @@ -0,0 +1,288 @@ +index_title=Hardware Information +index_system_desc=Review host, operating system, kernel, platform, boot firmware, security hardware, usable memory, processor and chassis information. +index_pci_desc=Review PCI devices, their bus addresses and classes, and the kernel drivers and modules associated with them. +index_usb_desc=Review USB devices, their bus locations and speeds, and the kernel drivers and modules associated with them. +index_storage_desc=Review storage devices, their capacity and media type, and the kernel drivers and modules associated with them. +index_network_desc=Review physical and virtual network interfaces, their link state and speed, and the kernel drivers and modules associated with them. +index_cpu_desc=Review logical processors, including their model, topology, online state and current frequency. +index_sensor_desc=Review temperature, fan, voltage, current and power readings exposed by Linux hardware-monitor drivers. +index_hostname=Hostname +index_os=Operating system +index_kernel=Kernel +index_architecture=Architecture +index_system=System +index_system_serial=System serial number +index_board=Mainboard +index_board_serial=Mainboard serial number +index_bios=BIOS or firmware +index_firmware_mode=Firmware interface +index_secure_boot=Secure Boot +index_tpm=Trusted Platform Module +index_tpm_detected=Detected ($1) +index_tpm_missing=Not detected +index_memory=Usable memory +index_processors_label=Processors +index_processors=$1 in $2 — $3 +index_processor_count_one=1 logical processor +index_processor_count_many=$1 logical processors +index_package_count_one=1 physical package +index_package_count_many=$1 physical packages +index_package_count_unknown=unknown physical package topology +index_uuid=System UUID +index_chassis=Chassis +index_chassis_serial=Chassis serial number +index_device=Device +index_interface=Interface +index_processor=Processor +index_class=Class +index_location=Location +index_driver=Driver +index_module=Kernel module +index_speed=Speed +index_model=Model +index_capacity=Capacity +index_type=Type +index_address=Address +index_state=State +index_socket=Package ID +index_core=Core +index_frequency=Frequency +index_sensor=Sensor +index_chip=Monitoring device +index_reading=Reading +index_usb_location=Bus $1, device $2 +index_empty_pci=No PCI devices were reported by the Linux kernel. +index_empty_usb=No USB devices were reported by the Linux kernel. +index_empty_storage=No whole block devices were reported by the Linux kernel. +index_empty_network=No network interfaces were reported by the Linux kernel. +index_empty_cpu=No processors were reported by the Linux kernel. +index_empty_sensor=No hardware-monitor readings were reported by the Linux kernel. + +type_system=System Report +type_pci=PCI Devices +type_usb=USB Devices +type_storage=Storage Devices +type_network=Network Interfaces +type_cpu=Processors +type_sensor=Sensors + +firmware_uefi=UEFI +firmware_bios=Legacy BIOS +firmware_other=Non-UEFI +secure_boot_enabled=Enabled +secure_boot_disabled=Disabled + +storage_virtual=Virtual device +storage_optical=Optical drive +storage_removable=Removable storage +storage_ssd=Solid-state drive +storage_disk=Hard disk + +network_wireless=Wireless +network_loopback=Loopback +network_virtual=Virtual +network_ethernet=Ethernet +network_other=Other + +view_title=Hardware device $1 +view_error=Failed to display hardware device +view_etype=Invalid hardware device type +view_enodevice=The requested hardware device no longer exists or is not available. +view_details=Device details +view_driver=Kernel driver and properties +view_interface=USB interface +view_interface_name=$1 — $2 +view_interface_class=Class: $1 +view_interface_driver=Driver: $1 +view_interface_module=Kernel module: $1 +view_return=hardware information + +detail_address=Address +detail_class=Class +detail_class_id=Class ID +detail_vendor=Vendor +detail_device=Device +detail_subsystem_vendor=Subsystem vendor +detail_subsystem_device=Subsystem device +detail_revision=Revision +detail_irq=IRQ +detail_numa_node=NUMA node +detail_iommu_group=IOMMU group +detail_local_cpus=Local processors +detail_enabled=Enabled +detail_location=Kernel location +detail_bus_number=Bus number +detail_device_number=Device number +detail_serial=Serial number +detail_usb_version=USB version +detail_speed=Speed +detail_protocol=Protocol +detail_removable=Removable +detail_authorized=Authorized +detail_device_name=Device file +detail_type=Type +detail_capacity=Capacity +detail_model=Model +detail_firmware_revision=Firmware revision +detail_wwid=WWID +detail_transport=Transport +detail_bus_device=Parent bus device +detail_state=State +detail_logical_block_size=Logical block size +detail_physical_block_size=Physical block size +detail_rotational=Rotational media +detail_read_only=Read-only +detail_discard=Discard/TRIM support +detail_scheduler=I/O scheduler +detail_interface=Interface +detail_carrier=Carrier detected +detail_duplex=Duplex +detail_mtu=MTU +detail_rx_bytes=Received data +detail_tx_bytes=Transmitted data +detail_rx_packets=Received packets +detail_tx_packets=Transmitted packets +detail_rx_errors=Receive errors +detail_tx_errors=Transmit errors +detail_rx_dropped=Dropped receive packets +detail_tx_dropped=Dropped transmit packets +detail_processor=Logical processor +detail_socket=Package ID +detail_core=Physical core +detail_online=Online +detail_frequency=Current frequency +detail_frequency_driver=Frequency scaling driver +detail_cpu_family=CPU family +detail_model_id=Model ID +detail_stepping=Stepping +detail_microcode=Microcode +detail_cache=Cache +detail_siblings=Threads in package +detail_cpu_cores=Cores in package +detail_bogomips=BogoMIPS +detail_flags=Features and flags +detail_sensor_chip=Monitoring device +detail_sensor_type=Reading type +detail_sensor_reading=Current reading +detail_driver=Driver +detail_module=Loadable kernel module +detail_modalias=Module alias + +module_title=Kernel module $1 +module_error=Failed to display kernel module +module_enotfound=The requested kernel module is not loaded or is not available. +module_details=Loaded module details +module_state=State +module_refcount=Reference count +module_version=Version +module_srcversion=Source version +module_taint=Taint flags +module_coresize=Core size +module_initsize=Initialization size +module_holders=Used by modules +module_additional=Module metadata and parameters +module_parameter=Parameter $1 +module_builtin=Built into kernel + +format_mbps=$1 Mb/s +format_mhz=$1 MHz +format_celsius=$1 °C +format_rpm=$1 RPM +format_volts=$1 V +format_amps=$1 A +format_watts=$1 W +sensor_numbered=$1 $2 +sensor_temp=Temperature +sensor_fan=Fan +sensor_in=Voltage +sensor_curr=Current +sensor_power=Power +online=Online +offline=Offline +unknown=Unknown + +chassis_type_3=Desktop +chassis_type_4=Low-profile desktop +chassis_type_5=Pizza box +chassis_type_6=Mini tower +chassis_type_7=Tower +chassis_type_8=Portable +chassis_type_9=Laptop +chassis_type_10=Notebook +chassis_type_11=Hand-held +chassis_type_12=Docking station +chassis_type_13=All-in-one +chassis_type_14=Sub-notebook +chassis_type_15=Space-saving +chassis_type_16=Lunch box +chassis_type_17=Main server chassis +chassis_type_18=Expansion chassis +chassis_type_19=Sub-chassis +chassis_type_20=Bus expansion chassis +chassis_type_21=Peripheral chassis +chassis_type_22=RAID chassis +chassis_type_23=Rack-mount chassis +chassis_type_24=Sealed-case PC +chassis_type_25=Multi-system chassis +chassis_type_26=CompactPCI +chassis_type_27=AdvancedTCA +chassis_type_28=Blade +chassis_type_29=Blade enclosure +chassis_type_30=Tablet +chassis_type_31=Convertible +chassis_type_32=Detachable +chassis_type_33=IoT gateway +chassis_type_34=Embedded PC +chassis_type_35=Mini PC +chassis_type_36=Stick PC +chassis_type_other=Type $1 + +pci_class_00=Unclassified device +pci_class_01=Mass storage controller +pci_class_02=Network controller +pci_class_03=Display controller +pci_class_04=Multimedia controller +pci_class_05=Memory controller +pci_class_06=Bridge +pci_class_07=Communication controller +pci_class_08=System peripheral +pci_class_09=Input device controller +pci_class_0a=Docking station +pci_class_0b=Processor +pci_class_0c=Serial bus controller +pci_class_0d=Wireless controller +pci_class_0e=Intelligent controller +pci_class_0f=Satellite communication controller +pci_class_10=Encryption controller +pci_class_11=Signal processing controller +pci_class_12=Processing accelerator +pci_class_13=Instrumentation device +pci_class_40=Coprocessor +pci_class_ff=Unassigned device +pci_class_other=Other PCI device + +usb_class_00=Defined by interface +usb_class_01=Audio +usb_class_02=Communications +usb_class_03=Human interface device +usb_class_05=Physical +usb_class_06=Imaging +usb_class_07=Printer +usb_class_08=Mass storage +usb_class_09=Hub +usb_class_0a=CDC data +usb_class_0b=Smart card +usb_class_0d=Content security +usb_class_0e=Video +usb_class_0f=Personal healthcare +usb_class_10=Audio/video +usb_class_11=Billboard +usb_class_12=USB Type-C bridge +usb_class_dc=Diagnostic +usb_class_e0=Wireless controller +usb_class_ef=Miscellaneous +usb_class_fe=Application-specific +usb_class_ff=Vendor-specific +usb_class_other=Other USB device + +__norefs=1 diff --git a/hardware-info/module.cgi b/hardware-info/module.cgi new file mode 100755 index 000000000..4e5ae2df5 --- /dev/null +++ b/hardware-info/module.cgi @@ -0,0 +1,57 @@ +#!/usr/local/bin/perl +# Display details about one loaded kernel module associated with a device. + +use strict; +use warnings; + +require './hardware-info-lib.pl'; ## no critic + +our (%in, %text); + +ReadParse(); +error_setup($text{'module_error'}); + +my $name = defined($in{'name'}) ? $in{'name'} : ""; +my $module = hardware_kernel_module($name); +error($text{'module_enotfound'}) if (!$module); + +ui_print_header(undef, text('module_title', html_escape($module->{'name'})), + "", undef, 0, 1); + +print ui_table_start($text{'module_details'}, "width=100%", 2, + [ "width=30%", undef ]); +my @fields = qw(state refcount version srcversion taint); +foreach my $field (@fields) { + next if (!defined($module->{$field}) || $module->{$field} eq ""); + print ui_table_row($text{"module_$field"}, + html_escape($module->{$field})); + } +print ui_table_row($text{'module_coresize'}, nice_size($module->{'coresize'})) + if (defined($module->{'coresize'}) && $module->{'coresize'} =~ /^\d+$/); +print ui_table_row($text{'module_initsize'}, nice_size($module->{'initsize'})) + if (defined($module->{'initsize'}) && $module->{'initsize'} =~ /^\d+$/); +print ui_table_row($text{'module_holders'}, + join(", ", map { ui_tag('tt', html_escape($_)) } + @{$module->{'holders'}})) if (@{$module->{'holders'}}); +print ui_table_end(); + +# Keep all supplementary metadata and parameters in one expanded panel. +my $has_modinfo = $module->{'modinfo'} && keys(%{$module->{'modinfo'}}); +my $has_parameters = keys(%{$module->{'parameters'}}); +if ($has_modinfo || $has_parameters) { + print ui_hidden_table_start($text{'module_additional'}, "width=100%", 2, + "module_additional", 1, [ "width=30%", undef ]); + foreach my $key (sort keys(%{$module->{'modinfo'} || { }})) { + print ui_table_row(ui_tag('tt', html_escape($key)), + html_escape($module->{'modinfo'}->{$key})); + } + foreach my $key (sort keys(%{$module->{'parameters'}})) { + my $label = text('module_parameter', + ui_tag('tt', html_escape($key))); + print ui_table_row($label, + ui_tag('tt', html_escape($module->{'parameters'}->{$key}))); + } + print ui_hidden_table_end(); + } + +ui_print_footer("index.cgi", $text{'view_return'}); diff --git a/hardware-info/module.info b/hardware-info/module.info new file mode 100644 index 000000000..91960fdd9 --- /dev/null +++ b/hardware-info/module.info @@ -0,0 +1,6 @@ +name=Hardware Information +category=hardware +os_support=*-linux +desc=Hardware Information +longdesc=View system hardware, firmware and security details, and inspect PCI, USB, storage, network and processor devices, hardware-monitor readings and their kernel drivers. +readonly=1 diff --git a/hardware-info/t/perlcritic.t b/hardware-info/t/perlcritic.t new file mode 100644 index 000000000..48d8e8b30 --- /dev/null +++ b/hardware-info/t/perlcritic.t @@ -0,0 +1,60 @@ +#!/usr/bin/perl +use strict; +use warnings; +use Test::More; + +BEGIN { +eval { require Perl::Critic; 1 } + or plan skip_all => 'Perl::Critic not installed'; +} + +use File::Find; + +# script_dir() +# Returns the directory containing this test file. +sub script_dir +{ +my $path = $0; +if ($path =~ m{^/}) { + $path =~ s{/[^/]+$}{}; + return $path; + } +my $cwd = `pwd`; +chomp($cwd); +if ($path =~ m{/}) { + $path =~ s{/[^/]+$}{}; + return $cwd.'/'.$path; + } +return $cwd; +} + +my $bindir = script_dir(); +my $module_dir = "$bindir/.."; +my $profile = "$bindir/../../.perlcriticrc"; +if (!-r $profile) { +plan skip_all => 'Perl::Critic profile not installed'; +} +chdir($module_dir) or die "chdir: $!"; + +my @files; +find( + sub { + return if -d; + return if -l; + return unless /\.(pl|cgi)\z/; + return if /\.info\.pl\z/; + push(@files, $File::Find::name); + }, + '.' +); +@files = sort @files; +plan skip_all => 'no Perl files to check' if (!@files); + +my $critic = Perl::Critic->new(-profile => $profile); +foreach my $file (@files) { + my @violations = $critic->critique($file); + is(scalar(@violations), 0, "$file perlcritic"); + diag join("", @violations) if (@violations); + } + +done_testing(); diff --git a/hardware-info/t/run-tests.t b/hardware-info/t/run-tests.t new file mode 100644 index 000000000..a4aaab7b7 --- /dev/null +++ b/hardware-info/t/run-tests.t @@ -0,0 +1,421 @@ +#!/usr/bin/perl +use strict; +use warnings; +no warnings 'redefine'; +no warnings 'once'; +use Test::More; +use Cwd qw(abs_path); +use File::Path qw(make_path); +use File::Temp qw(tempdir); + +# script_dir() +# Returns the directory containing this test file. +sub script_dir +{ +my $path = $0; +if ($path =~ m{^/}) { + $path =~ s{/[^/]+$}{}; + return $path; + } +my $cwd = `pwd`; +chomp($cwd); +if ($path =~ m{/}) { + $path =~ s{/[^/]+$}{}; + return $cwd.'/'.$path; + } +return $cwd; +} + +# write_test_file(file, data) +# Creates a fixture file and any missing parent directories. +sub write_test_file +{ +my ($file, $data) = @_; +my $directory = $file; +$directory =~ s{/[^/]+$}{}; +make_path($directory) if (!-d $directory); +open(my $fh, '>', $file) or die "$file: $!"; +print $fh $data; +close($fh); +} + +# slurp_test_file(file) +# Reads a source file for structural UI assertions. +sub slurp_test_file +{ +my ($file) = @_; +open(my $fh, '<', $file) or die "$file: $!"; +local $/; +my $data = <$fh>; +close($fh); +return $data; +} + +my $bindir = script_dir(); +my $rootdir = abs_path("$bindir/../..") or die "rootdir: $!"; +my $confdir = tempdir(CLEANUP => 1); +my $vardir = tempdir(CLEANUP => 1); +write_test_file("$confdir/config", + "os_type=generic-linux\nos_version=0\n". + "real_os_type=Test Linux\nreal_os_version=1\n"); +write_test_file("$confdir/var-path", "$vardir\n"); +$ENV{'WEBMIN_CONFIG'} = $confdir; +$ENV{'WEBMIN_VAR'} = $vardir; +$ENV{'FOREIGN_MODULE_NAME'} = 'hardware-info'; +$ENV{'FOREIGN_ROOT_DIRECTORY'} = $rootdir; + +chdir("$bindir/..") or die "chdir: $!"; +require './hardware-info-lib.pl'; ## no critic + +our ($hardware_sys_root, $hardware_proc_root, $hardware_pci_ids_file, + $hardware_usb_ids_file, $hardware_modinfo_command); +our %hardware_ids_cache; + +my $fixture = tempdir(CLEANUP => 1); +$hardware_sys_root = "$fixture/sys"; +$hardware_proc_root = "$fixture/proc"; +$hardware_pci_ids_file = "$fixture/pci.ids"; +$hardware_usb_ids_file = "$fixture/usb.ids"; +$hardware_modinfo_command = ''; +%hardware_ids_cache = ( ); + +# Create friendly-name databases used by the PCI and USB inventory parsers. +write_test_file($hardware_pci_ids_file, + "8086 Intel Corporation\n". + "\t15f3 Ethernet Controller\n". + "\t\t1a2b 3c4d Server Ethernet Adapter\n". + "1a2b Example Systems\n". + "\t3c4d Server Ethernet Adapter\n"); +write_test_file($hardware_usb_ids_file, + "046d Example Peripherals\n". + "\tc534 USB Receiver\n"); + +# Build a PCI function with an e1000e driver/module binding. +my $pci = "$hardware_sys_root/bus/pci/devices/0000:00:1f.6"; +my $pci_driver = "$hardware_sys_root/bus/pci/drivers/e1000e"; +my $module = "$hardware_sys_root/module/e1000e"; +my $iommu_group = "$hardware_sys_root/kernel/iommu_groups/12"; +make_path($pci, $pci_driver, $module, $iommu_group); +write_test_file("$pci/vendor", "0x8086\n"); +write_test_file("$pci/device", "0x15f3\n"); +write_test_file("$pci/subsystem_vendor", "0x1a2b\n"); +write_test_file("$pci/subsystem_device", "0x3c4d\n"); +write_test_file("$pci/class", "0x020000\n"); +write_test_file("$pci/revision", "0x03\n"); +write_test_file("$pci/irq", "16\n"); +write_test_file("$pci/uevent", + "DRIVER=e1000e\nPCI_SLOT_NAME=0000:00:1f.6\n". + "MODALIAS=pci:test\n"); +symlink($pci_driver, "$pci/driver") or die "pci driver symlink: $!"; +symlink($module, "$pci_driver/module") or die "pci module symlink: $!"; +symlink($iommu_group, "$pci/iommu_group") or die "iommu symlink: $!"; + +# Build a USB parent whose HID driver is bound to its interface. +my $usb = "$hardware_sys_root/bus/usb/devices/1-2"; +my $usb_interface = "$hardware_sys_root/bus/usb/devices/1-2:1.0"; +my $usb_driver = "$hardware_sys_root/bus/usb/drivers/usbhid"; +my $usb_module = "$hardware_sys_root/module/usbhid"; +make_path($usb, $usb_interface, $usb_driver, $usb_module); +write_test_file("$usb/idVendor", "046d\n"); +write_test_file("$usb/idProduct", "c534\n"); +write_test_file("$usb/manufacturer", "Example Peripherals\n"); +write_test_file("$usb/product", "USB Receiver\n"); +write_test_file("$usb/busnum", "1\n"); +write_test_file("$usb/devnum", "4\n"); +write_test_file("$usb/speed", "480\n"); +write_test_file("$usb/bDeviceClass", "00\n"); +write_test_file("$usb/uevent", "PRODUCT=46d/c534/2900\n"); +write_test_file("$usb_interface/bInterfaceClass", "03\n"); +write_test_file("$usb_interface/interface", "Keyboard\n"); +symlink($usb_driver, "$usb_interface/driver") or die "usb driver symlink: $!"; +symlink($usb_module, "$usb_driver/module") or die "usb module symlink: $!"; + +# Build one solid-state block device backed by the sd_mod module. +my $disk = "$hardware_sys_root/class/block/sda"; +my $disk_driver = "$hardware_sys_root/bus/scsi/drivers/sd"; +my $disk_module = "$hardware_sys_root/module/sd_mod"; +make_path("$disk/device", "$disk/queue", $disk_driver, $disk_module); +write_test_file("$disk/size", "2097152\n"); +write_test_file("$disk/removable", "0\n"); +write_test_file("$disk/ro", "0\n"); +write_test_file("$disk/device/vendor", "ATA\n"); +write_test_file("$disk/device/model", "Fixture SSD\n"); +write_test_file("$disk/device/serial", "DISK123\n"); +write_test_file("$disk/device/firmware_rev", "FW1.2\n"); +write_test_file("$disk/wwid", "naa.5000123456789abc\n"); +write_test_file("$disk/queue/rotational", "0\n"); +write_test_file("$disk/queue/logical_block_size", "512\n"); +write_test_file("$disk/queue/physical_block_size", "4096\n"); +write_test_file("$disk/queue/discard_max_bytes", "1048576\n"); +write_test_file("$disk/uevent", "DEVNAME=sda\nDEVTYPE=disk\n"); +symlink($disk_driver, "$disk/device/driver") or die "disk driver symlink: $!"; +symlink($disk_module, "$disk_driver/module") or die "disk module symlink: $!"; +symlink("$hardware_sys_root/bus/scsi", "$disk/device/subsystem") or + die "disk subsystem symlink: $!"; + +# Build a physical Ethernet interface and its current counters. +my $net = "$hardware_sys_root/class/net/eth0"; +my $net_driver = "$hardware_sys_root/bus/pci/drivers/igb"; +my $net_module = "$hardware_sys_root/module/igb"; +make_path("$net/device", "$net/statistics", $net_driver, $net_module); +write_test_file("$net/type", "1\n"); +write_test_file("$net/address", "00:11:22:33:44:55\n"); +write_test_file("$net/operstate", "up\n"); +write_test_file("$net/speed", "1000\n"); +write_test_file("$net/mtu", "1500\n"); +write_test_file("$net/statistics/rx_bytes", "4096\n"); +write_test_file("$net/statistics/tx_bytes", "2048\n"); +write_test_file("$net/uevent", "INTERFACE=eth0\n"); +symlink($net_driver, "$net/device/driver") or die "net driver symlink: $!"; +symlink($net_module, "$net_driver/module") or die "net module symlink: $!"; + +# Build standard hwmon temperature and fan readings. +my $hwmon = "$hardware_sys_root/class/hwmon/hwmon0"; +make_path($hwmon); +write_test_file("$hwmon/name", "fixture_hwmon\n"); +write_test_file("$hwmon/temp1_label", "CPU package\n"); +write_test_file("$hwmon/temp1_input", "42500\n"); +write_test_file("$hwmon/fan1_label", "System fan\n"); +write_test_file("$hwmon/fan1_input", "1800\n"); + +# Build two logical processors and system-level memory/DMI fields. +write_test_file("$hardware_proc_root/cpuinfo", <<'EOF'); +processor : 0 +vendor_id : GenuineFixture +model name : Fixture CPU 3.00GHz +physical id : 0 +core id : 0 +cpu MHz : 3000.000 +flags : fpu sse + +processor : 1 +vendor_id : GenuineFixture +model name : Fixture CPU 3.00GHz +physical id : 0 +core id : 1 +cpu MHz : 2800.000 +flags : fpu sse +EOF +write_test_file("$hardware_proc_root/meminfo", "MemTotal: 8388608 kB\n"); +write_test_file("$hardware_sys_root/class/dmi/id/sys_vendor", "Fixture Inc.\n"); +write_test_file("$hardware_sys_root/class/dmi/id/product_name", "Test Server\n"); +write_test_file("$hardware_sys_root/class/dmi/id/product_serial", "SYS123\n"); +write_test_file("$hardware_sys_root/class/dmi/id/chassis_type", "23\n"); +my $secure_boot_var = "$hardware_sys_root/firmware/efi/efivars/". + "SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"; +write_test_file($secure_boot_var, pack("V C", 7, 1)); +make_path("$hardware_sys_root/class/tpm/tpm0"); + +# Populate metadata and parameters for the linked e1000e module page. +write_test_file("$module/initstate", "live\n"); +write_test_file("$module/refcnt", "1\n"); +write_test_file("$module/version", "1.2.3\n"); +write_test_file("$module/coresize", "65536\n"); +write_test_file("$module/parameters/InterruptThrottleRate", "3\n"); +make_path("$module/holders"); + +my @pci_devices = list_pci_devices(); +is(scalar(@pci_devices), 1, 'one PCI function is enumerated'); +is($pci_devices[0]->{'name'}, 'Intel Corporation Ethernet Controller', + 'PCI database names are applied'); +is($pci_devices[0]->{'class'}, 'Network controller', + 'PCI base class is translated'); +is($pci_devices[0]->{'driver'}, 'e1000e', 'PCI driver is resolved'); +is($pci_devices[0]->{'module'}, 'e1000e', 'PCI kernel module is resolved'); +my %pci_details = map { $_->[0], $_->[1] } @{$pci_devices[0]->{'details'}}; +is($pci_details{'iommu_group'}, '12', 'PCI IOMMU group is retained'); + +my @usb_devices = list_usb_devices(); +is(scalar(@usb_devices), 1, 'USB interfaces are not duplicated as devices'); +is($usb_devices[0]->{'driver'}, 'usbhid', + 'USB interface driver is aggregated into parent'); +is_deeply($usb_devices[0]->{'modules'}, [ 'usbhid' ], + 'USB interface module is aggregated into parent'); +is($usb_devices[0]->{'interfaces'}->[0]->{'class'}, + 'Human interface device', 'USB interface class is translated'); + +my @storage = list_storage_devices(); +is(scalar(@storage), 1, 'one whole block device is enumerated'); +is($storage[0]->{'size'}, 1024 * 1024 * 1024, + 'block sectors are converted to bytes'); +is($storage[0]->{'kind'}, 'ssd', 'non-rotational disk is an SSD'); +is($storage[0]->{'module'}, 'sd_mod', 'storage module is resolved'); +is($storage[0]->{'device'}, '/dev/sda', 'storage device file is retained'); +is($storage[0]->{'model'}, 'ATA Fixture SSD', + 'storage model remains separate from its device file'); +my %storage_details = map { $_->[0], $_->[1] } + @{$storage[0]->{'details'}}; +is($storage_details{'firmware_revision'}, 'FW1.2', + 'storage firmware revision is retained'); +is($storage_details{'wwid'}, 'naa.5000123456789abc', + 'storage WWID is retained'); +is($storage_details{'transport'}, 'scsi', + 'storage transport is resolved'); +is($storage_details{'discard'}, 1, 'storage discard support is detected'); + +my @network = list_network_devices(); +is(scalar(@network), 1, 'one network interface is enumerated'); +is($network[0]->{'kind'}, 'ethernet', 'physical ARPHRD interface is Ethernet'); +is($network[0]->{'speed'}, '1000', 'network link speed is read'); +is($network[0]->{'module'}, 'igb', 'network module is resolved'); + +my @sensors = list_sensor_devices(); +is(scalar(@sensors), 2, 'standard hwmon readings are enumerated'); +is($sensors[0]->{'reading'}, '1800 RPM', 'fan reading keeps its base unit'); +is($sensors[1]->{'reading'}, '42.5 °C', + 'temperature reading is converted from millidegrees'); +is(hardware_sensor_reading('in', '11895'), '11.9 V', + 'voltage reading is converted from millivolts'); +is(hardware_sensor_reading('curr', '1500'), '1.5 A', + 'current reading is converted from milliamps'); +is(hardware_sensor_reading('power', '9706000'), '9.71 W', + 'power reading is converted from microwatts'); +ok(!defined(hardware_sensor_reading('temp', 'unavailable')), + 'invalid hwmon readings are omitted'); + +my @cpus = list_cpu_devices(); +is(scalar(@cpus), 2, 'logical processors are enumerated'); +is($cpus[1]->{'core'}, '1', 'processor topology is retained'); +is($cpus[0]->{'model'}, 'Fixture CPU 3.00GHz', 'processor model is retained'); + +my $summary = hardware_system_summary(\@cpus); +is($summary->{'system_vendor'}, 'Fixture Inc.', 'DMI vendor is summarized'); +is($summary->{'system_product'}, 'Test Server', 'DMI product is summarized'); +is($summary->{'memory'}, 8 * 1024 * 1024 * 1024, + 'usable memory is converted to bytes'); +is($summary->{'cpu_count'}, 2, 'logical processor count is summarized'); +is($summary->{'cpu_sockets'}, 1, 'physical package count is summarized'); +is($summary->{'firmware_mode'}, 'uefi', 'UEFI firmware is detected'); +is($summary->{'secure_boot'}, 1, 'UEFI Secure Boot state is read'); +is_deeply($summary->{'tpms'}, [ 'tpm0' ], 'TPM presence is reported'); +is(hardware_chassis_type_name($summary->{'chassis_type'}), + 'Rack-mount chassis', 'SMBIOS chassis type is translated'); +ok(!defined(hardware_chassis_type_name('2')), + 'unknown SMBIOS chassis placeholder is omitted'); + +# ARM cpuinfo commonly exposes IDs without a friendly model on every record. +{ + local $hardware_proc_root = "$fixture/arm-proc"; + local $hardware_sys_root = "$fixture/arm-sys"; + my $arm_cpuinfo = ""; + foreach my $id (0 .. 3) { + $arm_cpuinfo .= "processor : $id\n". + "BogoMIPS : 60.00\n". + "CPU implementer : 0x41\n". + "CPU architecture : 8\n". + "CPU part : 0xd0c\n\n"; + write_test_file("$hardware_sys_root/devices/system/cpu/cpu$id/". + "topology/physical_package_id", "60\n"); + write_test_file("$hardware_sys_root/devices/system/cpu/cpu$id/". + "topology/core_id", "$id\n"); + } + write_test_file("$hardware_proc_root/cpuinfo", $arm_cpuinfo); + my @arm_cpus = list_cpu_devices(); + is(scalar(@arm_cpus), 4, 'ARM logical processors are enumerated'); + is($arm_cpus[0]->{'model'}, + 'ARMv8 processor (implementer 0x41, part 0xd0c)', + 'ARM CPU identity does not fall back to its numeric processor ID'); + is($arm_cpus[3]->{'model'}, $arm_cpus[0]->{'model'}, + 'ARM processor model is consistent across logical CPUs'); + is($arm_cpus[0]->{'socket'}, '60', + 'kernel package identifiers are retained without implying a socket number'); + my $arm_summary = hardware_system_summary(\@arm_cpus); + is_deeply($arm_summary->{'cpu_models'}, [ $arm_cpus[0]->{'model'} ], + 'ARM summary contains one accurate processor model'); + is($arm_summary->{'cpu_sockets'}, 1, + 'ARM package count uses unique kernel package identifiers'); +} + +my $module_info = hardware_kernel_module('e1000e'); +is($module_info->{'state'}, 'live', 'loaded module state is read'); +is($module_info->{'coresize'}, '65536', 'loaded module size is read'); +is($module_info->{'parameters'}->{'InterruptThrottleRate'}, '3', + 'loaded module parameters are read'); + +# Read-only Webmin users may run modinfo because it only reports metadata. +my ($modinfo_safe, $safe_module_info); +{ + no warnings 'redefine'; + local *backquote_command = sub { + $modinfo_safe = $_[1]; + $? = 0; + return "description: Fixture driver\n"; + }; + local $hardware_modinfo_command = '/usr/sbin/modinfo'; + $safe_module_info = hardware_kernel_module('e1000e'); +} +is($modinfo_safe, 1, 'modinfo lookup is allowed in Webmin read-only mode'); +is($safe_module_info->{'modinfo'}->{'description'}, 'Fixture driver', + 'modinfo metadata is retained for read-only users'); + +ok(get_hardware_device('pci', '0000:00:1f.6'), + 'exact enumerated device IDs can be selected'); +ok(!get_hardware_device('pci', '../module/e1000e'), + 'device selection does not accept paths'); +ok(!get_hardware_device('unknown', '0000:00:1f.6'), + 'unknown device groups are rejected'); +ok(!hardware_kernel_module('../e1000e'), + 'kernel module selection does not accept paths'); +ok(!hardware_kernel_module('..'), + 'kernel module selection does not accept parent directory names'); +ok(!hardware_kernel_module('not_loaded'), + 'unloaded module names are rejected'); + +# The index presents its summary as a peer tab and describes every tab. +my $index_source = slurp_test_file("$bindir/../index.cgi"); +like($index_source, + qr/\[\s*'system'\s*,\s*\$text\{'type_system'\}\s*\]/, + 'system report is an index tab'); +like($index_source, + qr/\$text\{"type_\$_"\}\.\s*' '\.\s*ui_tag\('sup',/s, + 'inventory tab counts use superscript markup'); +unlike($index_source, qr/text\('index_tab'/, + 'inventory tab counts do not use parenthesized labels'); +like($index_source, + qr/foreach my \$tab \('system', \@types\).*?print ui_div\(\$text\{"index_\$\{tab\}_desc"\}\)/s, + 'every index tab renders its description'); +like($index_source, + qr/if \(\$tab eq 'system'\).*?print_system_summary\(\$summary\)/s, + 'system summary is rendered inside its tab'); +like($index_source, + qr/my \@types = grep \{ \$_ ne 'sensor' \|\| \@\{\$devices\{\$_\}\} \}/, + 'sensor tab is omitted when no readings are available'); +like($index_source, + qr/hardware_device_link\('storage', \$device, \$device->\{'device'\}\)/, + 'storage table identifies devices by their device files'); +like($index_source, + qr/ui_print_header\([^;]*"intro"\s*,\s*0\s*,\s*1\s*\);/s, + 'index keeps module help but disables the configuration action'); + +# Detail pages keep one general table and one expandable supplementary panel, +# without repeating the index-only help and configuration actions. +my $view_source = slurp_test_file("$bindir/../view.cgi"); +my $module_source = slurp_test_file("$bindir/../module.cgi"); +my @view_tables = $view_source =~ /\bui_table_start\(/g; +my @view_hidden = $view_source =~ /\bui_hidden_table_start\(/g; +my @module_tables = $module_source =~ /\bui_table_start\(/g; +my @module_hidden = $module_source =~ /\bui_hidden_table_start\(/g; +is(scalar(@view_tables), 1, 'device page has one general information table'); +is(scalar(@view_hidden), 1, 'device page has one supplementary panel'); +like($view_source, + qr/ui_hidden_table_start\([^;]*"driver_properties"\s*,\s*1\s*,/s, + 'device supplementary panel is initially expanded'); +unlike($view_source, qr/\bui_columns_start\(/, + 'device page has no additional standalone table'); +is(scalar(@module_tables), 1, 'module page has one general information table'); +is(scalar(@module_hidden), 1, 'module page has one supplementary panel'); +like($module_source, + qr/ui_hidden_table_start\([^;]*"module_additional"\s*,\s*1\s*,/s, + 'module supplementary panel is initially expanded'); +unlike($view_source, qr/ui_print_header\([^;]*"intro"/s, + 'device page does not show module help'); +unlike($module_source, qr/ui_print_header\([^;]*"intro"/s, + 'module page does not show module help'); +like($view_source, + qr/ui_print_header\([^;]*""\s*,\s*undef\s*,\s*0\s*,\s*1\s*\);/s, + 'device page disables module help and configuration actions'); +like($module_source, + qr/ui_print_header\([^;]*""\s*,\s*undef\s*,\s*0\s*,\s*1\s*\);/s, + 'module page disables module help and configuration actions'); + +done_testing(); diff --git a/hardware-info/view.cgi b/hardware-info/view.cgi new file mode 100755 index 000000000..100e9ddf1 --- /dev/null +++ b/hardware-info/view.cgi @@ -0,0 +1,112 @@ +#!/usr/local/bin/perl +# Display detailed information for one enumerated hardware device. + +use strict; +use warnings; + +require './hardware-info-lib.pl'; ## no critic + +our (%in, %text); + +ReadParse(); +error_setup($text{'view_error'}); + +my %valid_types = map { $_, 1 } hardware_device_types(); +my $requested = defined($in{'type'}) ? $in{'type'} : ""; +my $type = $valid_types{$requested} ? $requested : ""; +error($text{'view_etype'}) if (!$type); +my $id = defined($in{'id'}) ? $in{'id'} : ""; +my $device = get_hardware_device($type, $id); +error($text{'view_enodevice'}) if (!$device); + +ui_print_header($text{"type_$type"}, + text('view_title', html_escape($device->{'name'})), "", undef, 0, 1); + +print ui_table_start($text{'view_details'}, "width=100%", 2, + [ "width=30%", undef ]); +foreach my $detail (@{$device->{'details'}}) { + my ($key, $value, $format) = @$detail; + next if (!defined($value) || $value eq ""); + print ui_table_row($text{"detail_$key"} || html_escape($key), + format_hardware_value($value, $format)); + } +print ui_table_end(); + +# Keep driver bindings and low-level uevent properties in one expanded panel. +my $has_driver = $device->{'driver'} || @{$device->{'modules'}} || + $device->{'modalias'}; +my $has_properties = keys(%{$device->{'properties'}}); +my $has_interfaces = $type eq 'usb' && @{$device->{'interfaces'}}; +if ($has_driver || $has_properties || $has_interfaces) { + print ui_hidden_table_start($text{'view_driver'}, "width=100%", 2, + "driver_properties", 1, [ "width=30%", undef ]); + print ui_table_row($text{'detail_driver'}, + html_escape($device->{'driver'})) if ($device->{'driver'}); + print ui_table_row($text{'detail_module'}, + module_links($device->{'modules'}, $device->{'driver'})) + if (@{$device->{'modules'}} || $device->{'driver'}); + print ui_table_row($text{'detail_modalias'}, + ui_tag('tt', html_escape($device->{'modalias'}))) + if ($device->{'modalias'}); + + # USB interface class and driver bindings belong with other kernel details. + foreach my $interface (@{$device->{'interfaces'} || [ ]}) { + my $label = $interface->{'name'} ? + text('view_interface_name', html_escape($interface->{'id'}), + html_escape($interface->{'name'})) : + html_escape($interface->{'id'}); + my @details = (text('view_interface_class', + html_escape($interface->{'class'}))); + push(@details, text('view_interface_driver', + html_escape($interface->{'driver'}))) + if ($interface->{'driver'}); + push(@details, text('view_interface_module', + module_links($interface->{'module'} ? + [ $interface->{'module'} ] : [ ], + $interface->{'driver'}))) + if ($interface->{'module'} || $interface->{'driver'}); + print ui_table_row($label, join("
", @details)); + } + + # Raw properties already shown with friendly labels are omitted here. + foreach my $key (sort keys(%{$device->{'properties'}})) { + next if ($key eq 'DRIVER' && $device->{'driver'}); + next if ($key eq 'MODALIAS' && $device->{'modalias'}); + print ui_table_row(ui_tag('tt', html_escape($key)), + ui_tag('tt', html_escape($device->{'properties'}->{$key}))); + } + print ui_hidden_table_end(); + } + +ui_print_footer("index.cgi?type=".urlize($type), $text{'view_return'}); + +# format_hardware_value(value, format) +# Applies units and localized enums while escaping every raw sysfs value. +sub format_hardware_value +{ +my ($value, $format) = @_; +return nice_size($value) if ($format && $format eq 'size' && + $value =~ /^\d+(?:\.\d+)?$/); +return $value ? $text{'yes'} : $text{'no'} + if ($format && $format eq 'yesno'); +return html_escape($text{"storage_$value"}) + if ($format && $format eq 'storage_type'); +return html_escape($text{"network_$value"}) + if ($format && $format eq 'network_type'); +return html_escape(text('format_mbps', $value)) + if ($format && ($format eq 'network_speed' || $format eq 'usb_speed')); +return html_escape(text('format_mhz', hardware_number($value))) + if ($format && $format eq 'cpu_frequency'); +return html_escape($value); +} + +# module_links(modules, [driver]) +# Links loaded module names to their module metadata pages. +sub module_links +{ +my ($modules, $driver) = @_; +return $text{'module_builtin'} if (!@$modules && $driver); +return join(", ", map { + ui_link("module.cgi?name=".urlize($_), ui_tag('tt', html_escape($_))) + } @$modules); +} diff --git a/mod_core_list.txt b/mod_core_list.txt index b2845c430..6e675a72b 100644 --- a/mod_core_list.txt +++ b/mod_core_list.txt @@ -1 +1 @@ -acl apache authentic-theme backup-config bind8 change-user cron custom dovecot fail2ban fdisk filemin firewalld fsdump gray-theme htaccess-htpasswd init logrotate logviewer lvm mailboxes mailcap mount mysql net nftables package-updates passwd phpini postfix postgresql proc procmail proftpd quota servers software spam sshd status systemd system-status time updown useradmin usermin webmin webmincron webminlog xterm +acl apache authentic-theme backup-config bind8 change-user cron custom dovecot fail2ban fdisk filemin firewalld fsdump gray-theme hardware-info htaccess-htpasswd init logrotate logviewer lvm mailboxes mailcap mount mysql net nftables package-updates passwd phpini postfix postgresql proc procmail proftpd quota servers software spam sshd status systemd system-status time updown useradmin usermin webmin webmincron webminlog xterm diff --git a/mod_full_list.txt b/mod_full_list.txt index 619767c54..26956da3c 100644 --- a/mod_full_list.txt +++ b/mod_full_list.txt @@ -1 +1 @@ -acl adsl-client apache at authentic-theme backup-config bacula-backup bandwidth bind8 bsdexports bsdfdisk change-user cluster-copy cluster-cron cluster-passwd cluster-shell cluster-software cluster-useradmin cluster-usermin cluster-webmin cpan cron custom dfsadmin dhcpd dovecot exim exports fail2ban fdisk fetchmail filemin filter firewall firewall6 firewalld format fsdump gray-theme grub2 heartbeat hpuxexports htaccess-htpasswd idmapd inetd init systemd inittab ipfilter ipfw ipsec iscsi-client iscsi-server iscsi-target iscsi-tgtd kea-dhcp krb5 ldap-client ldap-server ldap-useradmin logrotate logviewer lpadmin lvm mailboxes mailcap man mount mysql net nftables nginx nis openslp package-updates pam pap passwd phpini postfix postgresql ppp-client pptp-client pptp-server proc procmail proftpd qmailadmin quota raid rbac samba sarg sendmail servers sgiexports shell shorewall shorewall6 smart-status smf software spam squid sshd status stunnel syslog syslog-ng system-status tcpwrappers time tunnel updown useradmin usermin webalizer webmin webmincron webminlog xinetd xterm zones +acl adsl-client apache at authentic-theme backup-config bacula-backup bandwidth bind8 bsdexports bsdfdisk change-user cluster-copy cluster-cron cluster-passwd cluster-shell cluster-software cluster-useradmin cluster-usermin cluster-webmin cpan cron custom dfsadmin dhcpd dovecot exim exports fail2ban fdisk fetchmail filemin filter firewall firewall6 firewalld format fsdump gray-theme grub2 hardware-info heartbeat hpuxexports htaccess-htpasswd idmapd inetd init systemd inittab ipfilter ipfw ipsec iscsi-client iscsi-server iscsi-target iscsi-tgtd kea-dhcp krb5 ldap-client ldap-server ldap-useradmin logrotate logviewer lpadmin lvm mailboxes mailcap man mount mysql net nftables nginx nis openslp package-updates pam pap passwd phpini postfix postgresql ppp-client pptp-client pptp-server proc procmail proftpd qmailadmin quota raid rbac samba sarg sendmail servers sgiexports shell shorewall shorewall6 smart-status smf software spam squid sshd status stunnel syslog syslog-ng system-status tcpwrappers time tunnel updown useradmin usermin webalizer webmin webmincron webminlog xinetd xterm zones