From d736e75ade7a4472aefb72af9036f86016adcb42 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 30 Sep 2021 17:44:30 +0200 Subject: [statistics] Adapt hw-data parsing to new json format for display --- modules-available/statistics/api.inc.php | 30 +- .../statistics/inc/hardwareinfo.inc.php | 6 +- .../statistics/inc/hardwareparser.inc.php | 388 ++------------------- .../statistics/inc/hardwareparserlegacy.inc.php | 284 +++++++++++++++ modules-available/statistics/inc/pciid.inc.php | 36 +- modules-available/statistics/install.inc.php | 4 +- modules-available/statistics/lang/de/messages.json | 1 + modules-available/statistics/lang/de/module.json | 3 + .../statistics/lang/de/template-tags.json | 20 +- .../statistics/lang/en/template-tags.json | 1 + modules-available/statistics/pages/hints.inc.php | 50 ++- modules-available/statistics/pages/machine.inc.php | 296 ++++++++++++++-- .../statistics/permissions/permissions.json | 3 + .../statistics/templates/hints-hdd-grow.html | 65 ++++ .../templates/hints-ram-underclocked.html | 2 +- .../statistics/templates/js-pciquery.html | 24 ++ .../statistics/templates/machine-hdds.html | 46 +-- .../statistics/templates/machine-main.html | 104 ++++-- 18 files changed, 868 insertions(+), 495 deletions(-) create mode 100644 modules-available/statistics/inc/hardwareparserlegacy.inc.php create mode 100644 modules-available/statistics/templates/hints-hdd-grow.html create mode 100644 modules-available/statistics/templates/js-pciquery.html (limited to 'modules-available/statistics') diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php index 52dbe284..a945cdf5 100644 --- a/modules-available/statistics/api.inc.php +++ b/modules-available/statistics/api.inc.php @@ -1,6 +1,7 @@ $uuid, 'data' => $data]); + HardwareParser::parseMachine($uuid, json_decode($data, true)); echo 'Kweries: ' . Database::getQueryCount(); exit; } @@ -82,9 +86,21 @@ if ($type[0] === '~') { if (!is_string($hostname) || $hostname === $ip) { $hostname = ''; } + $json = false; $data = Util::cleanUtf8(Request::post('json', '', 'string')); - $hasJson = !empty($data) && $data[0] === '{'; - if (!$hasJson) { + if (!empty($data) && $data[0] === '{') { + $json = json_decode($data, true); + if (!is_array($json)) { + $json = false; + } else { + $json['cpu'] = [ + 'sockets' => Request::post('sockets', 0, 'int'), + 'cores' => $realcores, + 'threads' => Request::post('vcores', 0, 'int'), + ]; + } + } + if ($json === false) { $data = Util::cleanUtf8(Request::post('data', '', 'string')); } // Prepare insert/update to machine table @@ -143,7 +159,7 @@ if ($type[0] === '~') { . ' id44mb = :id44mb,' . ' live_tmpsize = 0, live_swapsize = 0, live_memsize = 0, live_cpuload = 255, live_cputemp = 0,' . ' badsectors = :badsectors,' - . ' data = ' . ($hasJson ? ':data' : "If(Left(data, 1) = '{', data, :data)") . ',' + . ' data = ' . ($json !== false ? ':data' : "If(Left(data, 1) = '{', data, :data)") . ',' . ' state = :state ' . " WHERE machineuuid = :machineuuid AND state = :oldstate AND lastseen = :oldlastseen", $new); if ($res === 0) { @@ -175,8 +191,8 @@ if ($type[0] === '~') { $new['locationid'] = $loc; // For Filter Event } - if ($hasJson) { - HardwareParser::parseMachine($uuid, $data); + if ($json !== false) { + HardwareParser::parseMachine($uuid, $json); } // Check for suspicious hardware changes diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php index 6a6c74cd..586b8124 100644 --- a/modules-available/statistics/inc/hardwareinfo.inc.php +++ b/modules-available/statistics/inc/hardwareinfo.inc.php @@ -54,7 +54,7 @@ class HardwareInfo $gvt = true; } else { $passthrough[$row['vendor'] . ':' . $row['device']] = 1; - error_log('Passthouorgh: ' . $row['vendor'] . ':' . $row['device']); + //error_log('Passthouorgh: ' . $row['vendor'] . ':' . $row['device']); $slots[preg_replace('/\.[0-9]+$/', '', $row['slot'])] = 1; } } @@ -64,14 +64,14 @@ class HardwareInfo } if (!empty($passthrough)) { foreach (array_keys($slots) as $slot) { - error_log('Querying slot ' . $slot); + //error_log('Querying slot ' . $slot); $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true); $hw->addWhere(false, 'slot', 'LIKE', $slot . '.%'); $hw->addGlobalColumn('vendor'); $hw->addGlobalColumn('device'); foreach ($hw->query() as $row) { $passthrough[$row['vendor'] . ':' . $row['device']] = 1; - error_log('Extra PT: ' . $row['vendor'] . ':' . $row['device']); + //error_log('Extra PT: ' . $row['vendor'] . ':' . $row['device']); } } $kcl .= ' vfio-pci.ids=' . implode(',', array_keys($passthrough)); diff --git a/modules-available/statistics/inc/hardwareparser.inc.php b/modules-available/statistics/inc/hardwareparser.inc.php index 15534749..0217835e 100644 --- a/modules-available/statistics/inc/hardwareparser.inc.php +++ b/modules-available/statistics/inc/hardwareparser.inc.php @@ -6,119 +6,6 @@ class HardwareParser const SIZE_LOOKUP = ['T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024, '' => 1]; const SI_LOOKUP = ['T' => 1000000000000, 'G' => 1000000000, 'M' => 1000000, 'K' => 1000, '' => 1]; - public static function parseCpu(&$row, $data) - { - if (0 >= preg_match_all('/^(.+):\s+(\d+)$/im', $data, $out, PREG_SET_ORDER)) { - return; - } - foreach ($out as $entry) { - $row[str_replace(' ', '', $entry[1])] = $entry[2]; - } - } - - public static function parseDmiDecode(&$row, $data) - { - $lines = preg_split("/[\r\n]+/", $data); - $section = false; - $ramOk = false; - $ramForm = $ramType = $ramSpeed = $ramClockSpeed = false; - $ramslot = []; - $row['ramslotcount'] = $row['maxram'] = 0; - foreach ($lines as $line) { - if (empty($line)) { - continue; - } - if ($line[0] !== "\t" && $line[0] !== ' ') { - if (isset($ramslot['size'])) { - $row['ramslot'][] = $ramslot; - $ramslot = []; - } - $section = $line; - $ramOk = false; - if (($ramForm || $ramType) && ($ramSpeed || $ramClockSpeed)) { - if (isset($row['ramtype']) && !$ramClockSpeed) { - continue; - } - $row['ramtype'] = $ramType . ' ' . $ramForm; - if ($ramClockSpeed) { - $row['ramtype'] .= ', ' . $ramClockSpeed; - } elseif ($ramSpeed) { - $row['ramtype'] .= ', ' . $ramSpeed; - } - $ramForm = false; - $ramType = false; - $ramClockSpeed = false; - } - continue; - } - if ($section === 'Base Board Information') { - if (preg_match('/^\s*Product Name: +(\S.+?) *$/i', $line, $out)) { - $row['mobomodel'] = $out[1]; - } - if (preg_match('/^\s*Manufacturer: +(\S.+?) *$/i', $line, $out)) { - $row['mobomanufacturer'] = $out[1]; - } - } elseif ($section === 'System Information') { - if (preg_match('/^\s*Product Name: +(\S.+?) *$/i', $line, $out)) { - $row['pcmodel'] = $out[1]; - } - if (preg_match('/^\s*Manufacturer: +(\S.+?) *$/i', $line, $out)) { - $row['pcmanufacturer'] = $out[1]; - } - } elseif ($section === 'Physical Memory Array') { - if (!$ramOk && preg_match('/Use: System Memory/i', $line)) { - $ramOk = true; - } - if ($ramOk && preg_match('/^\s*Number Of Devices:\s+(\d+)\s*$/i', $line, $out)) { - $row['ramslotcount'] += $out[1]; - } - if ($ramOk && preg_match('/^\s*Maximum Capacity:\s+(\d.+)/i', $line, $out)) { - $row['maxram'] += self::convertSize($out[1], 'G', false); - } - } elseif ($section === 'Memory Device') { - if (preg_match('/^\s*Size:\s*(.*?)\s*$/i', $line, $out)) { - $row['extram'] = true; - if (preg_match('/(\d+)\s*(\w)i?B/i', $out[1])) { - if (self::convertSize($out[1], 'M', false) < 35) - continue; // TODO: Parsing this line by line is painful. Check for other indicators, like Locator - $ramslot['size'] = self::convertSize($out[1], 'G'); - } elseif (!isset($row['ramslot']) || (count($row['ramslot']) < 8 && (!isset($row['ramslotcount']) || $row['ramslotcount'] <= 8))) { - $ramslot['size'] = '_____'; - } - } - if (preg_match('/^\s*Manufacturer:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { - $ramslot['manuf'] = self::decodeJedec($out[1]); - } - if (preg_match('/^\s*Form Factor:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { - $ramForm = $out[1]; - } - if (preg_match('/^\s*Type:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { - $ramType = $out[1]; - } - if (preg_match('/^\s*Speed:\s*(\d.*?)\s*$/i', $line, $out)) { - $ramSpeed = $out[1]; - } - if (preg_match('/^\s*Configured (?:Clock|Memory) Speed:\s*(\d.*?)\s*$/i', $line, $out)) { - $ramClockSpeed = $out[1]; - } - } elseif ($section === 'BIOS Information') { - if (preg_match(',^\s*Release Date:\s*(\d{2}/\d{2}/\d{4})\s*$,i', $line, $out)) { - $row['biosdate'] = date('d.m.Y', strtotime($out[1])); - } elseif (preg_match('/^\s*BIOS Revision:\s*(.*?)\s*$/i', $line, $out)) { - $row['biosrevision'] = $out[1]; - } elseif (preg_match('/^\s*Version:\s*(.*?)\s*$/i', $line, $out)) { - $row['biosversion'] = $out[1]; - } - } - } - if (empty($row['ramslotcount']) || (isset($row['ramslot']) && $row['ramslotcount'] < count($row['ramslot']))) { - $row['ramslotcount'] = isset($row['ramslot']) ? count($row['ramslot']) : 0; - } - if ($row['maxram'] > 0) { - $row['maxram'] .= ' GiB'; - } - } - const LOOKUP = ['T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024, '' => 1]; /** @@ -151,248 +38,12 @@ class HardwareParser return $val; } - public static function parseHdd(&$row, $data) - { - $hdds = array(); - // Could have more than one disk - linear scan - $lines = preg_split("/[\r\n]+/", $data); - $i = 0; - $mbrToMbFactor = $sectorToMbFactor = 0; - foreach ($lines as $line) { - if (preg_match('/^Disk (\S+):.* (\d+) bytes/i', $line, $out)) { - // --- Beginning of MBR disk --- - unset($hdd); - if ($out[2] < 10000) // sometimes vmware reports lots of 512byte disks - continue; - if (substr($out[1], 0, 8) === '/dev/dm-') // Ignore device mapper - continue; - // disk total size and name - $mbrToMbFactor = 0; // This is != 0 for mbr - $sectorToMbFactor = 0; // This is != for gpt - $hdd = array( - 'devid' => 'devid-' . ++$i, - 'dev' => $out[1], - 'sectors' => 0, - 'size' => round($out[2] / (1024 * 1024 * 1024)), - 'used' => 0, - 'partitions' => array(), - 'json' => array(), - ); - $hdds[] = &$hdd; - } elseif (preg_match('/^Disk (\S+):\s+(\d+)\s+sectors,/i', $line, $out)) { - // --- Beginning of GPT disk --- - unset($hdd); - if ($out[2] < 1000) // sometimes vmware reports lots of 512byte disks - continue; - if (substr($out[1], 0, 8) === '/dev/dm-') // Ignore device mapper - continue; - // disk total size and name - $mbrToMbFactor = 0; // This is != 0 for mbr - $sectorToMbFactor = 0; // This is != for gpt - $hdd = array( - 'devid' => 'devid-' . ++$i, - 'dev' => $out[1], - 'sectors' => $out[2], - 'size' => 0, - 'used' => 0, - 'partitions' => array(), - 'json' => array(), - ); - $hdds[] = &$hdd; - } elseif (preg_match('/^Units =.*= (\d+) bytes/i', $line, $out)) { - // --- MBR: Line that tells us how to interpret units for the partition lines --- - // Unit for start and end - $mbrToMbFactor = $out[1] / (1024 * 1024); // Convert so that multiplying by unit yields MiB - } elseif (preg_match('/^Logical sector size:\s*(\d+)/i', $line, $out)) { - // --- GPT: Line that tells us the logical sector size used everywhere --- - $sectorToMbFactor = $out[1] / (1024 * 1024); - } elseif (isset($hdd) && preg_match('/^First usable sector.* is (\d+)$/i', $line, $out)) { - // --- Some fdisk versions are messed up and report 2^32 as the sector count in the first line, - // but the correct value in the "last usable sector is xxxx" line below --- - if ($out[1] > $hdd['sectors']) { - $hdd['sectors'] = $out[1]; - } - } elseif (isset($hdd) && $mbrToMbFactor !== 0 && preg_match(',^/dev/(\S+)\s+.*\s(\d+)[+\-]?\s+(\d+)[+\-]?\s+\d+[+\-]?\s+([0-9a-f]+)\s+(.*)$,i', $line, $out)) { - // --- MBR: Partition entry --- - // Some partition - $type = strtolower($out[4]); - if ($type === '5' || $type === 'f' || $type === '85') { - continue; - } elseif ($type === '44') { - $out[5] = 'OpenSLX-ID44'; - $color = '#5c1'; - } elseif ($type === '45') { - $out[5] = 'OpenSLX-ID45'; - $color = '#0d7'; - } elseif ($type === '82') { - $color = '#48f'; - } else { - $color = '#e55'; - } - - $partsize = round(($out[3] - $out[2]) * $mbrToMbFactor); - $hdd['partitions'][] = array( - 'id' => $out[1], - 'name' => $out[1], - 'size' => round($partsize / 1024, $partsize < 1024 ? 1 : 0), - 'type' => $out[5], - ); - $hdd['json'][] = array( - 'label' => $out[1], - 'value' => $partsize, - 'color' => $color, - ); - $hdd['used'] += $partsize; - } elseif (isset($hdd) && $sectorToMbFactor !== 0 && preg_match(',^\s*(\d+)\s+(\d+)[+\-]?\s+(\d+)[+\-]?\s+\S+\s+([0-9a-f]+)\s+(.*)$,i', $line, $out)) { - // --- GPT: Partition entry --- - // Some partition - $type = $out[5]; - if ($type === 'OpenSLX-ID44') { - $color = '#5c1'; - } elseif ($type === 'OpenSLX-ID45') { - $color = '#0d7'; - } elseif ($type === 'Linux swap') { - $color = '#48f'; - } else { - $color = '#e55'; - } - $id = $hdd['devid'] . '-' . $out[1]; - $partsize = round(($out[3] - $out[2]) * $sectorToMbFactor); - $hdd['partitions'][] = array( - 'id' => $id, - 'name' => $out[1], - 'size' => round($partsize / 1024, $partsize < 1024 ? 1 : 0), - 'type' => $type, - ); - $hdd['json'][] = array( - 'label' => $id, - 'value' => $partsize, - 'color' => $color, - ); - $hdd['used'] += $partsize; - } - } - unset($hdd); - $i = 0; - foreach ($hdds as &$hdd) { - $hdd['used'] = round($hdd['used'] / 1024); - if ($hdd['size'] === 0 && $hdd['sectors'] !== 0) { - $hdd['size'] = round(($hdd['sectors'] * $sectorToMbFactor) / 1024); - } - $free = $hdd['size'] - $hdd['used']; - if ($hdd['size'] > 0 && ($free > 5 || ($free / $hdd['size']) > 0.1)) { - $hdd['partitions'][] = array( - 'id' => 'free-id-' . $i, - 'name' => Dictionary::translate('unused'), - 'size' => $free, - 'type' => '-', - ); - $hdd['json'][] = array( - 'label' => 'free-id-' . $i, - 'value' => $free * 1024, - 'color' => '#aaa', - ); - ++$i; - } - $hdd['json'] = json_encode($hdd['json']); - } - unset($hdd); - $row['hdds'] = &$hdds; - } - - public static function parsePci(&$pci1, &$pci2, $data) - { - preg_match_all('/[a-f0-9:.]{7}\s+"(Class\s*)?(?[a-f0-9]{4})"\s+"(?[a-f0-9]{4})"\s+"(?[a-f0-9]{4})"/is', $data, $out, PREG_SET_ORDER); - $NOW = time(); - $pci = array(); - foreach ($out as $entry) { - if (!isset($pci[$entry['class']])) { - $class = 'c.' . $entry['class']; - $res = PciId::getPciId('CLASS', $class); - if ($res === false) { - $pci[$entry['class']]['lookupClass'] = 'do-lookup'; - $pci[$entry['class']]['class'] = $class; - } else { - $pci[$entry['class']]['class'] = $res; - } - } - $new = array( - 'ven' => $entry['ven'], - 'dev' => $entry['ven'] . ':' . $entry['dev'], - ); - $res = PciId::getPciId('VENDOR', $new['ven']); - if ($res === false) { - $new['lookupVen'] = 'do-lookup'; - } else { - $new['ven'] = $res; - } - $res = PciId::getPciId('DEVICE', $new['dev']); - if ($res === false) { - $new['lookupDev'] = 'do-lookup'; - } else { - $new['dev'] = $res . ' (' . $new['dev'] . ')'; - } - $pci[$entry['class']]['entries'][] = $new; - } - ksort($pci); - foreach ($pci as $class => $entry) { - if ($class === '0300' || $class === '0200' || $class === '0403') { - $pci1[] = $entry; - } else { - $pci2[] = $entry; - } - } - } - - public static function parseSmartctl(&$hdds, $data) - { - $lines = preg_split("/[\r\n]+/", $data); - foreach ($lines as $line) { - if (preg_match('/^NEXTHDD=(.+)$/', $line, $out)) { - unset($dev); - foreach ($hdds as &$hdd) { - if ($hdd['dev'] === $out[1]) { - $dev = &$hdd; - } - } - continue; - } - if (!isset($dev)) { - continue; - } - if (preg_match('/^([A-Z][^:]+):\s*(.*)$/', $line, $out)) { - $key = preg_replace('/\s|-|_/', '', $out[1]); - if ($key === 'ModelNumber') { - $key = 'DeviceModel'; - } - $dev['s_' . $key] = $out[2]; - } elseif (preg_match('/^\s*\d+\s+(\S+)\s+\S+\s+\d+\s+\d+\s+\S+\s+\S+\s+(\d+)(\s|$)/', $line, $out)) { - $dev['s_' . preg_replace('/\s|-|_/', '', $out[1])] = $out[2]; - } - } - // Format strings - foreach ($hdds as &$hdd) { - if (isset($hdd['s_PowerOnHours'])) { - $hdd['PowerOnTime'] = ''; - $val = (int)str_replace('.', '', $hdd['s_PowerOnHours']); - if ($val > 8760) { - $hdd['PowerOnTime'] .= floor($val / 8760) . 'Y, '; - $val %= 8760; - } - if ($val > 720) { - $hdd['PowerOnTime'] .= floor($val / 720) . 'M, '; - $val %= 720; - } - if ($val > 24) { - $hdd['PowerOnTime'] .= floor($val / 24) . 'd, '; - $val %= 24; - } - $hdd['PowerOnTime'] .= $val . 'h'; - } - } - } - - private static function decodeJedec(string $string): string + /** + * Decode JEDEC ID to according manufacturer + * @param string $string + * @return string + */ + public static function decodeJedec(string $string): string { // JEDEC ID:7F 7F 9E 00 00 00 00 00 if (preg_match('/JEDEC(?:\s*ID)?\s*:\s*([0-9a-f\s]+)/i', $string, $out)) { @@ -445,7 +96,7 @@ class HardwareParser } // Count, size (unitless) if (is_numeric($val) && preg_match('/^-?[0-9]+$/', $val) - && preg_match('/used|occupied|count|number|size|temperature/', $key)) { + && preg_match('/used|occupied|count|number|size|temperature|_start|_value|_thresh|_worst|_time/', $key)) { return (int)$val; } // Date @@ -464,7 +115,7 @@ class HardwareParser * @param int $type dmi type * @return array [ , , ... ] */ - private static function getDmiHandles(array $data, int $type): array + public static function getDmiHandles(array $data, int $type): array { if (empty($data['dmidecode'])) return []; @@ -532,9 +183,8 @@ class HardwareParser * * Along the way: * 1) any fields with bogus values, or values analogous to empty will get removed - * 2) Any values ending in Bytes, bits or speed will be normalized */ - private static function prepareDmiProperties(array $data): array + public static function prepareDmiProperties(array $data): array { $ret = []; foreach ($data as $key => $vals) { @@ -545,7 +195,7 @@ class HardwareParser if ($val === '' || $val === 'notspecified' || $val === 'tobefilledbyoem' || $val === 'unknown' || $val === 'chassismanufacture' || $val === 'chassismanufacturer' || $val === 'chassisversion' || $val === 'chassisserialnumber' || $val === 'defaultstring' || $val === 'productname' - || $val === 'manufacturer' || $val === 'systemmodel' || $val === 'fillbyoem') { + || $val === 'manufacturer' || $val === 'systemmodel' || $val === 'fillbyoem' || $val === 'none') { continue; } $val = trim($vals['values'][0] ?? ''); @@ -609,9 +259,8 @@ class HardwareParser return $cache[$id]; } - public static function parseMachine(string $uuid, string $data) + public static function parseMachine(string $uuid, array $data) { - $data = json_decode($data, true); $version = $data['version'] ?? 0; if ($version != 2) { error_log("Received unsupported hw json v$version"); @@ -675,9 +324,14 @@ class HardwareParser 'Maximum Voltage'], ['Locator', 'Bank Locator', 'Serial Number', 'Asset Tag', 'Configured Memory Speed', 'Configured Voltage'] ); - // Fake RAM slots used/total into this + // Fake RAM slots used/total etc. into this $localMainboardExtra['Memory Slot Occupied'] = $ramModCount; $localMainboardExtra['Memory Installed Capacity'] = self::convertSize($capa, 'G', true); + foreach (['sockets', 'cores', 'threads'] as $key) { + if (!isset($data['cpu'][$key])) + continue; + $localMainboardExtra['cpu-' . $key] = $data['cpu'][$key]; + } self::updateHwTypeFromDmi($uuid, $data, 2, HardwareInfo::MAINBOARD, ['Manufacturer', 'Product Name'], [], ['Manufacturer', 'Product Name', 'Type', 'Version'], @@ -759,8 +413,8 @@ class HardwareParser 'size' => $size, 'physical_block_size' => $smart['physical_block_size'] ?? $lsblk['phy-sec'] ?? 0, 'logical_block_size' => $smart['logical_block_size'] ?? $lsblk['log-sec'] ?? 0, - ] + self::propsFromArray($smart, - 'rotation_rate', 'sata_version//string', 'interface_speed//max//string')); + ] + self::propsFromArray($smart, 'rotation_rate', 'sata_version//string', + 'interface_speed//max//string', 'model_family')); // Mangle smart attribute table // TODO: Handle used endurance indicator for (SATA) SSDs $table = []; @@ -843,10 +497,12 @@ class HardwareParser } } $table['unused'] = $size - $used; + $table['dev'] = $dev['readlink']; $table += self::propsFromArray($smart + ($lsblk ?? []), 'serial_number', 'firmware_version', 'interface_speed//current//string', - 'smart_status//passed', 'temperature//current', 'temperature//min', 'temperature//max'); + 'smart_status//passed', 'temperature//current', 'temperature//min', 'temperature//max', + 'power_on_time//hours'); $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['readlink'], $table); // Delete old partition and smart attribute entries diff --git a/modules-available/statistics/inc/hardwareparserlegacy.inc.php b/modules-available/statistics/inc/hardwareparserlegacy.inc.php new file mode 100644 index 00000000..277f6d4a --- /dev/null +++ b/modules-available/statistics/inc/hardwareparserlegacy.inc.php @@ -0,0 +1,284 @@ + 'devid-' . ++$i, + 'dev' => $out[1], + 'sectors' => 0, + 'size' => $out[2], + 'used' => 0, + 'partitions' => [], + ]; + $hdds[] = &$hdd; + } elseif (preg_match('/^Disk (\S+):\s+(\d+)\s+sectors,/i', $line, $out)) { + // --- Beginning of GPT disk --- + unset($hdd); + if ($out[2] < 1000) // sometimes vmware reports lots of 512byte disks + continue; + if (preg_match('#^/dev/(dm-|x?loop|d?nbd)#', $out[1])) // Ignore device mapper etc. + continue; + // disk total size and name + $mbrToByteFactor = 0; // This is != 0 for mbr + $sectorToByteFactor = 0; // This is != for gpt + $hdd = [ + 'devid' => 'devid-' . ++$i, + 'dev' => $out[1], + 'sectors' => $out[2], + 'size' => 0, + 'used' => 0, + 'partitions' => [], + ]; + $hdds[] = &$hdd; + } elseif (preg_match('/^Units =.*= (\d+) bytes/i', $line, $out)) { + // --- MBR: Line that tells us how to interpret units for the partition lines --- + // Unit for start and end + $mbrToByteFactor = $out[1]; // Convert so that multiplying by unit yields MiB + } elseif (preg_match('/^Logical sector size:\s*(\d+)/i', $line, $out)) { + // --- GPT: Line that tells us the logical sector size used everywhere --- + $sectorToByteFactor = $out[1]; + } elseif (isset($hdd) && preg_match('/^First usable sector.* is (\d+)$/i', $line, $out)) { + // --- Some fdisk versions are messed up and report 2^32 as the sector count in the first line, + // but the correct value in the "last usable sector is xxxx" line below --- + if ($out[1] > $hdd['sectors']) { + $hdd['sectors'] = $out[1]; + } + } elseif (isset($hdd) && $mbrToByteFactor !== 0 && preg_match(', + ^/dev/(\S+) # device + \s+.*\s(\d+)[+\-]? # start + \s+(\d+)[+\-]? # end + \s+\d+[+\-]? # size + \s+([0-9a-f]+) # typeid + \s+(.*)$ # type name + ,ix', $line, $out)) { + // --- MBR: Partition entry --- + // Some partition + $type = strtolower($out[4]); + if ($type === '5' || $type === 'f' || $type === '85') { + continue; + } elseif ($type === '44') { + $out[5] = 'OpenSLX-ID44'; + } elseif ($type === '45') { + $out[5] = 'OpenSLX-ID45'; + } + + $start = $out[2] * $mbrToByteFactor; + $partsize = ($out[3] - $out[2]) * $mbrToByteFactor; + $hdd['partitions'][] = [ + 'id' => $out[1], + 'index' => $out[1], + 'start' => $start, + 'size' => $partsize, + 'name' => $out[5], + 'slxtype' => $type, + ]; + $hdd['used'] += $partsize; + } elseif (isset($hdd) && $sectorToByteFactor !== 0 && preg_match(', + ^\s*(\d+) # index + \s+(\d+)[+\-]? # start + \s+(\d+)[+\-]? # end + \s+\S+ # human readable size + \s+([0-9a-f]{2})[0-9a-f]* # pseudo-type-id + \s+(.*)$ # PartLabel + ,ix', $line, $out)) { + // --- GPT: Partition entry --- + // Some partition + $slxtype = $out[4]; + if ($out[5] === 'OpenSLX-ID44') { + $slxtype = '44'; + } elseif ($out[5] === 'OpenSLX-ID45') { + $slxtype = '45'; + } elseif ($out[5] === 'Linux swap') { + $slxtype = '82'; + } + $id = $hdd['devid'] . '-' . $out[1]; + $start = $out[2] * $sectorToByteFactor; + $partsize = ($out[3] - $out[2]) * $sectorToByteFactor; + $hdd['partitions'][] = [ + 'id' => $id, + 'index' => $out[1], + 'start' => $start, + 'size' => $partsize, + 'name' => $out[5], + 'slxtype' => $slxtype, + ]; + $hdd['used'] += $partsize; + } + } + unset($hdd); + foreach ($hdds as &$hdd) { + if ($hdd['size'] === 0 && $hdd['sectors'] !== 0) { + $hdd['size'] = round($hdd['sectors'] * $sectorToByteFactor); + } + } + unset($hdd); + $row['hdds'] = &$hdds; + } + + public static function parsePci(string $data): array + { + preg_match_all('/[a-f0-9:.]{7}\s+"(Class\s*)?(?[a-f0-9]{4})"\s+"(?[a-f0-9]{4})"\s+"(?[a-f0-9]{4})"/is', $data, $out, PREG_SET_ORDER); + return $out; + } + + public static function parseSmartctl(&$hdds, $data) + { + $lines = preg_split("/[\r\n]+/", $data); + foreach ($lines as $line) { + if (preg_match('/^NEXTHDD=(.+)$/', $line, $out)) { + unset($dev); + foreach ($hdds as &$hdd) { + if ($hdd['dev'] === $out[1]) { + $dev = &$hdd; + } + } + continue; + } + if (!isset($dev)) { + continue; + } + if (preg_match('/^([A-Z][^:]+):\s*(.*)$/', $line, $out)) { + $key = preg_replace('/\s|-|_/', '', $out[1]); + if ($key === 'ModelNumber' || $key === 'DeviceModel') { + $dev['model'] = $out[2]; + } elseif ($key === 'ModelFamily') { + $dev['model_family'] = $out[2]; + } elseif ($key === 'SerialNumber') { + $dev['serial_number'] = $out[2]; + } + } elseif (preg_match('/ + ^\s*(?\d+)\s+\S+ # flags + \s+\S+\s+(?\d+) + \s+(?\d+) + \s+(?\S+)\s+\S+ # fail + \s+(?\d+)(\s|$)/x', $line, $out)) { + $dev['attr_' . $out['id']] = [ + 'value' => $out['v'], + 'worst' => $out['w'], + 'thresh' => $out['t'], + 'raw' => $out['raw'], + ]; + if ($out['id'] == 194) { + $dev['temperature'] = $out['raw']; + } + } + } + } + + public static function parseCpu(&$row, $data) + { + if (0 >= preg_match_all('/^(.+):\s+(\d+)$/im', $data, $out, PREG_SET_ORDER)) { + return; + } + $tmp = []; + foreach ($out as $entry) { + $tmp[str_replace(' ', '', $entry[1])] = $entry[2]; + } + $row['cpu']['sockets'] = $tmp['Sockets']; + $row['cpu']['cores'] = $tmp['Realcores']; + $row['cpu']['threads'] = $tmp['Virtualcores']; + } + + public static function parseDmiDecode(&$row, $data) + { + $lines = preg_split("/[\r\n]+/", $data); + $section = false; + $ramOk = false; + $ramForm = $ramType = false; + $ramslot = []; + $row['ram'] = $row['system'] = $row['mainboard'] = $row['bios'] = []; + $row['Memory Slot Count'] = $row['Memory Maximum Capacity'] = 0; + foreach ($lines as $line) { + if (empty($line)) { + continue; + } + if ($line[0] !== "\t" && $line[0] !== ' ') { + if (isset($ramslot['Size'])) { + $row['ram'][] = $ramslot; + } + $ramslot = []; + $section = $line; + $ramOk = false; + if ($ramForm || $ramType) { + if (isset($row['ramtype'])) { + continue; + } + $row['ramtype'] = $ramType . '-' . $ramForm; + $ramForm = false; + $ramType = false; + } + continue; + } + if ($section === 'Base Board Information') { + if (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out) + && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified') { + $row['mainboard'][$out[1]] = $out[2]; + } + } elseif ($section === 'System Information') { + if (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out) + && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified') { + $row['system'][$out[1]] = $out[2]; + } + } elseif ($section === 'Physical Memory Array') { + if (!$ramOk && preg_match('/Use: System Memory/i', $line)) { + $ramOk = true; + } + if ($ramOk && preg_match('/^\s*Number Of Devices:\s+(\d+)\s*$/i', $line, $out)) { + $row['Memory Slot Count'] += $out[1]; + } + if ($ramOk && preg_match('/^\s*Maximum Capacity:\s+(\d.+)/i', $line, $out)) { + $row['Memory Maximum Capacity'] += HardwareParser::convertSize($out[1], 'G', false); + } + } elseif ($section === 'Memory Device') { + if (preg_match('/^\s*Size:\s*(.*?)\s*$/i', $line, $out)) { + if (preg_match('/(\d+)\s*(\w)i?B/i', $out[1])) { + if (HardwareParser::convertSize($out[1], 'M', false) < 35) + continue; // TODO: Parsing this line by line is painful. Check for other indicators, like Locator + $ramslot['Size'] = HardwareParser::convertSize($out[1], 'G'); + } + } elseif (preg_match('/^\s*Manufacturer:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { + $ramslot['Manufacturer'] = HardwareParser::decodeJedec($out[1]); + } elseif (preg_match('/^\s*Form Factor:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { + $ramForm = $out[1]; + } elseif (preg_match('/^\s*Type:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { + $ramType = $out[1]; + } elseif (preg_match('/^\s*Configured Memory Speed:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') { + $ramslot['Configured Clock Speed'] = $out[1]; + } elseif (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out) + && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified' && $out[2] !== 'None') { + $ramslot[$out[1]] = $out[2]; + } + } elseif ($section === 'BIOS Information') { + if (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out) + && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified') { + $row['bios'][$out[1]] = $out[2]; + } + } + } + if (empty($row['Memory Slot Count']) || (isset($row['ramslot']) && $row['Memory Slot Count'] < count($row['ramslot']))) { + $row['Memory Slot Count'] = isset($row['ramslot']) ? count($row['ramslot']) : 0; + } + if ($row['Memory Maximum Capacity'] > 0) { + $row['Memory Maximum Capacity'] .= ' GiB'; + } + } +} \ No newline at end of file diff --git a/modules-available/statistics/inc/pciid.inc.php b/modules-available/statistics/inc/pciid.inc.php index 65edf570..38a2c56d 100644 --- a/modules-available/statistics/inc/pciid.inc.php +++ b/modules-available/statistics/inc/pciid.inc.php @@ -17,20 +17,36 @@ class PciId public static function getPciId(string $cat, string $id, bool $dnsQuery = false) { static $cache = []; - if ($cat === self::DEVCLASS && $id[1] !== '.') { - $id = 'c.' . $id; + if ($cat === self::DEVCLASS && $id[1] === '.') { + $id = substr($id, 2); } if ($cat === self::AUTO) { - if (preg_match('/^([a-f0-9]{4}):([a-f0-9]{4})$/', $id, $out)) { + if (preg_match('/^([a-f0-9]{4})[:._-]?([a-f0-9]{4})$/', $id, $out)) { $cat = 'DEVICE'; - } elseif (preg_match('/^([a-f0-9]{4})$/', $id, $out)) { + $host = $out[2] . '.' . $out[1]; + $id = $out[1] . ':' . $out[2]; + } elseif (preg_match('/^[a-f0-9]{4}$/', $id)) { $cat = 'VENDOR'; - } elseif (preg_match('/^c\.([a-f0-9]{2})([a-f0-9]{2})$/', $id, $out)) { + $host = $id; + } elseif (preg_match('/^c[.-]([a-f0-9]{2})([a-f0-9]{2})$/', $id)) { $cat = 'CLASS'; + $host = $out[2] . '.' . $out[1] . '.c'; + $id = substr($id, 2); } else { error_log('Invalid PCIID lookup format: ' . $id); return false; } + } elseif ($cat === self::DEVICE && preg_match('/^([a-f0-9]{4})[:._-]?([a-f0-9]{4})$/', $id, $out)) { + $host = $out[2] . '.' . $out[1]; + $id = $out[1] . ':' . $out[2]; + } elseif ($cat === self::VENDOR && preg_match('/^([a-f0-9]{4})$/', $id)) { + $host = $id; + } elseif ($cat === self::DEVCLASS && preg_match('/^(?:c[.-])?([a-f0-9]{2})([a-f0-9]{2})$/', $id, $out)) { + $host = $out[2] . '.' . $out[1] . '.c'; + $id = 'c.' . $out[1] . $out[2]; + } else { + error_log("getPciId called with unknown format: ($cat) ($id)"); + return false; } $key = $cat . '-' . $id; if (isset($cache[$key])) @@ -43,16 +59,6 @@ class PciId if (!$dnsQuery) return false; // Unknown, query - if ($cat === self::DEVICE && preg_match('/^([a-f0-9]{4}):([a-f0-9]{4})$/', $id, $out)) { - $host = $out[2] . '.' . $out[1]; - } elseif ($cat === self::VENDOR && preg_match('/^([a-f0-9]{4})$/', $id, $out)) { - $host = $out[1]; - } elseif ($cat === self::DEVCLASS && preg_match('/^c\.([a-f0-9]{2})([a-f0-9]{2})$/', $id, $out)) { - $host = $out[2] . '.' . $out[1] . '.c'; - } else { - error_log("getPciId called with unknown format: ($cat) ($id)"); - return false; - } $res = dns_get_record($host . '.pci.id.ucw.cz', DNS_TXT); if (!is_array($res)) return false; diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php index 1b664d04..67c00d8f 100644 --- a/modules-available/statistics/install.inc.php +++ b/modules-available/statistics/install.inc.php @@ -40,7 +40,7 @@ $res[] = tableCreate('machine', " `cpumodel` varchar(120) NOT NULL, `systemmodel` varchar(120) NOT NULL DEFAULT '', `id44mb` int(10) unsigned NOT NULL, - `id45mb` int(10) unsigned NOT NULL, + `id45mb` int(10) unsigned NOT NULL DEFAULT 0, `badsectors` int(10) unsigned NOT NULL, `data` mediumblob NOT NULL, `dataparsetime` int(10) unsigned NOT NULL DEFAULT 0, @@ -346,7 +346,7 @@ if (!tableHasColumn('machine', 'dataparsetime')) { } if (!tableHasColumn('machine', 'id45mb')) { $ret = Database::exec("ALTER TABLE `machine` - ADD COLUMN `id45mb` int(10) unsigned NOT NULL AFTER `id44mb`"); + ADD COLUMN `id45mb` int(10) unsigned NOT NULL DEFAULT 0 AFTER `id44mb`"); if ($ret === false) { finalResponse(UPDATE_FAILED, 'Adding id45mb column to machine table failed: ' . Database::lastError()); } diff --git a/modules-available/statistics/lang/de/messages.json b/modules-available/statistics/lang/de/messages.json index e1688cbf..c3d9bb0b 100644 --- a/modules-available/statistics/lang/de/messages.json +++ b/modules-available/statistics/lang/de/messages.json @@ -2,6 +2,7 @@ "cleared-n-machines": "{{0}} Clients zur\u00fcckgesetzt", "deleted-n-machines": "{{0}} Clients gel\u00f6scht", "ignored-both-in-use": "Rechnerpaar ignoriert, da beide noch in Betrieb zu sein scheinen. ({{0}} und {{1}})", + "ignored-no-permission": "{{0}} wurde ignoriert: Keine Berechtigung", "invalid-cidr-notion": "Ung\u00fcltiges CIDR-Format: {{0}}", "invalid-date-format": "Ung\u00fcltige Datumsangabe: {{0}}", "invalid-enum-item": "Die Auswahl {{1}} ist ung\u00fcltig f\u00fcr {{0}}", diff --git a/modules-available/statistics/lang/de/module.json b/modules-available/statistics/lang/de/module.json index 902a9573..a3006881 100644 --- a/modules-available/statistics/lang/de/module.json +++ b/modules-available/statistics/lang/de/module.json @@ -1,5 +1,8 @@ { "module_name": "Client-Statistiken", "page_title": "Client-Statistiken", + "submenu_hints": "Hinweise", + "submenu_projectors": "Beamer", + "submenu_replace": "Rechner ersetzen", "unused": "Ungenutzt" } \ No newline at end of file diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json index 43665a78..d7013da6 100644 --- a/modules-available/statistics/lang/de/template-tags.json +++ b/modules-available/statistics/lang/de/template-tags.json @@ -24,12 +24,17 @@ "lang_eventType": "Typ", "lang_firstSeen": "Erste Aktivit\u00e4t", "lang_free": "frei", + "lang_fullInfo": "Alle Werte", "lang_gbRam": "RAM", "lang_hardwareSummary": "Hardware", "lang_hasNotes": "Zu diesem Rechner wurden Notizen hinterlegt", + "lang_hddUnused": "Ungenutzt", + "lang_hddUnusedId44": "Mit freiem HDD-Speicher, ohne ID44", + "lang_hddUnusedId45": "Mit freiem HDD-Speicher, ohne ID45", "lang_hdds": "Festplatten", "lang_hostname": "Hostname", "lang_inUseMachines": "In Verwendung", + "lang_installedCountMax": "Slots belegt \/ frei", "lang_ip": "IP-Adresse", "lang_knownMachines": "Bekannte Clients", "lang_kvmState": "Status", @@ -50,6 +55,7 @@ "lang_machineOff": "Der Rechner ist ausgeschaltet, oder hat kein bwLehrpool gebootet", "lang_machineStandby": "Im Standby", "lang_machineSummary": "Zusammenfassung", + "lang_manufacturer": "Hersteller", "lang_maximumAbbrev": "Max.", "lang_mediaIntegrityErrors": "\"Media Integrity Errors\"", "lang_memoryStats": "Arbeitsspeicher", @@ -72,12 +78,17 @@ "lang_partitionSize": "Gr\u00f6\u00dfe", "lang_pcmodel": "PC-Modell", "lang_pendingSectors": "Potentiell defekte Sektoren", + "lang_persistentPart": "ID45", "lang_powerOnTime": "Betriebszeit", "lang_projector": "Beamer", "lang_projectors": "Beamer", "lang_ram": "Arbeitsspeicher", "lang_ramSize": "Gr\u00f6\u00dfe", - "lang_ramSlots": "Speicher-Slots", + "lang_ramSizeCurrentMax": "Gr\u00f6\u00dfe \/ Maximal", + "lang_ramUnderclocked": "Unter Maximaltakt laufener RAM", + "lang_ramUnderclockedText": "Dies sind Rechner mit Speicherriegeln, die unter ihrem Maximaltakt laufen. Entweder l\u00e4sst sich die Leistung des Rechners steigern, indem im BIOS der Takt angehoben wird, oder die Speicherriegel k\u00f6nnen mit anderen Rechnern getauscht werden, um eine homogene Best\u00fcckung zu erreichen.", + "lang_ramUpgrade": "RAM aufr\u00fcsten", + "lang_ramUpgradeText": "Die folgenden Rechner haben wenig RAM. F\u00fcr den Betrieb mit VMs wird das Aufr\u00fcsten des Speichers empfohlen.", "lang_realCores": "Kerne", "lang_reallocatedSectors": "Defekte Sektoren", "lang_reboot": "Neustart", @@ -100,7 +111,12 @@ "lang_showVisualization": "Visualisierung", "lang_shutdown": "Herunterfahren", "lang_shutdownConfirm": "Ausgew\u00e4hlte Rechner wirklich herunterfahren?", + "lang_slot": "Slot", + "lang_slots": "Slots", + "lang_smartSelfTestFailed": "SMART Status FAILED", "lang_sockets": "Sockel", + "lang_speed": "Geschwindigkeit", + "lang_speedDesign": "Maximalgeschwindigkeit", "lang_sureClearIp": "Die IP-Adresse der ausgew\u00e4hlten Rechner wird auf 0.0.0.0 gesetzt, wodurch die Zuordnung zum aktuellen Raum aufgehoben wird.\r\nDie Rechner bleiben mit ihren sonstigen Daten in der Datenbank vorhanden, und sobald ein Rechner das n\u00e4chste mal startet, wird die IP-Adresse wieder aktualisiert. Diese Funktion ist dann n\u00fctzlich, wenn einige Rechner in einem Raum abgebaut wurden, und in der Zukunft in einem anderen Raum wieder aufgebaut werden sollen. Durch zur\u00fccksetzen der IP-Adresse werden die Rechner in der Zwischenzeit nicht mehr im alten Raum angezeigt, was die \u00dcbersicht verbessern kann, bleiben aber \u00fcber ihre sonstigen Merkmale weiterhin in den Statistiken aufsuchbar.", "lang_sureDeletePermanent": "M\u00f6chten Sie diese(n) Rechner wirklich unwiderruflich aus der Datenbank entfernen?\r\n\r\nWichtig: L\u00f6schen verhindert nicht, dass ein Rechner nach erneutem Starten von bwLehrpool wieder in die Datenbank aufgenommen wird.", "lang_sureReplaceNoUndo": "Wollen Sie die Daten der ausgew\u00e4hlten Rechner \u00fcbertragen? Diese Aktion kann nicht r\u00fcckg\u00e4ngig gemacht werden.", @@ -111,6 +127,8 @@ "lang_timebarDesc": "Visuelle Darstellung der letzten Tage. Rote Abschnitte zeigen, wann der Rechner belegt war, gr\u00fcne, wann er nicht verwendet wurde, aber eingeschaltet war. Die leicht abgedunkelten Abschnitte markieren N\u00e4chte (22 bis 8 Uhr).", "lang_tmpGb": "Temp-HDD", "lang_total": "Gesamt", + "lang_type": "Typ", + "lang_unused": "Ungenutzt", "lang_usageDetails": "Nutzungsdetails", "lang_usageState": "Zustand", "lang_uuid": "UUID", diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json index 3fcbc049..43b6aeea 100644 --- a/modules-available/statistics/lang/en/template-tags.json +++ b/modules-available/statistics/lang/en/template-tags.json @@ -24,6 +24,7 @@ "lang_eventType": "Type", "lang_firstSeen": "First seen", "lang_free": "free", + "lang_fullInfo": "All values", "lang_gbRam": "RAM", "lang_hardwareSummary": "Hardware", "lang_hasNotes": "Notes have been added to this client", diff --git a/modules-available/statistics/pages/hints.inc.php b/modules-available/statistics/pages/hints.inc.php index 5c6acfbb..278c0e26 100644 --- a/modules-available/statistics/pages/hints.inc.php +++ b/modules-available/statistics/pages/hints.inc.php @@ -5,19 +5,26 @@ class SubPage public static function doPreprocess() { - + User::assertPermission('hints'); } public static function doRender() { - self::showMemoryUpgrade(); - self::showMemorySlow(); - self::showUnusedSpace(); + $locs = User::getAllowedLocations('hints'); + if (in_array(0, $locs)) { + $locs = []; + } + self::showMemoryUpgrade($locs); + self::showMemorySlow($locs); + self::showUnusedSpace($locs); } - private static function showMemoryUpgrade() + private static function showMemoryUpgrade(array $locs) { $q = new HardwareQuery(HardwareInfo::MAINBOARD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } $q->addLocalColumn('Memory Slot Occupied'); $q->addGlobalColumn('Memory Slot Count'); $q->addGlobalColumn('Memory Maximum Capacity'); @@ -40,9 +47,12 @@ class SubPage Render::addTemplate('hints-ram-upgrade', ['list' => $list]); } - private static function showMemorySlow() + private static function showMemorySlow(array $locs) { $q = new HardwareQuery(HardwareInfo::RAM_MODULE); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } $q->addLocalColumn('Locator'); $q->addLocalColumn('Bank Locator'); $q->addGlobalColumn('Form Factor'); @@ -57,17 +67,39 @@ class SubPage Render::addTemplate('hints-ram-underclocked', ['list' => $list]); } - private static function showUnusedSpace() + private static function showUnusedSpace(array $locs) { + $id44 = $id45 = []; + // ID44 $q = new HardwareQuery(HardwareInfo::HDD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); $q->addWhere(false, 'unused', '>', 2000000000); // 2 GB $q->addMachineWhere('id44mb', '<', 20000); // 20 GB - $id44 = $q->query()->fetchAll(); + foreach ($q->query()->fetchAll() as $row) { + $row['unused_s'] = Util::readableFileSize($row['unused']); + $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2); + $id44[] = $row; + } + // ID45 $q = new HardwareQuery(HardwareInfo::HDD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); $q->addWhere(false, 'unused', '>', 25000000000); // 25 GB $q->addMachineWhere('id44mb', '>', 20000); // 20 GB $q->addMachineWhere('id45mb', '<', 20000); // 20 GB - $id45 = $q->query()->fetchAll(); + foreach ($q->query()->fetchAll() as $row) { + $row['unused_s'] = Util::readableFileSize($row['unused']); + $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2); + $row['id45mb_s'] = Util::readableFileSize($row['id45mb'], -1, 2); + $id45[] = $row; + } Render::addTemplate('hints-hdd-grow', [ 'id44' => $id44, 'id45' => $id45, diff --git a/modules-available/statistics/pages/machine.inc.php b/modules-available/statistics/pages/machine.inc.php index f3af4f47..b23db721 100644 --- a/modules-available/statistics/pages/machine.inc.php +++ b/modules-available/statistics/pages/machine.inc.php @@ -54,7 +54,7 @@ class SubPage $client = Database::queryFirst('SELECT machineuuid, locationid, macaddr, clientip, firstseen, lastseen, logintime, lastboot, state, mbram, live_tmpsize, live_tmpfree, live_id45size, live_id45free, live_swapsize, live_swapfree, live_memsize, live_memfree, live_cpuload, live_cputemp, - Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, data, hostname, currentuser, currentsession, notes + Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, id45mb, data, hostname, currentuser, currentsession, notes FROM machine WHERE machineuuid = :uuid', array('uuid' => $uuid)); if ($client === false) { @@ -70,6 +70,55 @@ class SubPage Header('Content-Type: text/plain; charset=utf-8'); die($client['data']); } + // Parse data + $hdds = array(); + if ($client['data'][0] === '{') { + $json = json_decode($client['data'], true); + if (is_array($json)) { + $client += self::parseJson($uuid, $json); + $hdds['hdds'] = self::queryHddData($uuid); + } + } else { + self::parseLegacy($client, $hdds); + } + unset($client['data']); + // Get rid of configured speed, if equal to maximum speed + foreach ($client['ram'] as &$item) { + if (isset($item['Configured Memory Speed']) && $item['Configured Memory Speed'] === $item['Speed']) { + unset($item['Configured Memory Speed']); + } + } + unset($item); + $client['lspci1'] = $client['lspci2'] = []; + foreach ($client['lspci'] as $item) { + $item['vendor_s'] = PciId::getPciId(PciId::VENDOR, $item['vendor']); + $item['device_s'] = PciId::getPciId(PciId::DEVICE, $item['vendor'] . $item['device']); + if ($item['vendor_s'] === false) { + $pciLookup[$item['vendor']] = true; + } + if ($item['device_s'] === false) { + $pciLookup[$item['vendor'] . ':' . $item['device']] = true; + } + $class = $item['class']; + if ($class === '0300' || $class === '0200' || $class === '0403') { + $dst =& $client['lspci1']; + } else { + $dst =& $client['lspci2']; + } + if (!isset($dst[$class])) { + $dst[$class] = [ + 'class' => $class, + 'class_s' => PciId::getPciId(PciId::DEVCLASS, $class, true), + 'entries' => [], + ]; + } + $dst[$class]['entries'][] = $item; + } + unset($dst, $client['lspci']); + ksort($client['lspci1']); + ksort($client['lspci2']); + $client['lspci1'] = array_values($client['lspci1']); + $client['lspci2'] = array_values($client['lspci2']); // Runmode if (Module::isAvailable('runmode')) { $data = RunMode::getRunMode($uuid, RunMode::DATA_STRINGS); @@ -121,6 +170,7 @@ class SubPage } $client['gbram'] = round(ceil($client['mbram'] / 512) / 2, 1); $client['gbtmp'] = round($client['id44mb'] / 1024); + $client['gbid45'] = round($client['id45mb'] / 1024); foreach (['tmp', 'id45', 'swap', 'mem'] as $item) { if ($client['live_' . $item . 'size'] == 0) continue; @@ -135,44 +185,25 @@ class SubPage $client['ramclass'] = StatisticsStyling::ramColorClass($client['mbram']); $client['kvmclass'] = StatisticsStyling::kvmColorClass($client['kvmstate']); $client['hddclass'] = StatisticsStyling::hddColorClass($client['gbtmp']); - // Parse the giant blob of data - if (strpos($client['data'], "\r") !== false) { - $client['data'] = str_replace("\r", "\n", $client['data']); - } - $hdds = array(); - if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) { - foreach ($out as $section) { - if ($section[1] === 'CPU') { - HardwareParser::parseCpu($client, $section[2]); - } - if ($section[1] === 'dmidecode') { - HardwareParser::parseDmiDecode($client, $section[2]); - } - if ($section[1] === 'Partition tables') { - HardwareParser::parseHdd($hdds, $section[2]); - } - if ($section[1] === 'PCI ID') { - $client['lspci1'] = $client['lspci2'] = array(); - HardwareParser::parsePci($client['lspci1'], $client['lspci2'], $section[2]); - } - if (isset($hdds['hdds']) && $section[1] === 'smartctl') { - // This currently requires that the partition table section comes first... - HardwareParser::parseSmartctl($hdds['hdds'], $section[2]); - } - } + // Format HDD data to strings + foreach ($hdds['hdds'] as &$hdd) { + $hdd['smart_status_failed'] = !($client['smart_status//passed'] ?? 1); + self::mangleHdd($hdd); } - unset($client['data']); // BIOS update check - if (!empty($client['biosrevision'])) { - $mainboard = $client['mobomanufacturer'] . '##' . $client['mobomodel']; - $system = $client['pcmanufacturer'] . '##' . $client['pcmodel']; - $ret = self::checkBios($mainboard, $system, $client['biosdate'], $client['biosrevision']); + if (!empty($client['bios']['BIOS Revision']) || !empty($client['bios']['Release Date'])) { + if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})#', $client['bios']['Release Date'], $out)) { + $client['bios']['Release Date'] = $out[2] . '.' . $out[1] . '.' . $out[3]; + } + $mainboard = $client['mainboard']['Manufacturer'] . '##' . $client['mainboard']['Product Name']; + $system = $client['system']['Manufacturer'] . '##' . $client['system']['Product Name']; + $ret = self::checkBios($mainboard, $system, $client['bios']['Release Date'], $client['bios']['BIOS Revision']); if ($ret === false) { // Not loaded, use AJAX $params = [ 'mainboard' => $mainboard, 'system' => $system, - 'date' => $client['biosdate'], - 'revision' => $client['biosrevision'], + 'date' => $client['bios']['Release Date'], + 'revision' => $client['bios']['BIOS Revision'], ]; $client['biosurl'] = '?do=statistics&action=bios&' . http_build_query($params); } elseif (!isset($ret['status']) || $ret['status'] !== 0) { @@ -211,6 +242,10 @@ class SubPage Permission::addGlobalTags($client['perms'], null, ['hardware.projectors.edit', 'hardware.projectors.view']); // Throw output at user Render::addTemplate('machine-main', $client); + if (!empty($pciLookup)) { + Render::addTemplate('js-pciquery', + ['missing_ids' => json_encode(array_keys($pciLookup))]); + } // Sessions $NOW = time(); $cutoff = $NOW - 86400 * 7; @@ -348,7 +383,182 @@ class SubPage } } - private static function eventToIconName($event) + private static function parseLegacy(array &$client, array &$hdds) + { + // Parse the giant blob of data + if (strpos($client['data'], "\r") !== false) { + $client['data'] = str_replace("\r", "\n", $client['data']); + } + if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) { + foreach ($out as $section) { + if ($section[1] === 'CPU') { + HardwareParserLegacy::parseCpu($client, $section[2]); + } + if ($section[1] === 'dmidecode') { + HardwareParserLegacy::parseDmiDecode($client, $section[2]); + } + if ($section[1] === 'Partition tables') { + HardwareParserLegacy::parseHdd($hdds, $section[2]); + } + if ($section[1] === 'PCI ID') { + $client['lspci'] = HardwareParserLegacy::parsePci($section[2]); + } + if (isset($hdds['hdds']) && $section[1] === 'smartctl') { + // This currently requires that the partition table section comes first... + HardwareParserLegacy::parseSmartctl($hdds['hdds'], $section[2]); + } + } + } + } + + private static function parseJson(string $uuid, array $json): array + { + $return = [ + 'cpu' => $json['cpu'] ?? [], + 'lspci' => $json['lspci'] ?? [], + 'ram' => array_map(function($item) { + return HardwareParser::prepareDmiProperties($item); + }, HardwareParser::getDmiHandles($json, 17)), + ]; + foreach ($return['ram'] as $ram) { + if (!empty($ram['Form Factor']) && !empty($ram['Type'])) { + $return['ramtype'] = $ram['Type'] . '-' . $ram['Form Factor']; + break; + } + } + $need = [ + 'bios' => 0, + 'system' => 1, + 'mainboard' => 2, + ]; + foreach ($need as $name => $id) { + $return[$name] = HardwareParser::prepareDmiProperties( + HardwareParser::getDmiHandles($json, $id)[0] ?? []); + } + $q = new HardwareQuery(HardwareInfo::MAINBOARD, $uuid); + $q->addGlobalColumn('Memory Maximum Capacity'); + $q->addGlobalColumn('Memory Slot Count'); + $res = $q->query()->fetch(); + if (is_array($res)) { + $return += $res; + } + return $return; + } + + private static function queryHddData(string $uuid): array + { + $hdds = []; + $ret = Database::simpleQuery("SELECT mp.`machinehwid`, mp.`prop`, mp.`value`, mp.`numeric` + FROM machine_x_hw_prop mp + INNER JOIN machine_x_hw mxhw ON (mp.machinehwid = mxhw.machinehwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0) + INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type) + UNION SELECT mxhw.`machinehwid`, hwp.`prop`, hwp.`value`, hwp.`numeric` + FROM statistic_hw_prop hwp + INNER JOIN machine_x_hw mxhw ON (hwp.hwid = mxhw.hwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0) + INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type) + ", + ['type' => HardwareInfo::HDD, 'uuid' => $uuid]); + foreach ($ret as $row) { + if (!isset($hdds[$row['machinehwid']])) { + $hdds[$row['machinehwid']] = ['partitions' => []]; + } + $hdd =& $hdds[$row['machinehwid']]; + if (preg_match('/^(attr_[0-9]+)_(.*)$/', $row['prop'], $out)) { + // SMART attributes + if (!isset($hdd[$out[1]])) { + $hdd[$out[1]] = []; + } + $hdd[$out[1]][$out[2]] = $row['numeric'] ?? $row['value']; + } elseif (preg_match('/^part_([0-9]+)_(.*)$/', $row['prop'], $out)) { + // Partitions + if (!isset($hdd['partitions'][$out[1]])) { + $hdd['partitions'][$out[1]] = ['id' => 'dev-' . count($hdds) . '-' . $out[1], 'index' => $out[1] + 1]; + } + $hdd['partitions'][$out[1]][$out[2]] = $row['numeric'] ?? $row['value']; + } else { + $hdd[$row['prop']] = $row['numeric'] ?? $row['value']; + } + } + foreach ($hdds as $k => &$hdd) { + $hdd['devid'] = 'k' . $k; + $hdd['partitions'] = array_values($hdd['partitions']); + } + return array_values($hdds); + } + + private static function mangleHdd(array &$hdd) + { + $hours = $hdd['power_on_time//hours'] ?? $hdd['attr_9']['raw'] ?? $hdd['power_on_hours'] + ?? $hdd['power_on_time']['hours'] ?? null; + if ($hours !== null) { + $hdd['PowerOnTime'] = ''; + $val = (int)str_replace('.', '', $hours); + if ($val > 8760) { + $hdd['PowerOnTime'] .= floor($val / 8760) . 'Y, '; + $val %= 8760; + } + if ($val > 720) { + $hdd['PowerOnTime'] .= floor($val / 720) . 'M, '; + $val %= 720; + } + if ($val > 24) { + $hdd['PowerOnTime'] .= floor($val / 24) . 'd, '; + $val %= 24; + } + $hdd['PowerOnTime'] .= $val . 'h'; + } + // Sort by start for building pie-chart + $xx = array_column($hdd['partitions'], 'start'); + array_multisort($xx, SORT_ASC, SORT_NUMERIC, + $hdd['partitions']); + $used = 0; + $json = []; + $lastEnd = 0; + $minDisplaySize = $hdd['size'] / 150; + foreach ($hdd['partitions'] as &$part) { + $dist = $part['start'] - $lastEnd; + if ($dist > $minDisplaySize) { + error_log('Dist: ' . Util::readableFileSize($dist)); + $json[] = ['value' => $dist, 'color' => '#aaa']; + } + if ($part['size'] > $minDisplaySize) { + $json[] = ['value' => $part['size'], 'color' => self::typeToColor($part), 'label' => $part['id']]; + } + $part['size_s'] = Util::readableFileSize($part['size']); + $used += $part['size']; + $lastEnd = $part['start'] + $part['size']; + if (!isset($part['name'])) { + $part['name'] = self::mbrType($part['slxtype'] ?? $part['type']); + } + } + $dist = $hdd['size'] - $lastEnd; + if ($dist > $minDisplaySize) { + $json[] = ['value' => $dist, 'color' => '#aaa']; + } + $hdd['json'] = json_encode($json); + $hdd['size_s'] = Util::readableFileSize($hdd['size']); + if ($hdd['size'] - $used > 1000000000) { + $hdd['unused_s'] = Util::readableFileSize($hdd['size'] - $used); + } + // Finally sort by index for table display + array_multisort(array_column($hdd['partitions'], 'index'), SORT_ASC, + $hdd['partitions']); + } + + private static function typeToColor(array $part): string + { + switch ($part['slxtype'] ?? $part['type']) { + case 44: + return '#5c1'; + case 45: + return '#0d7'; + case 82: + return '#48f'; + } + return '#e55'; + } + + private static function eventToIconName($event): string { switch ($event) { case 'session-open': @@ -441,4 +651,22 @@ class SubPage return $retval; } + private static function mbrType(string $type): string + { + switch ($type) { + case '44': + case '45': + return 'OpenSLX-ID' . $type; + case '82': + return 'Linux Swap'; + case '83': + return 'Linux'; + case '7': + return 'NTFS/Windows'; + case 'ef': + return 'EFI'; + } + return $type; + } + } \ No newline at end of file diff --git a/modules-available/statistics/permissions/permissions.json b/modules-available/statistics/permissions/permissions.json index b27ca992..a5823775 100644 --- a/modules-available/statistics/permissions/permissions.json +++ b/modules-available/statistics/permissions/permissions.json @@ -25,5 +25,8 @@ }, "replace": { "location-aware": true + }, + "hints": { + "location-aware": true } } \ No newline at end of file diff --git a/modules-available/statistics/templates/hints-hdd-grow.html b/modules-available/statistics/templates/hints-hdd-grow.html new file mode 100644 index 00000000..dd856700 --- /dev/null +++ b/modules-available/statistics/templates/hints-hdd-grow.html @@ -0,0 +1,65 @@ +

{{lang_hddUnused}}

+ +

{{lang_hddUnusedId44}}

+ + + + + + + + + + + {{#id44}} + + + + + + {{/id44}} + +
{{lang_machine}}{{lang_unused}}{lang_id44size}}
+ + {{hostname}}{{^hostname}}{{clientip}}{{/hostname}} + +
{{machineuuid}}
+
+ {{unused_s}} + + {{id44mb_s}} +
+ +

{{lang_hddUnusedId45}}

+ + + + + + + + + + + + {{#id45}} + + + + + + + {{/id45}} + +
{{lang_machine}}{{lang_unused}}{lang_id45size}}{lang_id44size}}
+ + {{hostname}}{{^hostname}}{{clientip}}{{/hostname}} + +
{{machineuuid}}
+
+ {{unused_s}} + + {{id45mb_s}} + + {{id44mb_s}} +
\ No newline at end of file diff --git a/modules-available/statistics/templates/hints-ram-underclocked.html b/modules-available/statistics/templates/hints-ram-underclocked.html index e1f19c4f..fffd3322 100644 --- a/modules-available/statistics/templates/hints-ram-underclocked.html +++ b/modules-available/statistics/templates/hints-ram-underclocked.html @@ -10,7 +10,7 @@ {lang_speedCurrent}} {{lang_speedDesign}} {{lang_manufacturer}} - {{lang_serialNumber}} + {{lang_serialNo}} diff --git a/modules-available/statistics/templates/js-pciquery.html b/modules-available/statistics/templates/js-pciquery.html new file mode 100644 index 00000000..5d4df867 --- /dev/null +++ b/modules-available/statistics/templates/js-pciquery.html @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/modules-available/statistics/templates/machine-hdds.html b/modules-available/statistics/templates/machine-hdds.html index 4d0409f9..84feccfd 100644 --- a/modules-available/statistics/templates/machine-hdds.html +++ b/modules-available/statistics/templates/machine-hdds.html @@ -4,24 +4,27 @@
- {{s_ModelFamily}} {{dev}} + {{model_family}}{{^model_family}}{{model}}{{/model_family}} {{dev}}
- {{#s_DeviceModel}} -
{{lang_modelNo}}: {{s_DeviceModel}}, {{lang_serialNo}}: {{s_SerialNumber}}
- {{/s_DeviceModel}} - {{#s_ReallocatedSectorCt}} -
{{lang_reallocatedSectors}}: {{s_ReallocatedSectorCt}}
- {{/s_ReallocatedSectorCt}} - {{#s_CurrentPendingSector}} -
{{lang_pendingSectors}}: {{s_CurrentPendingSector}}
- {{/s_CurrentPendingSector}} - {{#s_PowerOnHours}} -
{{lang_powerOnTime}}: {{s_PowerOnHours}} {{lang_hours}} ({{PowerOnTime}})
- {{/s_PowerOnHours}} - {{#s_MediaandDataIntegrityErrors}} -
{{lang_mediaIntegrityErrors}}: {{s_MediaandDataIntegrityErrors}}
- {{/s_MediaandDataIntegrityErrors}} + {{#model}} +
{{lang_modelNo}}: {{model}}, {{lang_serialNo}}: {{serial_number}}
+ {{/model}} + {{#smart_status_failed}} +
{{lang_smartSelfTestFailed}}
+ {{/smart_status_failed}} + {{#attr_5.raw}} +
{{lang_reallocatedSectors}}: {{attr_5.raw}}
+ {{/attr_5.raw}} + {{#attr_197.raw}} +
{{lang_pendingSectors}}: {{attr_197.raw}}
+ {{/attr_197.raw}} + {{#PowerOnTime}} +
{{lang_powerOnTime}}: {{PowerOnTime}}
+ {{/PowerOnTime}} + {{#media_errors}} +
{{lang_mediaIntegrityErrors}}: {{media_errors}}
+ {{/media_errors}}
@@ -32,13 +35,16 @@ {{#partitions}} + + - - {{/partitions}}
{{index}}{{size_s}} {{name}}{{size}} GiB{{type}}
-
{{lang_total}}: {{size}} GiB
+
{{lang_total}}: {{size_s}}
+ {{#unused_s}} +
{{lang_unused}}: {{unused_s}}
+ {{/unused_s}}
@@ -46,7 +52,7 @@ document.addEventListener("DOMContentLoaded", function() { var data = {{{json}}}; var sel = false; - new Chart(document.getElementById('{{devid}}-chart').getContext('2d')).Pie(data, { + new Chart(document.getElementById('{{devid}}-chart').getContext('2d')).Pie(data, { animation: false, tooltipTemplate: "<%if (label){%><%=label%><%}%>", customTooltips: function(tooltip) { diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html index 568099e0..71df723d 100644 --- a/modules-available/statistics/templates/machine-main.html +++ b/modules-available/statistics/templates/machine-main.html @@ -220,11 +220,11 @@ {{lang_cpuModel}} {{cpumodel}} - {{#Sockets}} + {{#cpu.sockets}}
- {{lang_sockets}}: {{Sockets}}, {{lang_cores}}: {{Realcores}}, {{lang_virtualCores}}: {{Virtualcores}} + {{lang_sockets}}: {{cpu.sockets}}, {{lang_cores}}: {{cpu.cores}}, {{lang_virtualCores}}: {{cpu.threads}}
- {{/Sockets}} + {{/cpu.sockets}} {{#live_cpuload_s}}
{{lang_cpuload}}
@@ -243,13 +243,12 @@ {{lang_pcmodel}} - {{pcmodel}} ({{pcmanufacturer}}) + {{system.Product Name}} ({{system.Manufacturer}}) {{lang_mobomodel}} - {{mobomodel}} ({{mobomanufacturer}}) + {{mainboard.Product Name}} ({{mainboard.Manufacturer}}) - {{#biosdate}}
{{lang_biosVersion}}
@@ -257,19 +256,23 @@
{{{bioshtml}}}
-
{{biosversion}} ({{biosrevision}})
-
{{biosdate}}
+
{{bios.Version}} ({{bios.BIOS Revision}})
+
{{bios.Release Date}}
- {{/biosdate}} {{lang_ram}}
{{gbram}} GiB - {{#maxram}}({{lang_maximumAbbrev}} {{maxram}}){{/maxram}} - {{ramtype}} + {{#Memory Maximum Capacity}} + / {{lang_maximumAbbrev}} {{Memory Maximum Capacity}} + {{/Memory Maximum Capacity}} + {{#Memory Slot Count}} + ({{Memory Slot Count}} {{lang_slots}}) + {{/Memory Slot Count}}
+
{{ramtype}}
{{#live_memsize}}
{{lang_ram}}
@@ -286,17 +289,36 @@ {{/live_swapsize}} - {{#extram}} - {{lang_ramSlots}} - - {{ramslotcount}}: - {{#ramslot}} - [ {{size}} ] - {{/ramslot}} + + + + + + + + + + + + {{#ram}} + {{#Speed}} + + + + + + + + {{/Speed}} + {{/ram}} +
{{lang_slot}}{{lang_speed}}{{lang_manufacturer}}{{lang_serialNo}}
+ {{Locator}}, + {{Bank Locator}} + {{^Bank Locator}}{{#Set}}Set {{Set}}{{/Set}}{{/Bank Locator}} + {{Size}}{{#Configured Memory Speed}}{{Configured Memory Speed}} / {{/Configured Memory Speed}}{{Speed}}{{Manufacturer}}{{Serial Number}}
- {{/extram}} {{lang_tempPart}} @@ -304,11 +326,19 @@ {{gbtmp}} GiB
{{#live_tmpsize}} -
-
{{live_tmpfree_s}} {{lang_free}}
-
-
+
+
{{live_tmpfree_s}} {{lang_free}}
+
+
{{/live_tmpsize}} + + + + {{lang_persistentPart}} + +
+ {{gbid45}} GiB +
{{#live_id45size}}
{{live_id45free_s}} {{lang_free}}
@@ -362,16 +392,26 @@

{{lang_devices}}

{{#lspci1}} -
{{class}}
+
{{class_s}}
{{#entries}} -
 └ {{ven}} {{dev}}
+
+  └ + {{vendor_s}} + {{device_s}} + [{{vendor}}:{{device}}] +
{{/entries}} {{/lspci1}}
{{#lspci2}} -
{{class}}
+
{{class_s}}
{{#entries}} -
 └ {{ven}} {{dev}}
+
+  └ + {{vendor_s}} + {{device_s}} + [{{vendor}}:{{device}}] +
{{/entries}} {{/lspci2}}
@@ -380,13 +420,3 @@
- -- cgit v1.2.3-55-g7522