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 000000000..ebe8fd239 Binary files /dev/null and b/hardware-info/images/icon.gif differ diff --git a/hardware-info/images/icon.svg b/hardware-info/images/icon.svg new file mode 100644 index 000000000..a13148788 --- /dev/null +++ b/hardware-info/images/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + 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