Add new Hardware Information module
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled

ⓘ 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/
This commit is contained in:
Ilia Ross
2026-08-28 13:52:47 +02:00
parent 25d5189a7c
commit 7bb24044f2
15 changed files with 2243 additions and 2 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -0,0 +1,11 @@
<header>Hardware Information</header>
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.<p>
Select a device to inspect the identifiers and properties reported by the Linux kernel, including its bound driver and loadable kernel module when available.<p>
Hardware data and sensor readings are read from the Linux <tt>/sys</tt> and <tt>/proc</tt> filesystems.<p>
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.<p>
<hr>

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<rect x="3.5" y="5.5" width="37" height="35" rx="4" fill="#3b9157" stroke="#154e2b"/>
<path d="M8 13h8m17 0h4M8 33h8m17 0h4M11 9v7m0 15v6M35 9v7m0 15v6" fill="none" stroke="#a8d89c" stroke-width="1.5"/>
<circle cx="11" cy="22" r="2.5" fill="#e0c761" stroke="#6a5d25"/>
<circle cx="33" cy="25" r="2.5" fill="#72bce8" stroke="#285b78"/>
<rect x="16.5" y="14.5" width="16" height="16" rx="2" fill="#2b465f" stroke="#102334"/>
<path d="M20 11v3m4-3v3m4-3v3m-8 17v3m4-3v3m4-3v3M13 18h3m-3 4h3m-3 4h3m16-8h3m-3 4h3m-3 4h3" stroke="#d1d8dc" stroke-width="2"/>
<circle cx="35" cy="34" r="8" fill="#f7fbff" fill-opacity=".9" stroke="#386a8a" stroke-width="2"/>
<circle cx="35" cy="34" r="4" fill="#8ed2ef" fill-opacity=".55"/>
<path d="M40.5 39.5L45 44" stroke="#386a8a" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 913 B

320
hardware-info/index.cgi Executable file
View File

@@ -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_$_"}.'&nbsp;'.
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);
}

View File

@@ -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;

288
hardware-info/lang/en Normal file
View File

@@ -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

57
hardware-info/module.cgi Executable file
View File

@@ -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'});

View File

@@ -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

View File

@@ -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();

421
hardware-info/t/run-tests.t Normal file
View File

@@ -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*'&nbsp;'\.\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();

112
hardware-info/view.cgi Executable file
View File

@@ -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("<br>", @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);
}

View File

@@ -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

View File

@@ -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