diff options
Diffstat (limited to 'modules-available/statistics/inc/hardwareinfo.inc.php')
-rw-r--r-- | modules-available/statistics/inc/hardwareinfo.inc.php | 463 |
1 files changed, 0 insertions, 463 deletions
diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php index 1259391f..5ef94365 100644 --- a/modules-available/statistics/inc/hardwareinfo.inc.php +++ b/modules-available/statistics/inc/hardwareinfo.inc.php @@ -13,467 +13,4 @@ class HardwareInfo const HDD = 'HDD'; const CPU = 'CPU'; - - public static function parseMachine(string $uuid, string $data) - { - $data = json_decode($data, true); - $version = $data['version'] ?? 0; - if ($version != 2) { - error_log("Received unsupported hw json v$version"); - return; - } - // determine misc stuff first - $globalMainboardExtra = []; - $localMainboardExtra = []; - // physical memory array - $memArrays = self::getDmiHandles($data, 16); - $globalMainboardExtra['Memory Slot Count'] = 0; - $globalMainboardExtra['Memory Maximum Capacity'] = 0; - foreach ($memArrays as $mem) { - $mem = self::prepareDmiProperties($mem); - if (isset($mem['Number Of Devices']) && ($mem['Use'] ?? 0) === 'System Memory') { - $globalMainboardExtra['Memory Slot Count'] += $mem['Number Of Devices']; - } - if (isset($mem['Maximum Capacity'])) { - $globalMainboardExtra['Memory Maximum Capacity'] += Parser::convertSize($mem['Maximum Capacity'], 'M', false); - } - } - // BIOS section - need to cross-match this with mainboard or system model, as it doesn't have a meaningful - // identifier on its own - $bios = self::prepareDmiProperties(self::getDmiHandles($data, 0)[0]); - foreach (['Version', 'Release Date', 'Firmware Revision'] as $k) { - if (isset($bios[$k])) { - $localMainboardExtra['BIOS ' . $k] = $bios[$k]; - } - } - if (isset($bios['BIOS Revision'])) { - $localMainboardExtra['BIOS Revision'] = $bios['BIOS Revision']; - } - foreach (['Vendor', 'ROM Size'] as $k) { - if (isset($bios[$k])) { - $globalMainboardExtra['BIOS ' . $k] = $bios[$k]; - } - } - // Using the general helper function - $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, self::RAM_MODULE, function (array $flat): bool { - return ($flat['Size'] ?? 0) > 65 * 1024 * 1024; - }, - ['Locator'], - ['Data Width', 'Size', 'Form Factor', 'Type', 'Type Detail', 'Speed', 'Manufacturer', 'Part Number', - 'Minimum Voltage', 'Maximum Voltage'], - ['Locator', 'Bank Locator', 'Serial Number', 'Asset Tag', 'Configured Memory Speed', 'Configured Voltage'] - ); - // Fake RAM slots used/total into this - $localMainboardExtra['Memory Slot Occupied'] = $ramModCount; - self::updateHwTypeFromDmi($uuid, $data, 2, self::MAINBOARD, ['Manufacturer', 'Product Name'], - [], - ['Manufacturer', 'Product Name', 'Type', 'Version'], - ['Serial Number', 'Asset Tag', 'Location In Chassis'], - $globalMainboardExtra, $localMainboardExtra - ); - self::updateHwTypeFromDmi($uuid, $data, 1, self::DMI_SYSTEM, ['Manufacturer', 'Product Name'], - [], - ['Manufacturer', 'Product Name', 'Version', 'Wake-up Type'], - ['Serial Number', 'UUID', 'SKU Number'] - ); - self::updateHwTypeFromDmi($uuid, $data, 39, self::POWER_SUPPLY, ['Manufacturer'], - ['Location', 'Power Unit Group', 'Name'], // Location might be empty/"Unknown", but Name can be something like "PSU 2" - ['Manufacturer', 'Product Name', 'Model Part Number', 'Revision', 'Max Power Capacity'], - ['Serial Number', 'Asset Tag', 'Status', 'Plugged', 'Hot Replaceable'] - ); - self::updateHwTypeFromDmi($uuid, $data, 4, self::CPU, ['Version'], - ['Socket Designation'], - ['Type', 'Family', 'Manufacturer', 'Signature', 'Version', 'Core Count', 'Thread Count'], - ['Voltage', 'Current Speed', 'Upgrade', 'Core Enabled']); - self::updateHwTypeFromDmi($uuid, $data, 9, self::SYSTEM_SLOT, function(array &$entry): bool { - if (!isset($entry['Type'])) - return false; - // Split up PCIe info - if (preg_match('/^x(?<b>\d+) PCI Express( (?<g>\d+)( x(?<s>\d+))?)?$/', $entry['Type'], $out)) { - $entry['Type'] = 'PCI Express'; - $entry['PCIe Bus Width'] = $out['b']; - if (!empty($out['g'])) { - $entry['PCIe Gen'] = $out['g']; - } - if (!empty($out['s'])) { - $entry['PCIe Slot Width'] = $out['s']; - } - } - return true; - }, - ['Designation', 'ID', 'Bus Address'], - ['Type', 'PCIe Bus Width', 'PCIe Gen', 'PCIe Slot Width'], - ['Current Usage', 'Designation'] - ); - // ---- lspci ------------------------------------ - $pciHwIds = []; - foreach (($data['lspci'] ?? []) as $dev) { - $hwid = self::writeGlobalHardwareData(self::PCI_DEVICE, - self::propsFromArray($dev, 'vendor', 'device', 'rev', 'class')); - $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['slot'] ?? 'unknown', - self::propsFromArray($dev, 'slot', 'subsystem', 'subsystem_vendor')); - $pciHwIds[] = $mappingId; - } - self::markDisconnected($uuid, self::PCI_DEVICE, $pciHwIds); - // ---- Disks ------------------------------------0Y3R3K - $hddHwIds = []; - foreach (($data['drives'] ?? []) as $dev) { - if (empty($dev['readlink'])) - continue; - if (!isset($dev['smartctl'])) { - $smart = []; - } else { - $smart =& $dev['smartctl']; - } - if (!isset($dev['lsblk']['blockdevices'][0])) { - $lsblk = []; - } else { - $lsblk =& $dev['lsblk']['blockdevices'][0]; - } - if (!isset($smart['rotation_rate']) && isset($lsblk['rota']) && !$lsblk['rota']) { - $smart['rotation_rate'] = 0; - } - $hwid = self::writeGlobalHardwareData(self::HDD, [ - // Try to use the model name as the unique identifier - 'model' => $smart['model_name'] ?? $lsblk['model'] ?? 'unknown', - // Append device size as some kind of fallback, in case model is unknown - 'size' => $lsblk['size'] ?? $smart['user_capacity']['bytes'] ?? 'unknown', - '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')); - // Mangle smart attribute table - // TODO: This is (S)ATA, do NVMe too, has different structure - // TODO: Handle used endurance indicator for SSDs - $table = []; - foreach (($smart['ata_smart_attributes']['table'] ?? []) as $attr) { - if (!isset($attr['id'])) - continue; - $id = 'attr_' . $attr['id'] . '_'; - foreach (['value', 'worst', 'thresh', 'when_failed'] as $item) { - if (isset($attr[$item])) { - $table[$id . $item] = $attr[$item]; - } - } - if (isset($attr['raw']['value'])) { - if ($attr['id'] === 194) { - if (!isset($smart['temperature'])) { - $smart['temperature'] = []; - } - if (!isset($smart['temperature']['current'])) { - $smart['temperature']['current'] = $attr['raw']['value'] & 0xffff; - } - $smart['temperature']['min'] = ($attr['raw']['value'] >> 16) & 0xffff; - $smart['temperature']['max'] = ($attr['raw']['value'] >> 32) & 0xffff; - } - $table[$id . 'raw'] = $attr['raw']['value']; - } - } - // Partitions - find special ones - if (isset($dev['sfdisk']['partitiontable'])) { - $table['partition_table'] = $dev['sfdisk']['partitiontable']['label'] ?? 'none'; - switch ($dev['sfdisk']['partitiontable']['unit'] ?? 'sectors') { - case 'sectors': - $fac = 512; - break; - case 'bytes': - $fac = 1; - break; - default: - $fac = 0; - } - $i = 0; - foreach (($dev['sfdisk']['partitiontable']['partitions'] ?? []) as $part) { - $id = 'part_' . $i . '_'; - foreach (['start', 'size', 'type', 'uuid', 'name'] as $item) { - if (!isset($part[$item])) - continue; - if ($item === 'size' || $item === 'start') { - // Turn size and start into byte offsets - $table[$id . $item] = $part[$item] * $fac; - } else { - $table[$id . $item] = $part[$item]; - } - } - $type = $table[$id . 'type'] ?? 0; - $name = $table[$id . 'name'] ?? ''; - if ($type == '44' || strtolower($type) === '87f86132-ff94-4987-b250-444444444444' - || $name === 'OpenSLX-ID44') { - $table[$id . 'slxtype'] = '44'; - } elseif ($type == '45' || strtolower($type) === '87f86132-ff94-4987-b250-454545454545' - || $name === 'OpenSLX-ID45') { - $table[$id . 'slxtype'] = '45'; - } - // - ++$i; - } - } - $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['readlink'], - self::propsFromArray($smart + ($lsblk ?? []), - 'serial_number', 'firmware_version', - 'interface_speed//current//string', - 'smart_status//passed', 'temperature//current', 'temperature//min', 'temperature//max') + $table); - // Delete old partition and smart attribute entries - Database::exec("DELETE FROM machine_x_hw_prop WHERE machinehwid = :id AND prop NOT IN (:keep) - AND prop NOT LIKE '@%'", [ - 'id' => $mappingId, - 'keep' => array_keys($table), - ]); - $hddHwIds[] = $mappingId; - unset($smart, $lsblk); - } // End loop over disks - self::markDisconnected($uuid, self::HDD, $hddHwIds); - } - - private static function updateHwTypeFromDmi(string $uuid, array $data, int $type, string $dbType, - $requiredPropsOrCallback, array $pathFields, array $globalProps, array $localProps, - array $globalExtra = [], array $localExtra = []): int - { - $sections = self::getDmiHandles($data, $type); - $thisMachineHwIds = []; - foreach ($sections as $section) { - $flat = self::prepareDmiProperties($section); - if (is_array($requiredPropsOrCallback)) { - foreach ($requiredPropsOrCallback as $prop) { - if (!isset($flat[$prop])) - continue 2; - } - } - if (is_callable($requiredPropsOrCallback)) { - if (!$requiredPropsOrCallback($flat)) - continue; - } - // Global - $props = self::propsFromArray($flat, ...$globalProps); - $hwid = self::writeGlobalHardwareData($dbType, $props + $globalExtra); - // Local - $pathId = md5(self::idFromArray($flat, ...$pathFields)); - $props = self::propsFromArray($flat, ...$localProps); - $mappingId = self::writeLocalHardwareData($uuid, $hwid, $pathId, $props + $localExtra); - $thisMachineHwIds[] = $mappingId; - } - // Any hw <-> client mappings not in that list get marked as disconnected - self::markDisconnected($uuid, $dbType, $thisMachineHwIds); - return count($thisMachineHwIds); - } - - private static function writeGlobalHardwareData(string $dbType, array $global): int - { - static $cache = []; - // Since the global properties are supposed to be unique for a specific piece of hardware, use them all - // to generate a combined ID for this hardware entity, as opposed to $localProps, which should differ - // between instances of the same hardware entity, e.g. one specific HDD model has different serial numbers. - $id = md5(implode(' ', $global)); - if (!isset($cache[$id])) { - // Cache lookup, make sure we insert this only once for every run, as this is supposed to be general - // information about the hardware, e.g. model number, max. resultion, capacity, ... - $hwid = Database::insertIgnore('statistic_hw', 'hwid', ['hwtype' => $dbType, 'hwname' => $id]); - $vals = []; - foreach ($global as $k => $v) { - $vals[] = [$hwid, $k, $v]; - } - if (!empty($vals)) { - Database::exec("INSERT IGNORE INTO statistic_hw_prop (hwid, prop, `value`) VALUES :vals", - ['vals' => $vals]); - } - $cache[$id] = $hwid; - } - return $cache[$id]; - } - - /** - * Mark all devices of a given type disconnected from the given machine, with an optional - * exclude list of machine-client-mapping IDs - * @param string $uuid client - * @param string $dbType type, eg HDD - * @param array $excludedHwIds mappingIDs to exclude, ie. devices that are still connected - */ - private static function markDisconnected(string $uuid, string $dbType, array $excludedHwIds) - { - error_log("Marking disconnected for $dbType from " . implode(', ', $excludedHwIds)); - if (empty($excludedHwIds)) { - Database::exec("UPDATE machine_x_hw mxh, statistic_hw h - SET mxh.disconnecttime = UNIX_TIMESTAMP() - WHERE h.hwtype = :type AND h.hwid = mxh.hwid AND mxh.machineuuid = :uuid - AND mxh.disconnecttime = 0", - ['type' => $dbType, 'uuid' => $uuid]); - } else { - Database::exec("UPDATE machine_x_hw mxh, statistic_hw h - SET mxh.disconnecttime = UNIX_TIMESTAMP() - WHERE h.hwtype = :type AND h.hwid = mxh.hwid AND mxh.machineuuid = :uuid - AND mxh.disconnecttime = 0 AND mxh.machinehwid NOT IN (:hwids)", - ['type' => $dbType, 'uuid' => $uuid, 'hwids' => $excludedHwIds]); - } - } - - /** - * Takes key-value-array, returns a new array with only the keys listed in $fields. - */ - private static function propsFromArray(array $array, string ...$fields): array - { - $ret = []; - foreach ($fields as $field) { - if (strpos($field, '//') === false) { - if (isset($array[$field]) && !is_array($array[$field])) { - $ret[$field] = $array[$field]; - } - } else { - $parts = explode('//', $field); - $elem = $array; - foreach ($parts as $part) { - if (isset($elem[$part])) { - $elem = $elem[$part]; - } else { - $elem = false; - break; - } - } - if ($elem !== false && !is_array($elem)) { - $ret[preg_replace('~//(value|string)$~', '', $field)] = $elem; - } - } - } - return $ret; - } - - /** - * Takes key-value-array, returns a concatenated string of all the values with the keys given in $fields. - * The items are separated by spaces, and returned in the order they were given in $fields. Missing keys - * are silently omitted. - */ - private static function idFromArray(array $array, string ...$fields): string - { - $out = ''; - foreach ($fields as $field) { - if (!isset($array[$field])) - continue; - if (empty($out)) { - $out = $array[$field]; - } else { - $out .= ' ' . $array[$field]; - } - } - return $out; - } - - /** - * Takes hwinfo json, then looks up and returns all sections from the - * dmidecode subtree that represent the given dmi table entry type, - * e.g. 17 for memory. It will then return an array of 'props' subtrees. - * @param array $data hwinfo tree - * @param int $type dmi type - * @return array [ <props>, <props>, ... ] - */ - private static function getDmiHandles(array $data, int $type): array - { - if (empty($data['dmidecode'])) - return []; - $ret = []; - foreach ($data['dmidecode'] as $section) { - if ($section['handle']['type'] == $type) { - $ret[] = $section['props']; - } - } - return $ret; - } - - const SIZE_LOOKUP = ['T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024]; - - /** - * Takes an array of type [ key1 => [ 'values' => [ <val1.1>, <val1.2>, ... ] ], key2 => ... ] - * and turns it into [ key1 => <val1.1>, key2 => <val2.1>, ... ] - * Along the way, any fields with bogus values, or values analogous to empty will get removed - */ - private static function prepareDmiProperties(array $data): array - { - $ret = []; - foreach ($data as $key => $vals) { - $val = trim($vals['values'][0]); - if ($val === '[Empty]' || $val === 'NULL') - continue; - $val = preg_replace('/[^a-z0-9]/', '', strtolower($val)); - 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') { - continue; - } - $val = trim($vals['values'][0] ?? ''); - if ($key === 'Manufacturer') { - $val = self::fixManufacturer($val); - } elseif (preg_match('/^(\d+)\s+([TGMK])B$/i', $val, $out)) { - $val = $out[1] * self::SIZE_LOOKUP[strtoupper($out[2])]; - } - $ret[$key] = $val; - } - return $ret; - } - - /** - * Unify different variants of manufacturer names - */ - private static function fixManufacturer(string $in): string - { - $in = Parser::decodeJedec($in); - switch (strtolower($in)) { - case 'advanced micro devices, inc.': - case 'advanced micro devices': - case 'authenticamd': - return 'AMD'; - case 'apple inc.': - return 'Apple'; - case 'asustek computer inc.': - return 'ASUS'; - case 'dell inc.': - return 'Dell'; - case 'fujitsu': - case 'fujitsu client computing limited': - return 'Fujitsu'; - case 'hewlett packard': - case 'hewlett-packard': - return 'HP'; - case 'hynix semiconduc': - case 'hynix/hyundai': - case 'hyundai electronics hynix semiconductor inc': - case 'hynix semiconductor inc sk hynix': - return 'SK Hynix'; - case 'genuineintel': - case 'intel corporation': - case 'intel(r) corp.': - case 'intel(r) corporation': - return 'Intel'; - case 'samsung sdi': - return 'Samsung'; - } - return $in; - } - - /** - * Establish a mapping between a client and some hardware device. - * Optionally writes hardware properties specific to a hardware instance of a client - * @param string $uuid client - * @param int $hwid hw global hw id - * @param string $pathId unique identifier for the local instance of this hw, e.q. PCI slot, /dev path, something that handles the case that there are multiple instances of the same hardware in one machine - * @param array $props KV-pairs of properties to write for this instance; can be empty - * @return int - */ - private static function writeLocalHardwareData(string $uuid, int $hwid, string $pathId, array $props): int - { - // Add mapping between hw entity and machine - $mappingId = Database::insertIgnore('machine_x_hw', 'machinehwid', - ['hwid' => $hwid, 'machineuuid' => $uuid, 'devpath' => $pathId], - ['disconnecttime' => 0]); - // And all the properties specific to this entity instance (e.g. serial number) - if (!empty($props)) { - $vals = []; - foreach ($props as $k => $v) { - $vals[] = [$mappingId, $k, $v]; - } - Database::exec("INSERT INTO machine_x_hw_prop (machinehwid, prop, value) - VALUES :vals - ON DUPLICATE KEY UPDATE value = VALUES(value)", ['vals' => $vals]); - } - return $mappingId; - } - } |