diff options
Diffstat (limited to 'modules-available/statistics/inc')
13 files changed, 2068 insertions, 530 deletions
diff --git a/modules-available/statistics/inc/devicetype.inc.php b/modules-available/statistics/inc/devicetype.inc.php deleted file mode 100644 index 41ee237d..00000000 --- a/modules-available/statistics/inc/devicetype.inc.php +++ /dev/null @@ -1,6 +0,0 @@ -<?php - -class DeviceType -{ - const SCREEN = 'SCREEN'; -} diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php new file mode 100644 index 00000000..7e0bdba8 --- /dev/null +++ b/modules-available/statistics/inc/hardwareinfo.inc.php @@ -0,0 +1,249 @@ +<?php + +class HardwareInfo +{ + + // Never change these! + const RAM_MODULE = 'RAM'; + const MAINBOARD = 'MAINBOARD'; + const DMI_SYSTEM = 'DMI_SYSTEM'; + const POWER_SUPPLY = 'POWER_SUPPLY'; + const SYSTEM_SLOT = 'SYSTEM_SLOT'; + const PCI_DEVICE = 'PCI_DEVICE'; + const HDD = 'HDD'; + const CPU = 'CPU'; + const SCREEN = 'SCREEN'; + + /** + * Get a KCL modification string for the given machine, enabling GVT, PCI passthrough etc. + * You can provide a UUID and/or MAC, or nothing. If nothing is provided, + * the "uuid" and "mac" GET parameters will be used. If both are provided, + * the resulting machine that has the greatest "lastseen" value will be used. + * @param ?string $uuid UUID of machine + * @param ?string $mac MAC of machine + */ + public static function getKclModifications(?string $uuid = null, ?string $mac = null): string + { + if ($uuid === null && $mac === null) { + $uuid = Request::get('uuid', '', 'string'); + $mac = Request::get('mac', '', 'string'); + $mac = str_replace(':', '-', $mac); + } + $res = Database::simpleQuery("SELECT machineuuid, lastseen, cpumodel, locationid FROM machine + WHERE machineuuid = :uuid OR macaddr = :mac", ['uuid' => $uuid, 'mac' => $mac]); + $best = null; + foreach ($res as $row) { + if ($best === null || $best['lastseen'] < $row['lastseen']) { + $best = $row; + } + } + if ($best === null || ((int)$best['locationid']) === 0) + return ''; + $locations = Location::getLocationRootChain($best['locationid']); + if (empty($locations)) + return ''; + $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true); + // TODO: Get list of enabled pass through groups for this client's location + $hw->addForeignJoin(true, '@PASSTHROUGH', 'passthrough_group_x_location', 'groupid', + 'locationid', $locations); + $hw->addGlobalColumn('vendor'); + $hw->addGlobalColumn('device'); + $hw->addLocalColumn('slot'); + $res = $hw->query(['vendor', 'device']); + $passthrough = []; + $slots = []; + $gvt = false; + foreach ($res as $row) { + if ($row['@PASSTHROUGH'] === 'GVT') { + $gvt = true; + } else { + $passthrough[$row['vendor'] . ':' . $row['device']] = 1; + $slots[preg_replace('/\.[0-9]+$/', '', $row['slot'])] = 1; + } + } + $kcl = ''; + if ($gvt || !empty($passthrough)) { + if (strpos($best['cpumodel'], 'Intel') !== false) { + $kcl = '-iommu -intel_iommu iommu=pt intel_iommu=on'; + } elseif (strpos($best['cpumodel'], 'AMD') !== false) { + $kcl = '-iommu -amd_iommu iommu=pt amd_iommu=on'; + } else { + error_log("Cannot determine CPU manufacturer from " . $best['cpumodel']); + $kcl = '-iommu -intel_iommu iommu=pt intel_iommu=on -amd_iommu amd_iommu=on'; + } + } + if (!empty($passthrough)) { + foreach (array_keys($slots) as $slot) { + //error_log('Querying slot ' . $slot); + $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true); + $hw->addLocalColumn('slot')->addCondition('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']); + } + } + $kcl .= ' vfio-pci.ids=' . implode(',', array_keys($passthrough)); + } + if ($gvt) { + $kcl .= ' i915.enable_gvt=1'; + } + return $kcl; + } + + // For lookup (from https://en.wikipedia.org/wiki/GUID_Partition_Table) + const GPT = [ + '00000000-0000-0000-0000-000000000000' => 'Unused entry', + '024DEE41-33E7-11D3-9D69-0008C781F39F' => 'MBR partition scheme', + 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' => 'EFI System partition', + '21686148-6449-6E6F-744E-656564454649' => 'BIOS boot partition', + 'D3BFE2DE-3DAF-11DF-BA40-E3A556D89593' => 'Intel Fast Flash (iFFS) partition (for Intel Rapid Start technology)', + 'F4019732-066E-4E12-8273-346C5641494F' => 'Sony boot partition', + 'BFBFAFE7-A34F-448A-9A5B-6213EB736C22' => 'Lenovo boot partition', + 'E3C9E316-0B5C-4DB8-817D-F92DF00215AE' => 'Microsoft Reserved Partition (MSR)', + 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7' => 'Microsoft Basic data partition', + '5808C8AA-7E8F-42E0-85D2-E1E90434CFB3' => 'Microsoft Logical Disk Manager (LDM) metadata partition', + 'AF9B60A0-1431-4F62-BC68-3311714A69AD' => 'Microsoft Logical Disk Manager data partition', + 'DE94BBA4-06D1-4D40-A16A-BFD50179D6AC' => 'Windows Recovery Environment', + '37AFFC90-EF7D-4E96-91C3-2D7AE055B174' => 'IBM General Parallel File System (GPFS) partition', + 'E75CAF8F-F680-4CEE-AFA3-B001E56EFC2D' => 'Storage Spaces partition', + '558D43C5-A1AC-43C0-AAC8-D1472B2923D1' => 'Storage Replica partition', + '75894C1E-3AEB-11D3-B7C1-7B03A0000000' => 'HPUX Data partition', + 'E2A1E728-32E3-11D6-A682-7B03A0000000' => 'HPUX Service partition', + '0FC63DAF-8483-4772-8E79-3D69D8477DE4' => 'Linux filesystem data', + 'A19D880F-05FC-4D3B-A006-743F0F84911E' => 'Linux RAID partition', + '44479540-F297-41B2-9AF7-D131D5F0458A' => 'Linux Root partition (x86)', + '4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709' => 'Linux Root partition (x86-64)', + '69DAD710-2CE4-4E3C-B16C-21A1D49ABED3' => 'Linux Root partition (32-bit ARM)', + 'B921B045-1DF0-41C3-AF44-4C6F280D3FAE' => 'Linux Root partition (64-bit ARM/AArch64)', + 'BC13C2FF-59E6-4262-A352-B275FD6F7172' => 'Linux /boot partition', + '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F' => 'Linux Swap partition', + 'E6D6D379-F507-44C2-A23C-238F2A3DF928' => 'Logical Volume Manager (LVM) partition', + '933AC7E1-2EB4-4F13-B844-0E14E2AEF915' => 'Linux /home partition', + '3B8F8425-20E0-4F3B-907F-1A25A76F98E8' => 'Linux /srv (server data) partition', + '7FFEC5C9-2D00-49B7-8941-3EA10A5586B7' => 'Linux Plain dm-crypt partition', + 'CA7D7CCB-63ED-4C53-861C-1742536059CC' => 'LUKS partition', + '8DA63339-0007-60C0-C436-083AC8230908' => 'Linux Reserved', + '83BD6B9D-7F41-11DC-BE0B-001560B84F0F' => 'FreeBSD Boot partition', + '516E7CB4-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD disklabel partition', + '516E7CB5-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD Swap partition', + '516E7CB6-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD Unix File System (UFS) partition', + '516E7CB8-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD Vinum volume manager partition', + '516E7CBA-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD ZFS partition', + '74BA7DD9-A689-11E1-BD04-00E081286ACF' => 'FreeBSD nandfs partition', + '48465300-0000-11AA-AA11-00306543ECAC' => 'Hierarchical File System Plus (HFS+) partition', + '7C3457EF-0000-11AA-AA11-00306543ECAC' => 'APFS FileVault volume container', + '55465300-0000-11AA-AA11-00306543ECAC' => 'Apple UFS container', + '52414944-0000-11AA-AA11-00306543ECAC' => 'Apple RAID partition', + '52414944-5F4F-11AA-AA11-00306543ECAC' => 'Apple RAID partition, offline', + '426F6F74-0000-11AA-AA11-00306543ECAC' => 'Apple Boot partition (Recovery HD)', + '4C616265-6C00-11AA-AA11-00306543ECAC' => 'Apple Label', + '5265636F-7665-11AA-AA11-00306543ECAC' => 'Apple TV Recovery partition', + '53746F72-6167-11AA-AA11-00306543ECAC' => 'HFS+ FileVault volume container', + '69646961-6700-11AA-AA11-00306543ECAC' => 'Apple APFS Preboot partition', + '52637672-7900-11AA-AA11-00306543ECAC' => 'Apple APFS Recovery partition', + '6A82CB45-1DD2-11B2-99A6-080020736631' => 'Solaris Boot partition', + '6A85CF4D-1DD2-11B2-99A6-080020736631' => 'Solaris Root partition', + '6A87C46F-1DD2-11B2-99A6-080020736631' => 'Solaris Swap partition', + '6A8B642B-1DD2-11B2-99A6-080020736631' => 'Solaris Backup partition', + '6A898CC3-1DD2-11B2-99A6-080020736631' => 'Solaris /usr partition', + '6A8EF2E9-1DD2-11B2-99A6-080020736631' => 'Solaris /var partition', + '6A90BA39-1DD2-11B2-99A6-080020736631' => 'Solaris /home partition', + '6A9283A5-1DD2-11B2-99A6-080020736631' => 'Solaris Alternate sector', + '6A945A3B-1DD2-11B2-99A6-080020736631' => 'Solaris Reserved partition', + '49F48D32-B10E-11DC-B99B-0019D1879648' => 'NetBSD Swap partition', + '49F48D5A-B10E-11DC-B99B-0019D1879648' => 'NetBSD FFS partition', + '49F48D82-B10E-11DC-B99B-0019D1879648' => 'NetBSD LFS partition', + '49F48DAA-B10E-11DC-B99B-0019D1879648' => 'NetBSD RAID partition', + '2DB519C4-B10F-11DC-B99B-0019D1879648' => 'NetBSD Concatenated partition', + '2DB519EC-B10F-11DC-B99B-0019D1879648' => 'NetBSD Encrypted partition', + 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309' => 'Chrome OS kernel', + '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC' => 'Chrome OS rootfs', + 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3' => 'Chrome OS firmware', + '2E0A753D-9E48-43B0-8337-B15192CB1B5E' => 'Chrome OS future use', + '09845860-705F-4BB5-B16C-8A8A099CAF52' => 'Chrome OS miniOS', + '3F0F8318-F146-4E6B-8222-C28C8F02E0D5' => 'Chrome OS hibernate', + '5DFBF5F4-2848-4BAC-AA5E-0D9A20B745A6' => '/usr partition (coreos-usr)', + '3884DD41-8582-4404-B9A8-E9B84F2DF50E' => 'Resizable rootfs (coreos-resize)', + 'C95DC21A-DF0E-4340-8D7B-26CBFA9A03E0' => 'OEM customizations (coreos-reserved)', + 'BE9067B9-EA49-4F15-B4F6-F36F8C9E1818' => 'Root filesystem on RAID (coreos-root-raid)', + '42465331-3BA3-10F1-802A-4861696B7521' => 'Haiku BFS', + '85D5E45E-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Boot partition', + '85D5E45A-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Data partition', + '85D5E45B-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Swap partition', + '0394EF8B-237E-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Unix File System (UFS) partition', + '85D5E45C-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Vinum volume manager partition', + '85D5E45D-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD ZFS partition', + '45B0969E-9B03-4F30-B4C6-B4B80CEFF106' => 'Cepth Journal', + '45B0969E-9B03-4F30-B4C6-5EC00CEFF106' => 'Cepth dm-crypt journal', + '4FBD7E29-9D25-41B8-AFD0-062C0CEFF05D' => 'Cepth OSD', + '4FBD7E29-9D25-41B8-AFD0-5EC00CEFF05D' => 'Cepth dm-crypt OSD', + '89C57F98-2FE5-4DC0-89C1-F3AD0CEFF2BE' => 'Cepth Disk in creation', + '89C57F98-2FE5-4DC0-89C1-5EC00CEFF2BE' => 'Cepth dm-crypt disk in creation', + 'CAFECAFE-9B03-4F30-B4C6-B4B80CEFF106' => 'Cepth Block', + '30CD0809-C2B2-499C-8879-2D6B78529876' => 'Cepth Block DB', + '5CE17FCE-4087-4169-B7FF-056CC58473F9' => 'Cepth Block write-ahead log', + 'FB3AABF9-D25F-47CC-BF5E-721D1816496B' => 'Cepth Lockbox for dm-crypt keys', + '4FBD7E29-8AE0-4982-BF9D-5A8D867AF560' => 'Cepth Multipath OSD', + '45B0969E-8AE0-4982-BF9D-5A8D867AF560' => 'Cepth Multipath journal', + 'CAFECAFE-8AE0-4982-BF9D-5A8D867AF560' => 'Cepth Multipath block', + '7F4A666A-16F3-47A2-8445-152EF4D03F6C' => 'Cepth Multipath block', + 'EC6D6385-E346-45DC-BE91-DA2A7C8B3261' => 'Cepth Multipath block DB', + '01B41E1B-002A-453C-9F17-88793989FF8F' => 'Cepth Multipath block write-ahead log', + 'CAFECAFE-9B03-4F30-B4C6-5EC00CEFF106' => 'Cepth dm-crypt block', + '93B0052D-02D9-4D8A-A43B-33A3EE4DFBC3' => 'Cepth dm-crypt block DB', + '306E8683-4FE2-4330-B7C0-00A917C16966' => 'Cepth dm-crypt block write-ahead log', + '45B0969E-9B03-4F30-B4C6-35865CEFF106' => 'Cepth dm-crypt LUKS journal', + 'CAFECAFE-9B03-4F30-B4C6-35865CEFF106' => 'Cepth dm-crypt LUKS block', + '166418DA-C469-4022-ADF4-B30AFD37F176' => 'Cepth dm-crypt LUKS block DB', + '86A32090-3647-40B9-BBBD-38D8C573AA86' => 'Cepth dm-crypt LUKS block write-ahead log', + '4FBD7E29-9D25-41B8-AFD0-35865CEFF05D' => 'Cepth dm-crypt LUKS OSD', + '824CC7A0-36A8-11E3-890A-952519AD3F61' => 'OpenBSD Data partition', + 'CEF5A9AD-73BC-4601-89F3-CDEEEEE321A1' => 'Power-safe (QNX6) file system', + 'C91818F9-8025-47AF-89D2-F030D7000C2C' => 'Plan 9 partition', + '9D275380-40AD-11DB-BF97-000C2911D1B8' => 'vmkcore (coredump partition)', + 'AA31E02A-400F-11DB-9590-000C2911D1B8' => 'VMFS filesystem partition', + '9198EFFC-31C0-11DB-8F78-000C2911D1B8' => 'VMware Reserved', + '2568845D-2332-4675-BC39-8FA5A4748D15' => 'Android-x86 Bootloader', + '114EAFFE-1552-4022-B26E-9B053604CF84' => 'Android-x86 Bootloader2', + '49A4D17F-93A3-45C1-A0DE-F50B2EBE2599' => 'Android-x86 Boot', + '4177C722-9E92-4AAB-8644-43502BFD5506' => 'Android-x86 Recovery', + 'EF32A33B-A409-486C-9141-9FFB711F6266' => 'Android-x86 Misc', + '20AC26BE-20B7-11E3-84C5-6CFDB94711E9' => 'Android-x86 Metadata', + '38F428E6-D326-425D-9140-6E0EA133647C' => 'Android-x86 System', + 'A893EF21-E428-470A-9E55-0668FD91A2D9' => 'Android-x86 Cache', + 'DC76DDA9-5AC1-491C-AF42-A82591580C0D' => 'Android-x86 Data', + 'EBC597D0-2053-4B15-8B64-E0AAC75F4DB1' => 'Android-x86 Persistent', + 'C5A0AEEC-13EA-11E5-A1B1-001E67CA0C3C' => 'Android-x86 Vendor', + 'BD59408B-4514-490D-BF12-9878D963F378' => 'Android-x86 Config', + '8F68CC74-C5E5-48DA-BE91-A0C8C15E9C80' => 'Android-x86 Factory', + '9FDAA6EF-4B3F-40D2-BA8D-BFF16BFB887B' => 'Android-x86 Factory (alt)', + '767941D0-2085-11E3-AD3B-6CFDB94711E9' => 'Android-x86 Fastboot / Tertiary', + 'AC6D7924-EB71-4DF8-B48D-E267B27148FF' => 'Android-x86 OEM', + '19A710A2-B3CA-11E4-B026-10604B889DCF' => 'Android Meta', + '193D1EA4-B3CA-11E4-B075-10604B889DCF' => 'Android EXT', + '7412F7D5-A156-4B13-81DC-867174929325' => 'ONIE Boot', + 'D4E6E2CD-4469-46F3-B5CB-1BFF57AFC149' => 'ONIE Config', + '9E1A2D38-C612-4316-AA26-8B49521E5A8B' => 'PReP boot', + '734E5AFE-F61A-11E6-BC64-92361F002671' => 'Atari TOS Basic data partition (GEM, BGM, F32)', + '8C8F8EFF-AC95-4770-814A-21994F2DBC8F' => 'VeraCrypt Encrypted data', + '90B6FF38-B98F-4358-A21F-48F35B4A8AD3' => 'ArcaOS Type 1', + '7C5222BD-8F5D-4087-9C00-BF9843C7B58C' => 'SPDK block device', + '4778ED65-BF42-45FA-9C5B-287A1DC4AAB1' => 'barebox-state', + '3DE21764-95BD-54BD-A5C3-4ABE786F38A8' => 'U-Boot environment', + 'B6FA30DA-92D2-4A9A-96F1-871EC6486200' => 'SoftRAID_Status', + '2E313465-19B9-463F-8126-8A7993773801' => 'SoftRAID_Scratch', + 'FA709C7E-65B1-4593-BFD5-E71D61DE9B02' => 'SoftRAID_Volume', + 'BBBA6DF5-F46F-4A89-8F59-8765B2727503' => 'SoftRAID_Cache', + 'FE8A2634-5E2E-46BA-99E3-3A192091A350' => 'Fuchsia Bootloader (slot A/B/R)', + 'D9FD4535-106C-4CEC-8D37-DFC020CA87CB' => 'Fuchsia Durable mutable encrypted system data', + 'A409E16B-78AA-4ACC-995C-302352621A41' => 'Fuchsia Durable mutable bootloader data (including A/B/R metadata)', + 'F95D940E-CABA-4578-9B93-BB6C90F29D3E' => 'Fuchsia Factory-provisioned read-only system data', + '10B8DBAA-D2BF-42A9-98C6-A7C5DB3701E7' => 'Fuchsia Factory-provisioned read-only bootloader data', + '49FD7CB8-DF15-4E73-B9D9-992070127F0F' => 'Fuchsia Volume Manager', + '421A8BFC-85D9-4D85-ACDA-B64EEC0133E9' => 'Fuchsia Verified boot metadata (slot A/B/R)', + '9B37FFF6-2E58-466A-983A-F7926D0B04E0' => 'Fuchsia Zircon boot image (slot A/B/R)', + ]; + +} diff --git a/modules-available/statistics/inc/hardwareparser.inc.php b/modules-available/statistics/inc/hardwareparser.inc.php new file mode 100644 index 00000000..428f7d55 --- /dev/null +++ b/modules-available/statistics/inc/hardwareparser.inc.php @@ -0,0 +1,789 @@ +<?php + +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]; + + /** + * Convert/format size unit. Input string can be a size like + * 8 GB or 1024 MB and will be converted according to passed parameters. + * @param string $string Input string + * @param string $scale 'a' for auto, T/G/M/K/'' for according units + * @param bool $appendUnit append unit string, e.g. 'GiB' + * @return false|string|int Formatted result + */ + public static function convertSize(string $string, string $scale = 'a', bool $appendUnit = true) + { + if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out)) { + //error_log("Not size: $string"); + return false; + } + $val = (int)$out[1] * self::SIZE_LOOKUP[strtoupper($out[2])]; + if (!array_key_exists($scale, self::SIZE_LOOKUP)) { + foreach (self::SIZE_LOOKUP as $k => $v) { + if ($k === '' || $val / 8 >= $v || abs($val - $v) < 50) { + $scale = $k; + break; + } + } + } + $val = (int)round($val / self::SIZE_LOOKUP[$scale]); + if ($appendUnit) { + $val .= ' ' . ($scale === '' ? 'Byte' : $scale . 'iB'); // NBSP!! + } + return $val; + } + + /** + * Decode JEDEC ID to according manufacturer + */ + public static function decodeJedec(string $string): string + { + // JEDEC ID:7F 7F 9E 00 00 00 00 00 + // or the ID as 8 hex digits with no spacing and prefix + $id = null; + if (preg_match('/JEDEC(?:\s*ID)?\s*:?\s*([0-9a-f\s]{8,23})\s*$/i', $string, $out) + || preg_match('/^([0-9a-f]{14}00)$/i', $string, $out)) { + preg_match_all('/[0-9a-f]{2}/i', $out[1], $out); + $bank = 0; + foreach ($out[0] as $id) { + $bank++; + $id = hexdec($id) & 0x7f; // Let's just ignore the parity bit, and any potential error + if ($id !== 0x7f) + break; + } + if ($id !== null) { + $id = self::lookupJedec($bank, $id); + } + } elseif (preg_match('/Unknown.{0,16}[\[(](?:0x)?([0-9a-fA-F]{2,4})[\])]/', $string, $out)) { + // First byte (big endian) is id-in-bank, low byte is bank + $id = self::decodeBankAndId($out, false); + } elseif (preg_match('/JEDEC(?:\s*ID)?\s*:?\s*([0-9a-f]{2}\s?[0-9a-f]{2})/i', $string, $out) + || (preg_match('/^([0-9A-F]{4})([0-9A-F]{4})([0-9A-F]{4})$/', $string, $out) && $out[2] === '0000')) { + // First byte is bank, second byte is id-in-bank + $id = self::decodeBankAndId($out, true); + } elseif (preg_match('/^([0-9a-f]{4})$/i', $string, $out)) { + // This one was seen with both endianesses + $id = self::decodeBankAndId($out, true); + if ($id === null) { + $id = self::decodeBankAndId($out, false); + } + } + + if ($id !== null) + return $id; + return $string; + } + + private static function decodeBankAndId(array $out, bool $bankFirst): ?string + { + // 16bit encoding from DDR3+: lower byte is number of 0x7f bytes, upper byte is id within bank + $id = hexdec(str_replace(' ', '', $out[1])); + // Our bank counting starts at one. Also ignore parity bit. + $bank = ($id & 0x7f); + // Shift down id, get rid of parity bit + $id = ($id >> 8) & 0x7f; + if ($bankFirst) { + // Observed second case, on OptiPlex 5050, is 80AD000080AD, but here endianness is reversed + $tmp = $id; + $id = $bank; + $bank = $tmp; + } + $bank++; + return self::lookupJedec($bank, $id); + } + + private static function lookupJedec(int $bank, int $id): ?string + { + static $data = false; + if ($data === false) { + $data = json_decode(file_get_contents(dirname(__FILE__) . '/jedec.json'), true); + } + if (array_key_exists('bank' . $bank, $data) && array_key_exists('id' . $id, $data['bank' . $bank])) + return $data['bank' . $bank]['id' . $id]; + return null; + } + + /** + * Turn several numeric measurements like Size, Speed, Voltage into a unitless + * base representation, meant for comparison. For example, Voltages are converted + * to Millivolts, Anything measured in [KMGT]Bytes (per second) to bytes, GHz to + * Hz, and so on. + * @return ?int value, or null if not numeric + */ + private static function toNumeric(string $key, string $val): ?int + { + $key = strtolower($key); + // Normalize voltage to mV + if ((strpos($key, 'volt') !== false || strpos($key, 'current') !== false) + && preg_match('/^([0-9]+(?:\.[0-9]+)?)\s+(m?)V/', $val, $out)) { + return (int)($out[1] * ($out[2] === 'm' ? 1 : 1000)); + } + if (preg_match('/speed|width|size|capacity/', $key) + && preg_match('#^([0-9]+(?:\.[0-9]+)?)\s*([TGMK]?)i?([BT](?:it|yte|))s?(?:/s)?#i', + $val, $out)) { + // Matched (T/G/M) Bits, Bytes, etc... + // For bits, use SI + if ($out[3] !== 'B' && strtolower($out[3]) !== 'byte') + return (int)($out[1] * self::SI_LOOKUP[strtoupper($out[2])]); + // For bytes, use 1024 + return (int)($out[1] * self::SIZE_LOOKUP[strtoupper($out[2])]); + } + // Speed in Hz + if (preg_match('#^([0-9]+(?:\.[0-9]+)?)\s*([TGMK]?)Hz#i', + $val, $out)) { + return (int)($out[1] * self::SI_LOOKUP[strtoupper($out[2])]); + } + // Count, size (unitless) + if (is_numeric($val) && preg_match('/^-?[0-9]+$/', $val) + && preg_match('/used|occupied|count|number|speed|width|size|capacity|temperature|_start|_value|_thresh|_worst|_time|_rate/', $key)) { + return (int)$val; + } + // Date + if (preg_match('#^(?:[0-9]{2}/[0-9]{2}/[0-9]{4}|[0-9]{4}-[0-9]{2}-[0-9]{2})$#', $val)) { + return (int)strtotime($val); + } + return null; + } + + /** + * 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>, ... ] + */ + public 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; + } + + /** + * 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; + } + + /** + * 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 ID of mapping in DB + */ + private static function writeLocalHardwareData(string $uuid, int $hwid, string $pathId, array $props): int + { + // Add mapping between hw entity and machine + $pathId = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $pathId); + $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, self::toNumeric($k, $v)]; + } + Database::exec("INSERT INTO machine_x_hw_prop (machinehwid, prop, `value`, `numeric`) + VALUES :vals + ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `numeric` = VALUES(`numeric`)", ['vals' => $vals]); + } + return $mappingId; + } + + /** + * 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: + * 1) any fields with bogus values, or values analogous to empty will get removed + */ + public static function prepareDmiProperties(array $data): array + { + $ret = []; + foreach ($data as $key => $vals) { + $val = trim($vals['values'][0] ?? 'NULL'); + 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' || $val === 'none') { + continue; + } + $val = trim($vals['values'][0] ?? ''); + if ($key === 'Manufacturer') { + $val = self::fixManufacturer($val); + } + $ret[$key] = $val; + } + return $ret; + } + + /** + * 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 except " . 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]); + } + } + + /** + * Insert some hardware into database. $global is supposed to contain key-value-pairs of properties + * this hardware has that is the same for every instance of this hardware, like model number, speed + * or size. Individual properties, like a serial number, are considered local properties, and go + * into a different table, that would contain a row for each client that has this hardware. + * @param string $dbType Hardware typ (HDD, RAM, ...) + * @param array $global associative array of properties this hardware has + * @return int id of this hardware; primary key of row in statistic_hw_prop + */ + private static function writeGlobalHardwareData(string $dbType, array $global, array $globalExtra = []): 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)); + // But don't include our "fake" fields in this as we might add more there later, which would + // change the ID then. + $global += $globalExtra; + 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. resolution, capacity, ... + $hwid = Database::insertIgnore('statistic_hw', 'hwid', ['hwtype' => $dbType, 'hwname' => $id]); + $vals = []; + foreach ($global as $k => $v) { + $vals[] = [$hwid, $k, $v, self::toNumeric($k, $v)]; + } + if (!empty($vals)) { + Database::exec("INSERT INTO statistic_hw_prop (hwid, prop, `value`, `numeric`) + VALUES :vals + ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `numeric` = VALUES(`numeric`)", + ['vals' => $vals]); + } + $cache[$id] = $hwid; + } + return $cache[$id]; + } + + /** + * Process hardware info for given client. + * @param string $uuid System-UUID of client + * @param array $data Hardware info, deserialized assoc array. + * @return ?array id44mb and id45mb as calculated from given HDD data + */ + public static function parseMachine(string $uuid, array $data): ?array + { + $version = $data['version'] ?? 0; + if ($version != 2) { + error_log("Received unsupported hw json v$version"); + return null; + } + // determine misc stuff first + $globalCpuExtra = []; + $globalMainboardExtra = []; + $localMainboardExtra = []; + // physical memory array + $memArrays = self::getDmiHandles($data, 16); + // We mostly have a seprate hardware type for all the dmi types, but not for memory arrays. + // These get added to the mainboard hw-type as it's practically a property of the mainboard. + // While we can have multiple physical memory arrays, we only ever have one mainboard per + // client. Add up the data from all arrays. + $globalMainboardExtra['Memory Slot Count'] = 0; + $globalMainboardExtra['Memory Maximum Capacity'] = 0; + foreach ($memArrays as $mem) { + $mem = self::prepareDmiProperties($mem); + // Not all memory arrays are for RAM.... + if (($mem['Use'] ?? 0) !== 'System Memory') + continue; + if (isset($mem['Number Of Devices'])) { + $globalMainboardExtra['Memory Slot Count'] += $mem['Number Of Devices']; + } + if (isset($mem['Maximum Capacity'])) { + // Temporary unit is MB + $globalMainboardExtra['Memory Maximum Capacity'] + += self::convertSize($mem['Maximum Capacity'], 'M', false); + } + } + // Now finally convert to GB + $globalMainboardExtra['Memory Maximum Capacity'] + = self::convertSize($globalMainboardExtra['Memory Maximum Capacity'] . ' MB', 'G'); + // BIOS section - need to combine this with mainboard or system model, as it doesn't have a meaningful + // identifier on its own. So again like above, we add this to the mainboard data. + $bios = self::prepareDmiProperties(self::getDmiHandles($data, 0)[0] ?? []); + foreach (['Version', 'Release Date', 'Firmware Revision'] as $k) { + if (isset($bios[$k])) { + // Prefix with "BIOS" to clarify, since it's added to the mainboard meta-data + $localMainboardExtra['BIOS ' . $k] = $bios[$k]; + } + } + if (isset($bios['BIOS Revision'])) { // This one already has the BIOS prefix + $localMainboardExtra['BIOS Revision'] = $bios['BIOS Revision']; + } + // Vendor and ROM size of BIOS *should* always be the same for a specific mainboard + foreach (['Vendor', 'ROM Size'] as $k) { + if (isset($bios[$k])) { + $globalMainboardExtra['BIOS ' . $k] = $bios[$k]; + } + } + // "Normal" dmi entries - these map directly to one of our hardware types + // RAM modules + $capa = 0; + $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, HardwareInfo::RAM_MODULE, + // Filter callback - we can modify the entry, or return false to ignore it + function (array $flat) use (&$capa): bool { + $size = self::convertSize(($flat['Size'] ?? 0), '', false); + // Let's assume we're never running on old HW with <=128MB modules, so this + // might be a hint that this is some other kind of memory. The proper way would be + // to check if the related physical memory array (16) has "Use" = "System Memory" + if ($size > 129 * 1024 * 1024) { + $capa += $size; + return true; + } + return false; + }, + ['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'] + ); + // Put RAM slots used/total etc. into mainboard data + $localMainboardExtra['Memory Slot Occupied'] = $ramModCount; + $localMainboardExtra['Memory Installed Capacity'] = self::convertSize($capa, 'G', true); + // Also add generic socket, core and thread count to mainboard data. This doesn't seem to make too much sense + // at first since it's not a property of the mainboard. But we can get away with it since we make it a local + // property, i.e. specific to a client. This is just aggregated, so it's not super well suited for the CPU + // hardware type, referenced below. In fact, on some systems the dmi/smbios tables don't contain all that much + // information about the CPU at all, so we have at least this. + foreach (['sockets', 'cores', 'threads'] as $key) { + if (!isset($data['cpu'][$key])) + continue; + $localMainboardExtra['cpu-' . $key] = $data['cpu'][$key]; + } + if ($data['cpu']['vmx-legacy'] ?? false) { + $globalCpuExtra['vmx-legacy'] = 1; + } + // Do the same hack with the primary NIC's speed and duplex. Even if it's not an onboard NIC, we only have one + // primary boot interface + $bootNic = $data['net']['boot0'] ?? $data['net']['eth0'] ?? null; + if ($bootNic !== null) { + $localMainboardExtra['nic-speed'] = $bootNic['speed'] ?? 0; + $localMainboardExtra['nic-duplex'] = $bootNic['duplex'] ?? 'unknown'; + } + // Finally handle mainbard data, with all of our gathered extra fields + self::updateHwTypeFromDmi($uuid, $data, 2, HardwareInfo::MAINBOARD, [], + [], + ['Manufacturer', 'Product Name', 'Type', 'Version'], // Global props, don't change + ['Serial Number', 'Asset Tag', 'Location In Chassis'], + $globalMainboardExtra, $localMainboardExtra + ); + // System information, mostly set by OEMs, might be empty/bogus on custom systems + self::updateHwTypeFromDmi($uuid, $data, 1, HardwareInfo::DMI_SYSTEM, ['Manufacturer', 'Product Name'], + [], + ['Manufacturer', 'Product Name', 'Version', 'Wake-up Type'], // Global props, don't change + ['Serial Number', 'UUID', 'SKU Number'] + ); + // Might contain more or less accurate information. Mostly works on servers and OEM systems + self::updateHwTypeFromDmi($uuid, $data, 39, HardwareInfo::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'], // Global props, don't change + ['Serial Number', 'Asset Tag', 'Status', 'Plugged', 'Hot Replaceable'] + ); + // On some more recent systems this contains quite some useful information + self::updateHwTypeFromDmi($uuid, $data, 4, HardwareInfo::CPU, ['Version'], + ['Socket Designation'], + ['Type', 'Family', 'Manufacturer', 'Signature', 'Version', 'Core Count', 'Thread Count'], // Global props, don't change + ['Voltage', 'Current Speed', 'Upgrade', 'Core Enabled'], + $globalCpuExtra); + // Information about system slots + self::updateHwTypeFromDmi($uuid, $data, 9, HardwareInfo::SYSTEM_SLOT, + function (array &$entry): bool { + // Use a callback filter to extract PCIe slot metadata into unique fields + if (!isset($entry['Type'])) + return false; + // Split up PCIe info – gen, electrical width and physical width are mashed into one field + 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'], // Global props, don't change + ['Current Usage', 'Designation'] + ); + // dmidecode end + // ---- lspci ------------------------------------ + $pciHwIds = []; + foreach (($data['lspci'] ?? []) as $dev) { + // $props is global props, don't change or the ID will change + $props = self::propsFromArray($dev, 'vendor', 'device', 'rev', 'class'); + if (!isset($props['vendor']) || !isset($props['device'])) + continue; + $hwid = self::writeGlobalHardwareData(HardwareInfo::PCI_DEVICE, $props); + $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['slot'] ?? 'unknown', + self::propsFromArray($dev, 'slot', 'subsystem', 'subsystem_vendor', 'iommu_group')); + $pciHwIds[] = $mappingId; + } + self::markDisconnected($uuid, HardwareInfo::PCI_DEVICE, $pciHwIds); + // ---- Disks ------------------------------------ + $excludedHddHwIds = []; + // Sum of all ID44/45 partitions in bytes + $id44 = $id45 = 0; + foreach (($data['drives'] ?? []) as $dev) { + if (($dev['type'] ?? 'drive') !== 'drive') + continue; // TODO: Handle CD/DVD drives? Still relevant? + if (empty($dev['readlink'])) // This is the canonical entry name directly under /dev/, e.g. /dev/sda + continue; + // Use smartctl as the source of truth, lsblk as fallback if data is missing + if (!isset($dev['smartctl']) || !is_array($dev['smartctl'])) { + $smart = []; + } else { + $smart =& $dev['smartctl']; + } + if (!isset($dev['lsblk']['blockdevices'][0]) || !is_array($dev['lsblk']['blockdevices'][0])) { + $lsblk = []; + } else { + $lsblk =& $dev['lsblk']['blockdevices'][0]; + } + if (!isset($smart['rotation_rate']) && isset($lsblk['rota']) && !$lsblk['rota']) { + // smartctl didn't report on it, lsblk says it's non-rotational + $smart['rotation_rate'] = 0; + } + $size = $lsblk['size'] ?? $smart['user_capacity']['bytes'] ?? -1; + // Don't change the global props, it would change the HW ID + $hwid = self::writeGlobalHardwareData(HardwareInfo::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' => $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', 'model_family')); + // Mangle smart attribute table + // TODO: Handle used endurance indicator for (SATA) 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']; + } + } + if (isset($smart['nvme_smart_health_information_log']) + && is_array($smart['nvme_smart_health_information_log'])) { + $table += array_filter($smart['nvme_smart_health_information_log'], function ($v, $k) { + return !is_array($v) && $k !== 'temperature' && $k !== 'temperature_sensors'; + }, ARRAY_FILTER_USE_BOTH); + } + // Partitions + $used = 0; + 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) { + if (!isset($part['size'])) + continue; + if ($table['partition_table'] === 'dos') { + $type = hexdec($part['type'] ?? '0'); + if ($type === 0x0 || $type === 0x5 || $type === 0xf || $type === 0x15 || $type === 0x1f + || $type === 0x85 || $type === 0xc5 || $type == 0xcf) { + // Extended partition, ignore + continue; + } + } + $used += $part['size'] * $fac; + if (isset($part['node']) && preg_match('/-part(\d+)$/', $part['node'], $out)) { + $id = 'part_' . $out[1] . '_'; + } else { + $id = 'part_' . ($i + 1) . '_'; + } + 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'; + $id44 += $part['size'] * $fac; + } elseif ($type == '45' || strtolower($type) === '87f86132-ff94-4987-b250-454545454545' + || $name === 'OpenSLX-ID45') { + $table[$id . 'slxtype'] = '45'; + $id45 += $part['size'] * $fac; + } + // + ++$i; + } + } + $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', + 'power_on_time//hours'); + $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['readlink'], + $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), + ]); + $excludedHddHwIds[] = $mappingId; + unset($smart, $lsblk); + } // End loop over disks + self::markDisconnected($uuid, HardwareInfo::HDD, $excludedHddHwIds); + // + // Mark parse date + $params = [ + 'uuid' => $uuid, + 'id44mb' => round($id44 / (1024 * 1024)), + 'id45mb' => round($id45 / (1024 * 1024)), + ]; + Database::exec("UPDATE machine SET dataparsetime = UNIX_TIMESTAMP(), id44mb = :id44mb, id45mb = :id45mb + WHERE machineuuid = :uuid", $params); + return $params; + } + + /** + * Unify different variants of manufacturer names + */ + private static function fixManufacturer(string $in): string + { + $in = self::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 'crucial'; + case 'crucial technology': + return 'Crucial'; + case 'dell inc.': + return 'Dell'; + case 'fujitsu': + case 'fujitsu client computing limited': + return 'Fujitsu'; + case 'hewlett packard': + case 'hewlett-packard': + return 'HP'; + case 'genuineintel': + case 'intel corporation': + case 'intel(r) corp.': + case 'intel(r) corporation': + return 'Intel'; + case 'micron technology': + return 'Micron'; + case 'ramaxel technology': + return 'Ramaxel'; + case 'samsung sdi': + return 'Samsung'; + case 'hynix semiconduc': + case 'hynix/hyundai': + case 'hyundai electronics hynix semiconductor inc': + case 'hynix semiconductor inc sk hynix': + return 'SK Hynix'; + } + return $in; + } + + /** + * Takes key-value-array, returns a new array with only the keys listed in $fields. + * Checks if the given key is not an array. If it's an array, it will be ignored. + * Supports nested arrays. Nested keys are separated by '//', so to query + * $array['x']['y'], add 'x//y' to $fields. The value will be added to the return + * value as key 'x//y'. + */ + 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; + } + + /** + * Extract data from dmi/smbios and write to DB. + * This is a pretty involved function that does several things, among them is splitting + * up the data by global and local properties, create new hardware entry if new, and + * making sure other hardware of same type gets marked as disconnected from given client. + * @param string $uuid client uuid + * @param array $data dmidecode part of hardware info + * @param int $type dmi type to extract from $data + * @param string $dbType hardware type to write this to DB as + * @param array|callable $requiredPropsOrCallback either a list of properties that are + * mandatory for this hwtype, or a callback function that returns true/false for + * valid/invalid dmi entries + * @param array $pathFields fields from entry that define the path or location of the + * hardware in the client + * @param array $globalProps properties of entry that are considered the same for all + * instances of that hardware, e.g. model name + * @param array $localProps properties of entry that are considered different for each + * instance of this hardware per client, e.g. serial number, power-on hours + * @param array $globalExtra additional key-value-pairs to write to DB as being global + * @param array $localExtra additional key-value-pairs to write to DB as being local + * @return int number of table entries written to DB, i.e. passed $requiredPropsOrCallback + */ + 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); + if (empty($sections) && !empty($globalExtra) || !empty($localExtra)) { + // Section not found, but as we want to store additional artificial columms, + // just create one empty fake section so the loop will be executed + $sections = [[]]; + } + $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); + } + +} diff --git a/modules-available/statistics/inc/hardwareparserlegacy.inc.php b/modules-available/statistics/inc/hardwareparserlegacy.inc.php new file mode 100644 index 00000000..a6ac6d5e --- /dev/null +++ b/modules-available/statistics/inc/hardwareparserlegacy.inc.php @@ -0,0 +1,285 @@ +<?php + +class HardwareParserLegacy +{ + + public static function parseHdd(&$row, $data) + { + $hdds = []; + // Could have more than one disk - linear scan + $lines = preg_split("/[\r\n]+/", $data); + $i = 0; + $mbrToByteFactor = $sectorToByteFactor = 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 (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' => 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*)?(?<class>[a-f0-9]{4})"\s+"(?<vendor>[a-f0-9]{4})"\s+"(?<device>[a-f0-9]{4})".*(:?-r(?<rev>[0-9a-f]+))?/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*(?<id>\d+)\s+\S+ # flags + \s+\S+\s+(?<v>\d+) + \s+(?<w>\d+) + \s+(?<t>\S+)\s+\S+ # fail + \s+(?<raw>\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'] += (int)$out[1]; + } + if ($ramOk && preg_match('/^\s*Maximum Capacity:\s+(\d.+)/i', $line, $out)) { + /** @var array{"Memory Slot Count": int} $row */ + $row['Memory Maximum Capacity'] += (int)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/hardwarequery.inc.php b/modules-available/statistics/inc/hardwarequery.inc.php new file mode 100644 index 00000000..6b1b5043 --- /dev/null +++ b/modules-available/statistics/inc/hardwarequery.inc.php @@ -0,0 +1,169 @@ +<?php + +class HardwareQuery +{ + + private $id = 0; + private $joins = []; + private $where = []; + private $args = []; + private $columns = []; + + /** + * @param string $type hardware type form HardwareInfo + * @param ?string $uuid If set, only return data for specific client + */ + public function __construct(string $type, string $uuid = null, $connectedOnly = true) + { + if ($connectedOnly) { + $this->joins['mxhw_join'] = "INNER JOIN machine_x_hw mxhw ON (mxhw.hwid = shw.hwid AND mxhw.disconnecttime = 0)"; + } else { + $this->joins['mxhw_join'] = "INNER JOIN machine_x_hw mxhw ON (mxhw.hwid = shw.hwid)"; + } + if ($uuid !== null) { + $this->where[] = 'mxhw.machineuuid = :uuid'; + $this->args['uuid'] = $uuid; + } + $this->where[] = 'shw.hwtype = :hwtype'; + $this->args['hwtype'] = $type; + } + + private function id(): string + { + return 'b' . (++$this->id); + } + + /** + * Add join of a virtual column (hw property) to an arbitrary table and column. + * @param bool $global Is the virtual column global or local to machine? + * @param string $prop Name of property/virtual column + * @param string $jTable Table to join on + * @param string $jColumn Column to join on + * @param string $condColumn optionally, another column from the joined table to match against $condVal + * @param string|array $condVal optionally, a literal, or array of literals, to match foreign column against + * @return void + */ + public function addForeignJoin(bool $global, string $prop, string $jTable, string $jColumn, string $condColumn = '', $condVal = null) + { + if (isset($this->columns["$jTable.$prop"])) + return; + if ($global) { + $srcTable = 'shw'; + $table = 'statistic_hw_prop'; + $column = 'hwid'; + } else { + $srcTable = 'mxhw'; + $table = 'machine_x_hw_prop'; + $column = 'machinehwid'; + } + $tid = $this->id(); + $pid = $this->id(); + $this->joins[$prop] = "INNER JOIN $table $tid ON ($srcTable.$column = $tid.$column + AND $tid.prop = :$pid)"; + $this->args[$pid] = $prop; + $this->columns[$prop] = "$tid.`value` AS `$prop`"; + $jtid = $this->id(); + $cond = ''; + if (!empty($condColumn)) { + $vid = $this->id(); + if (is_array($condVal)) { + $cond = " AND $jtid.`$condColumn` IN (:$vid)"; + } else { + $cond = " AND $jtid.`$condColumn` = :$vid"; + } + $this->args[$vid] = $condVal; + } + $this->joins[$jTable] = "INNER JOIN $jTable $jtid ON ($jtid.$jColumn = $tid.`value` $cond)"; + } + + public function addMachineWhere(string $column, string $op, $value) + { + if (isset($this->columns[$column])) + return; + $vid = $this->id(); + $this->joins['machine'] = 'INNER JOIN machine m USING (machineuuid)'; + $this->where[] = "m.$column $op (:$vid)"; + $this->args[$vid] = $value; + $this->columns[$column] = "m.$column"; + } + + public function addGlobalColumn(string $prop): HardwareQueryColumn + { + return $this->addColumn(true, $prop); + } + + public function addLocalColumn(string $prop): HardwareQueryColumn + { + return $this->addColumn(false, $prop); + } + + public function addColumn(bool $global, string $prop, string $alias = null): HardwareQueryColumn + { + return $this->columns[] = new HardwareQueryColumn($global, $prop, $alias); + } + + /** + * Join the machine table and add the given column from it to the SELECT + */ + public function addMachineColumn(string $column): void + { + if (isset($this->columns[$column])) + return; + $this->joins['machine'] = 'INNER JOIN machine m USING (machineuuid)'; + $this->columns[$column] = "m.$column"; + } + + /** + * @return false|PDOStatement + */ + public function query($groupBy = '') + { + return Database::simpleQuery($this->buildQuery($groupBy), $this->args); + } + + /** + * Build query string + * @param string[]|string $groupBy Column to group by + */ + public function buildQuery($groupBy = ''): string + { + if (empty($groupBy)) { + $groupBy = []; + } elseif (!is_array($groupBy)) { + $groupBy = [$groupBy]; + } + foreach ($groupBy as &$gb) { + if ($gb[0] !== '`') { + $gb = "`$gb`"; + } + } + $columns = []; + foreach ($this->columns as $column) { + if ($column instanceof HardwareQueryColumn) { + $column->generate($this->joins, $columns, $this->args, $groupBy); + } else { + $columns[] = $column; + } + } + $columns[] = 'mxhw.machineuuid'; + $columns[] = 'shw.hwid'; + // TODO: Untangle this implicit magic + if (empty($groupBy) || $groupBy[0] === 'mxhw.machinehwid') { + $columns[] = 'mxhw.disconnecttime'; + } else { + $columns[] = 'Sum(If(mxhw.disconnecttime = 0, 1, 0)) AS connected_count'; + } + if (!empty($groupBy)) { + $columns[] = 'Count(*) AS group_count'; + $groupBy = " GROUP BY " . implode(', ', $groupBy); + } else { + $groupBy = ''; + } + return 'SELECT ' . implode(', ', $columns) + . ' FROM statistic_hw shw ' + . implode(' ', $this->joins) + . ' WHERE ' . implode(' AND ', $this->where) + . $groupBy; + } + +} diff --git a/modules-available/statistics/inc/hardwarequerycolumn.inc.php b/modules-available/statistics/inc/hardwarequerycolumn.inc.php new file mode 100644 index 00000000..01e32978 --- /dev/null +++ b/modules-available/statistics/inc/hardwarequerycolumn.inc.php @@ -0,0 +1,94 @@ +<?php + +class HardwareQueryColumn +{ + /** @var int For unique table names in join */ + private static $id = 0; + + private $global; + private $tableAlias; + private $virtualColumnName; + private $alias; + private $conditions = []; + private $params = []; + private $classId; + + private static function getId(): string + { + return 't' . ++self::$id; + } + + public function __construct(bool $global, string $column, string $alias = null) + { + $this->classId = ++self::$id; + $this->global = $global; + $this->tableAlias = self::getId(); + $this->virtualColumnName = $column; + $this->alias = '`' . ($alias ?? $column) . '`'; + } + + /** + * Add necessary conditions, joins, columns to final SQL arrays. To be called + * from HardwareQuery::buildQuery(). + * @param string[] $groupConcat if column name is NOT in this array, add as distinct GROUP_CONCAT to column. + */ + public function generate(array &$joins, array &$columns, array &$params, array $groupConcat = [], string $globalSrcTableAlias = null) + { + if ($this->global) { + $srcTable = $globalSrcTableAlias ?? 'shw'; + $table = 'statistic_hw_prop'; + $column = 'hwid'; + } else { + $srcTable = 'mxhw'; + $table = 'machine_x_hw_prop'; + $column = 'machinehwid'; + } + $tid = $this->tableAlias; + $pid = self::getId(); + $this->conditions[] = "$srcTable.$column = $tid.$column AND $tid.prop = :$pid"; + $params[$pid] = $this->virtualColumnName; // value of property column is our virtual column + // If we have just one condition, it's the join condition itself. Since we pretend we're just adding + // a column to the query, do a left join, so the "column" is NULL if the join doesn't match. + // If however any conditions were added to this class via the addCondition() method, do a regular + // INNER JOIN, so the result will be empty if the condition doesn't match. + $type = count($this->conditions) === 1 ? 'LEFT' : 'INNER'; + $joins[] = "$type JOIN $table $tid ON (" . implode(' AND ', $this->conditions) . ")"; + if (!empty($groupConcat) && !in_array($this->alias, $groupConcat)) { + $columns[] = "Group_Concat(DISTINCT $tid.`value` SEPARATOR ', ') AS {$this->alias}"; + } else { + $columns[] = "$tid.`value` AS {$this->alias}"; + } + $params += $this->params; + } + + /** + * @param string $op Operator (<>=, IN, LIKE) + * @param string|string[]|HardwareQueryColumn $other value to compare with. + * Can be a literal, an array (if opererator is IN), or another Column + * @return void + */ + public function addCondition(string $op, $other) + { + $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value'; + if ($other instanceof HardwareQueryColumn) { + $cond = "{$this->tableAlias}.`$valueCol` $op {$other->tableAlias}.`$valueCol`"; + // Don't reference a column of a table that hasn't been joined yet + if ($this->classId > $other->classId) { + $this->conditions[] = $cond; + } else { + $other->conditions[] = $cond; + } + } elseif ($op === '~' || $op === '!~') { + $op = $op === '~' ? 'LIKE' : 'NOT LIKE'; + $other = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $other); + $pid = self::getId(); + $this->conditions[] = "{$this->tableAlias}.`$valueCol` $op (:$pid) ESCAPE '='"; + $this->params[$pid] = $other; + } else { + $pid = self::getId(); + $this->conditions[] = "{$this->tableAlias}.`$valueCol` $op (:$pid)"; + $this->params[$pid] = $other; + } + } + +} diff --git a/modules-available/statistics/inc/parser.inc.php b/modules-available/statistics/inc/parser.inc.php deleted file mode 100644 index bdf021a6..00000000 --- a/modules-available/statistics/inc/parser.inc.php +++ /dev/null @@ -1,410 +0,0 @@ -<?php - -class Parser { - 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]; - - /** - * Convert/format size unit. Input string can be a size like - * 8 GB or 1024 MB and will be converted according to passed parameters. - * @param string $string Input string - * @param string $scale 'a' for auto, T/G/M/K for according units - * @param bool $appendUnit append unit string, e.g. 'GiB' - * @return string|int Formatted result - */ - private static function convertSize($string, $scale = 'a', $appendUnit = true) - { - if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out)) - return false; - $val = (int)$out[1] * self::LOOKUP[strtoupper($out[2])]; - if (!array_key_exists($scale, self::LOOKUP)) { - foreach (self::LOOKUP as $k => $v) { - if ($k === '' || $val / 8 >= $v || abs($val - $v) < 50) { - $scale = $k; - break; - } - } - } - $val = round($val / self::LOOKUP[$scale]); - if ($appendUnit) { - $val .= ' ' . ($scale === '' ? 'Byte' : $scale . 'iB'); // NBSP!! - } - 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*)?(?<class>[a-f0-9]{4})"\s+"(?<ven>[a-f0-9]{4})"\s+"(?<dev>[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 = Page_Statistics::getPciId('CLASS', $class); - if ($res === false || $res['dateline'] < $NOW) { - $pci[$entry['class']]['lookupClass'] = 'do-lookup'; - $pci[$entry['class']]['class'] = $class; - } else { - $pci[$entry['class']]['class'] = $res['value']; - } - } - $new = array( - 'ven' => $entry['ven'], - 'dev' => $entry['ven'] . ':' . $entry['dev'], - ); - $res = Page_Statistics::getPciId('VENDOR', $new['ven']); - if ($res === false || $res['dateline'] < $NOW) { - $new['lookupVen'] = 'do-lookup'; - } else { - $new['ven'] = $res['value']; - } - $res = Page_Statistics::getPciId('DEVICE', $new['dev']); - if ($res === false || $res['dateline'] < $NOW) { - $new['lookupDev'] = 'do-lookup'; - } else { - $new['dev'] = $res['value'] . ' (' . $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'; - } - } - } - - public static function decodeJedec($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)) { - preg_match_all('/[0-9a-f]{2}/i', $out[1], $out); - $bank = 0; - foreach ($out[0] as $id) { - $bank++; - $id = hexdec($id) & 0x7f; // Let's just ignore the parity bit, and any potential error - if ($id !== 0x7f) - break; - } - if ($id !== 0) { - static $data = false; - if ($data === false) $data = json_decode(file_get_contents(dirname(__FILE__) . '/jedec.json'), true); - if (array_key_exists('bank' . $bank, $data) && array_key_exists('id' . $id, $data['bank' . $bank])) - return $data['bank' . $bank]['id' . $id]; - } - } - return $string; - } - -} diff --git a/modules-available/statistics/inc/pciid.inc.php b/modules-available/statistics/inc/pciid.inc.php new file mode 100644 index 00000000..38a2c56d --- /dev/null +++ b/modules-available/statistics/inc/pciid.inc.php @@ -0,0 +1,82 @@ +<?php + +class PciId +{ + + const DEVICE = 'DEVICE'; + const VENDOR = 'VENDOR'; + const DEVCLASS = 'CLASS'; + const AUTO = 'AUTO'; + + + /** + * @param string $cat type of query - self::DEVICE, self::VENDOR, self::DEVCLASS or self::AUTO for auto detection + * @param string $id the id to query - depends on $cat + * @return string|false Name of Class/Vendor/Device, false if not found + */ + public static function getPciId(string $cat, string $id, bool $dnsQuery = false) + { + static $cache = []; + 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)) { + $cat = 'DEVICE'; + $host = $out[2] . '.' . $out[1]; + $id = $out[1] . ':' . $out[2]; + } elseif (preg_match('/^[a-f0-9]{4}$/', $id)) { + $cat = 'VENDOR'; + $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])) + return $cache[$key]; + $row = Database::queryFirst('SELECT value, dateline FROM pciid WHERE category = :cat AND id = :id LIMIT 1', + array('cat' => $cat, 'id' => $id)); + if ($row !== false && $row['dateline'] >= time()) { + return $cache[$key] = $row['value']; + } + if (!$dnsQuery) + return false; + // Unknown, query + $res = dns_get_record($host . '.pci.id.ucw.cz', DNS_TXT); + if (!is_array($res)) + return false; + foreach ($res as $entry) { + if (isset($entry['txt']) && substr($entry['txt'], 0, 2) === 'i=') { + $string = substr($entry['txt'], 2); + Database::exec('INSERT INTO pciid (category, id, value, dateline) VALUES (:cat, :id, :value, :timeout)' + . ' ON DUPLICATE KEY UPDATE value = VALUES(value), dateline = VALUES(dateline)', + array( + 'cat' => $cat, + 'id' => $id, + 'value' => $string, + 'timeout' => time() + mt_rand(10, 30) * 86400, + ), true); + return $cache[$key] = $string; + } + } + return $cache[$key] = ($row['value'] ?? false); + } + +}
\ No newline at end of file diff --git a/modules-available/statistics/inc/statistics.inc.php b/modules-available/statistics/inc/statistics.inc.php index 1f8a081a..c12f5be4 100644 --- a/modules-available/statistics/inc/statistics.inc.php +++ b/modules-available/statistics/inc/statistics.inc.php @@ -7,7 +7,7 @@ class Statistics private static $machineFields = false; - private static function initFields($returnData) + private static function initFields(int $returnData): string { if (self::$machineFields === false) { $r = new ReflectionClass('Machine'); @@ -19,23 +19,21 @@ class Statistics } elseif ($returnData === Machine::RAW_DATA) { self::$machineFields['data'] = true; } else { - Util::traceError('Invalid $returnData option passed'); + ErrorHandler::traceError('Invalid $returnData option passed'); } return implode(',', array_keys(self::$machineFields)); } /** - * @param string $machineuuid * @param int $returnData What kind of data to return Machine::NO_DATA, Machine::RAW_DATA, ... - * @return \Machine|false */ - public static function getMachine($machineuuid, $returnData) + public static function getMachine(string $machineuuid, int $returnData): ?Machine { $fields = self::initFields($returnData); $row = Database::queryFirst("SELECT $fields FROM machine WHERE machineuuid = :machineuuid", compact('machineuuid')); if ($row === false) - return false; + return null; $m = new Machine(); foreach ($row as $key => $val) { $m->{$key} = $val; @@ -44,23 +42,22 @@ class Statistics } /** - * @param string $ip * @param int $returnData What kind of data to return Machine::NO_DATA, Machine::RAW_DATA, ... * @param string $sort something like 'lastseen ASC' - not sanitized, don't pass user input! - * @return \Machine[] list of matches + * @return Machine[] list of matches */ - public static function getMachinesByIp($ip, $returnData, $sort = false) + public static function getMachinesByIp(string $ip, int $returnData, string $sort = null): array { $fields = self::initFields($returnData); - if ($sort === false) { + if ($sort === null) { $sort = ''; } else { $sort = "ORDER BY $sort"; } $res = Database::simpleQuery("SELECT $fields FROM machine WHERE clientip = :ip $sort", compact('ip')); $list = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $m = new Machine(); foreach ($row as $key => $val) { $m->{$key} = $val; @@ -74,7 +71,7 @@ class Statistics const OFFLINE_LENGTH = '~offline-length'; const SUSPEND_LENGTH = '~suspend-length'; - public static function logMachineState($uuid, $ip, $type, $start, $length, $username = '') + public static function logMachineState(string $uuid, string $ip, string $type, int $start, int $length, string $username = ''): int { return Database::exec('INSERT INTO statistic (dateline, typeid, machineuuid, clientip, username, data)' . " VALUES (:start, :type, :uuid, :clientip, :username, :length)", array( diff --git a/modules-available/statistics/inc/statisticsfilter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php index 4a4899e2..5e6448c7 100644 --- a/modules-available/statistics/inc/statisticsfilter.inc.php +++ b/modules-available/statistics/inc/statisticsfilter.inc.php @@ -10,8 +10,10 @@ abstract class StatisticsFilter */ const LEGACY_DELIMITER = '~,~'; - const SIZE_ID44 = array(0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 2000, 4000); - const SIZE_RAM = array(1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024); + const SIZE_PARTITION = [0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 1500, 2000, 3000, + 4000, 6000, 8000, 10000]; + const SIZE_RAM = [1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024, 1536, + 2048]; private static $keyCounter = 0; @@ -56,22 +58,57 @@ abstract class StatisticsFilter $this->placeholder = $placeholder; } - public function type() + public function type(): string { return ($this->ops === self::OP_ORDINAL || $this->ops === self::OP_FUZZY_ORDINAL) ? 'int' : 'string'; } - /* returns a where clause and adds needed operators to the passed arrays */ - public abstract function whereClause(string $operator, $argument, array &$args, array &$joins); + /** + * Needed for joins with the hardware tables, to use the HardwareQueryColumn afterwards. + * The HardwareQuery class should probably be extended/rewritten to be more versatile in + * this regard. + */ + public static function addHardwareJoin(array &$args, array &$joins, string $hwtype = null): string + { + $joins['mxhw'] = ' INNER JOIN machine_x_hw mxhw ON (mxhw.disconnecttime = 0 AND mxhw.machineuuid = m.machineuuid)'; + $key = self::getNewKey('foo'); + $shw = self::getNewKey('shw'); + if ($hwtype === null) { + $joins[] = " INNER JOIN statistic_hw $shw ON (mxhw.hwid = {$shw}.hwid)"; + } else { + $joins[] = " INNER JOIN statistic_hw $shw ON (mxhw.hwid = {$shw}.hwid AND {$shw}.hwtype = :$key)"; + $args[$key] = $hwtype; + } + return $shw; + } - public function bind(string $op, $argument) { return new DatabaseFilter($this, $op, $argument); } + /** + * To be called by DatabaseFilter::whereClause() when building actual query. + * @param string $operator operator to use + * @param string[]|string $argument argument to compare against + * @param string[] $args assoc array to add parametrized version of $argument to + * @param string[] $joins any optional joins can be added to this array + * @return string where clause + */ + public abstract function whereClause(string $operator, $argument, array &$args, array &$joins): string; + /** + * Called to get an instance of DatabaseFilter that binds the given $op and $argument to this filter. + * @param string[]|string $argument + */ + public function bind(string $op, $argument): DatabaseFilter { return new DatabaseFilter($this, $op, $argument); } + + /** + * Check if given $operator is valid for this filter. Throws error and halts if not. + * @return void + */ public final function validateOperator(string $operator) { if (empty($this->ops)) return; if (!in_array($operator, $this->ops)) { - Util::traceError("Invalid op '$operator' for " . get_class($this) . '::' . $this->column); + // Yes keep $this in this call, get_class() !== get_class($this) + ErrorHandler::traceError("Invalid op '$operator' for " . get_class($this) . '::' . $this->column); } } @@ -100,7 +137,7 @@ abstract class StatisticsFilter return ($array[$best] + $array[$best - 1]) / 2; } - public static function getNewKey($colname) + public static function getNewKey($colname): string { return $colname . '_' . (self::$keyCounter++); } @@ -108,7 +145,7 @@ abstract class StatisticsFilter /** * @return DatabaseFilter[] */ - public static function parseQuery() + public static function parseQuery(): array { // Get current settings from GET $ops = Request::get('op', [], 'array'); @@ -141,10 +178,7 @@ abstract class StatisticsFilter return $filters; } - /** - * @param \StatisticsFilterSet $filterSet - */ - public static function renderFilterBox($show, $filterSet) + public static function renderFilterBox(string $show, StatisticsFilterSet $filterSet): void { // Build location list, with permissions if (Module::isAvailable('locations')) { @@ -156,7 +190,7 @@ abstract class StatisticsFilter foreach (self::$columns as $key => $filter) { $col = [ 'key' => $key, - 'name' => Dictionary::translateFile('filters', $key, true), + 'name' => Dictionary::translateFile('filters', $key), 'placeholder' => $filter->placeholder, ]; $bind = $filterSet->hasFilterKey($key); @@ -169,8 +203,9 @@ abstract class StatisticsFilter $col['inputclass'] = 'is-date'; } elseif ($filter->type() === 'enum') { $col['enum'] = true; + /** @var EnumStatisticsFilter $filter */ $col['values'] = $filter->values; - if ($bind !== false) { + if ($bind !== null) { // Current value from GET foreach ($col['values'] as &$value) { if ($value['key'] == $bind->argument) { @@ -180,7 +215,7 @@ abstract class StatisticsFilter } } // current value from GET - if ($bind !== false) { + if ($bind !== null) { $col['currentvalue'] = $bind->argument; $col['checked'] = 'checked'; $showCount++; @@ -190,7 +225,7 @@ abstract class StatisticsFilter $col['op'] = $filter->ops; foreach ($col['op'] as &$value) { $value = ['op' => $value]; - if ($bind !== false && $bind->op === $value['op']) { + if ($bind !== null && $bind->op === $value['op']) { $value['selected'] = 'selected'; } } @@ -218,16 +253,16 @@ abstract class StatisticsFilter 'clientip' => new IpStatisticsFilter(), 'hostname' => new SimpleStatisticsFilter('hostname', self::OP_STRCMP, 'pc.fqdn.example.com'), 'machineuuid' => new SimpleStatisticsFilter('machineuuid', self::OP_STRCMP, '88888888-4444-4444-121212121212'), - 'macaddr' => new SimpleStatisticsFilter('macaddr', self::OP_STRCMP, '11-22-33-44-55-66'), + 'macaddr' => new MacAddressStatisticsFilter(), 'firstseen' => new DateStatisticsFilter('firstseen', '2020-10-15 14:00'), 'lastseen' => new DateStatisticsFilter('lastseen', '2020-10-15 14:00'), - 'logintime' => new DateStatisticsFilter('logintime', '2020-10-15 14:00'), 'lastboot' => new DateStatisticsFilter('lastboot', '2020-10-15 14:00'), 'runtime' => new RuntimeStatisticsFilter(), 'realcores' => new SimpleStatisticsFilter('realcores', self::OP_ORDINAL, ''), - 'systemmodel' => new SimpleStatisticsFilter('systemmodel', self::OP_STRCMP, 'PC-365 (IBM)'), + 'systemmodel' => new SystemModelStatisticsFilter(), 'cpumodel' => new SimpleStatisticsFilter('cpumodel', self::OP_STRCMP, 'Pentium Pro 200 MHz'), - 'hddgb' => new Id44GbStatisticsFilter(), + 'hddgb' => new PartitionGbStatisticsFilter('id44mb'), + 'persistentgb' => new PartitionGbStatisticsFilter('id45mb'), 'gbram' => new RamGbStatisticsFilter(), 'kvmstate' => new EnumStatisticsFilter('kvmstate', ['ENABLED', 'DISABLED', 'UNSUPPORTED']), 'badsectors' => new SimpleStatisticsFilter('badsectors', self::OP_ORDINAL, ''), @@ -236,6 +271,12 @@ abstract class StatisticsFilter 'live_swapfree' => new SimpleStatisticsFilter('live_swapfree', self::OP_ORDINAL, 'MiB'), 'live_memfree' => new SimpleStatisticsFilter('live_memfree', self::OP_ORDINAL, 'MiB'), 'live_tmpfree' => new SimpleStatisticsFilter('live_tmpfree', self::OP_ORDINAL, 'MiB'), + 'live_id45free' => new SimpleNotZeroStatisticsFilter('live_id45free', self::OP_ORDINAL, 'MiB'), + 'standbycrash' => new StandbyCrashStatisticsFilter(), + 'pcidev' => new PciDeviceStatisticsFilter(), + 'nicspeed' => new NicSpeedStatisticsFilter(), + 'hddrpm' => new HddRpmStatisticsFilter(), + //'anydev' => new AnyHardwarePropStatisticsFilter(), ]; if (Module::isAvailable('locations')) { self::$columns['location'] = new LocationStatisticsFilter(); @@ -247,14 +288,14 @@ abstract class StatisticsFilter class SimpleStatisticsFilter extends StatisticsFilter { - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { $addendum = ''; $key = self::getNewKey($this->column); $args[$key] = $argument; if (is_array($argument)) { - if ($operator{0} === '!') { + if ($operator[0] === '!') { $op = 'NOT IN'; } else { $op = 'IN'; @@ -277,6 +318,20 @@ class SimpleStatisticsFilter extends StatisticsFilter } +class SimpleNotZeroStatisticsFilter extends SimpleStatisticsFilter +{ + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + $str = parent::whereClause($operator, $argument, $args, $joins); + if ((int)$argument !== 0 || $operator !== '=') { + $str = "($str AND {$this->column} != 0)"; + } + return $str; + } + +} + class EnumStatisticsFilter extends SimpleStatisticsFilter { @@ -301,28 +356,55 @@ class EnumStatisticsFilter extends SimpleStatisticsFilter $this->values = $values; } - public function type() { return 'enum'; } + public function type(): string { return 'enum'; } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { - $keys = ArrayUtil::flattenByKey($this->values, 'key'); - if (is_array($argument)) { - $ok = true; - foreach ($argument as $e) { - if (!in_array($e, $keys)) { - $ok = false; + if ($this->validateArgument()) { + $keys = ArrayUtil::flattenByKey($this->values, 'key'); + if (is_array($argument)) { + $ok = true; + foreach ($argument as $e) { + if (!in_array($e, $keys)) { + $ok = false; + } } + } else { + $ok = in_array($argument, $keys); + } + if (!$ok) { + Message::addError('invalid-enum-item', $this->column, $argument); + return '0'; } - } else { - $ok = in_array($argument, $keys); } - if (!$ok) { - Message::addError('invalid-enum-item', $this->column, $argument); - return '0'; + return parent::whereClause($operator, $argument, $args, $joins); + } + + protected function validateArgument(): bool { return true; } + +} + +class StandbyCrashStatisticsFilter extends EnumStatisticsFilter +{ + + public function __construct() + { + parent::__construct('standbysem', ['NONE', 'MANY']); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + if ($argument === 'NONE') { + $argument = 0; + } else { // MANY + $argument = 3; + $operator = $operator === '=' ? '>' : '<='; } return parent::whereClause($operator, $argument, $args, $joins); } + protected function validateArgument(): bool { return false; } + } class DateStatisticsFilter extends StatisticsFilter @@ -333,12 +415,11 @@ class DateStatisticsFilter extends StatisticsFilter parent::__construct($column, self::OP_ORDINAL, $placeholder); } - public function type() { return 'date'; } + public function type(): string { return 'date'; } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { $key = self::getNewKey($this->column); - $addendum = ''; if (!preg_match('/^(?<date>\d{4}-\d{2}-\d{2})(\s+(?<h>\d{1,2})(:(?<m>\d{2})(:\d+)?)?)?$/', $argument, $out)) { Message::addError('invalid-date-format', $argument); @@ -364,7 +445,7 @@ class DateStatisticsFilter extends StatisticsFilter $args[$key] = strtotime('+1 ' . $span . ' -1 second', $args[$key]); } - return 'm.' . $this->column . ' ' . $operator . ' :' . $key . $addendum; + return 'm.' . $this->column . ' ' . $operator . ' :' . $key; } } @@ -377,7 +458,7 @@ class RuntimeStatisticsFilter extends StatisticsFilter parent::__construct('lastboot', self::OP_ORDINAL); } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { $upper = time() - (int)$argument * 3600; $lower = $upper - 3600; @@ -401,7 +482,7 @@ class RuntimeStatisticsFilter extends StatisticsFilter abstract class GbToMbRangeStatisticsFilter extends StatisticsFilter { - protected function rangeClause(string $operator, $argument, array $fuzzyVals) + protected function rangeClause(string $operator, $argument, array $fuzzyVals): string { if ($operator === '~' || $operator === '!~') { $lower = (int)floor(StatisticsFilter::findBestValue($fuzzyVals, (int)$argument, false) * 1024 - 500); @@ -434,24 +515,24 @@ class RamGbStatisticsFilter extends GbToMbRangeStatisticsFilter parent::__construct('mbram', self::OP_FUZZY_ORDINAL, 'GiB'); } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { return parent::rangeClause($operator, $argument, self::SIZE_RAM); } } -class Id44GbStatisticsFilter extends GbToMbRangeStatisticsFilter +class PartitionGbStatisticsFilter extends GbToMbRangeStatisticsFilter { - public function __construct() + public function __construct(string $column) { - parent::__construct('id44mb', self::OP_FUZZY_ORDINAL,'GiB'); + parent::__construct($column, self::OP_FUZZY_ORDINAL, 'GiB'); } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { - return parent::rangeClause($operator, $argument, self::SIZE_ID44); + return parent::rangeClause($operator, $argument, self::SIZE_PARTITION); } } @@ -463,18 +544,17 @@ class StateStatisticsFilter extends EnumStatisticsFilter parent::__construct('state', ['on', 'off', 'idle', 'occupied', 'standby']); } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { $map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ]; - $neg = $operator == '!=' ? 'NOT ' : ''; + $neg = $operator === '!=' ? 'NOT ' : ''; if (array_key_exists($argument, $map)) { $key = StatisticsFilter::getNewKey($this->column); $args[$key] = $map[$argument]; return " m.state $neg IN ( :$key ) "; - } else { - Message::addError('invalid-filter-argument', 'state', $argument); - return ' 1'; } + Message::addError('invalid-filter-argument', 'state', $argument); + return ' 1'; } } @@ -493,15 +573,15 @@ class LocationStatisticsFilter extends EnumStatisticsFilter parent::__construct('locationid', $locs, self::OP_LOCATIONS); } - public function type() { return 'enum'; } + public function type(): string { return 'enum'; } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { $recursive = (substr($operator, -1) === '~'); $operator = str_replace('~', '=', $operator); if ($recursive && is_array($argument)) { - Util::traceError('Cannot use ~ operator for location with array'); + ErrorHandler::traceError('Cannot use ~ operator for location with array'); } if ($recursive) { $argument = array_keys(Location::getRecursiveFlat($argument)); @@ -539,21 +619,13 @@ class IpStatisticsFilter extends StatisticsFilter } elseif (strpos($argument, '/') !== false) { // TODO: IPv6 CIDR $range = IpUtil::parseCidr($argument); - if ($range === false) { + if ($range === null) { Message::addError('invalid-cidr-notion', $argument); return '0'; } return 'INET_ATON(clientip) BETWEEN ' . $range['start'] . ' AND ' . $range['end']; } elseif (($num = substr_count($argument, ':')) !== 0 && $num <= 7) { - // IPv6, not yet in DB but let's prepare - if ($num > 7 || strpos($argument, '::') !== false) { // Too many :, or invalid compressed format - Message::addError('invalid-ip-address', $argument); - return '0'; - } elseif ($num <= 7 && substr($argument, -1) === ':') { - $argument .= '*'; - } elseif ($num < 7) { - $argument .= ':*'; - } + // TODO: Probably valid IPv6, not yet in DB } elseif (($num = substr_count($argument, '.')) !== 0 && $num <= 3) { if (substr($argument, -1) === '.') { $argument .= '*'; @@ -564,7 +636,8 @@ class IpStatisticsFilter extends StatisticsFilter Message::addError('invalid-ip-address', $argument); return '0'; } - return "clientip LIKE '" . str_replace('*', '%', $argument) . "'"; + $operator = $operator[0] === '!' ? 'NOT LIKE' : 'LIKE'; + return "clientip $operator '" . str_replace('*', '%', $argument) . "'"; } } @@ -576,18 +649,170 @@ class IsClientStatisticsFilter extends StatisticsFilter parent::__construct(null, []); } - public function whereClause(string $operator, $argument, array &$args, array &$joins) + public function whereClause(string $operator, $argument, array &$args, array &$joins): string { if ($argument) { - $joins[] = ' LEFT JOIN runmode USING (machineuuid)'; + $joins[] = ' LEFT JOIN runmode ON (m.machineuuid = runmode.machineuuid)'; return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)"; } - $joins[] = ' INNER JOIN runmode USING (machineuuid)'; + $joins[] = ' INNER JOIN runmode ON (m.machineuuid = runmode.machineuuid)'; return "runmode.isclient = 0"; } } +class PciDeviceStatisticsFilter extends StatisticsFilter +{ + + public function __construct() + { + parent::__construct(null, ['='], 'vvvv[:dddd][,cccc]'); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + // vendor[:device][,class] + if (!preg_match('/^(?<v>[0-9a-f]{4})(?::(?<d>[0-9a-f]{4}))?(?:,(?<c>[0-9a-f]{4}))?$/i', $argument, $out)) { + Message::addError('invalid-pciid', $argument); + return '0'; + } + $vendor = $out['v']; + $device = $out['d'] ?? ''; + $class = $out['c'] ?? ''; + // basic join for hw_x_machine + $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::PCI_DEVICE); + $_ = []; + $c = new HardwareQueryColumn(true, 'vendor'); + $c->addCondition($operator, $vendor); + $c->generate($joins, $_, $args, [], $shw); + if (!empty($device)) { + $c = new HardwareQueryColumn(true, 'device'); + $c->addCondition($operator, $device); + $c->generate($joins, $_, $args, [], $shw); + } + if (!empty($class)) { + $c = new HardwareQueryColumn(true, 'class'); + $c->addCondition($operator, $class); + $c->generate($joins, $_, $args, [], $shw); + } + return '1'; + } + +} + +class NicSpeedStatisticsFilter extends StatisticsFilter +{ + + public function __construct() + { + parent::__construct(null, StatisticsFilter::OP_ORDINAL, 'MBit/s'); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::MAINBOARD); + $_ = []; + $c = new HardwareQueryColumn(false, 'nic-speed'); + $c->addCondition($operator, $argument); + $c->generate($joins, $_, $args, [], $shw); + return '1'; + } + +} + +class HddRpmStatisticsFilter extends StatisticsFilter +{ + + public function __construct() + { + parent::__construct(null, StatisticsFilter::OP_ORDINAL, '7200'); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::HDD); + $_ = []; + $c = new HardwareQueryColumn(true, 'rotation_rate'); + $c->addCondition($operator, $argument); + $c->generate($joins, $_, $args, [], $shw); + return '1'; + } + +} + +class SystemModelStatisticsFilter extends StatisticsFilter +{ + + public function __construct() + { + parent::__construct(null, StatisticsFilter::OP_STRCMP, 'PC-365 (IBM)'); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::DMI_SYSTEM); + $_ = []; + $manufacturer = null; + $model = $argument; + if (preg_match('/^(.*)\((.*)\)\s*$/', $model, $out)) { + $manufacturer = trim($out[2]); + $model = trim($out[1]); + } + $c = new HardwareQueryColumn(true, 'Product Name'); + $c->addCondition($operator, $model); + $c->generate($joins, $_, $args, [], $shw); + if ($manufacturer !== null) { + $c = new HardwareQueryColumn(true, 'Manufacturer'); + $c->addCondition($operator, $manufacturer); + $c->generate($joins, $_, $args, [], $shw); + } + return '1'; + } + +} + +class MacAddressStatisticsFilter extends SimpleStatisticsFilter +{ + public function __construct() + { + parent::__construct('macaddr', self::OP_STRCMP, '11-22-33-44-55-66'); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + // Allow just 12 hex digits, and convert ':' to '-', which we unfortunately settled on for the DB format + if (preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i', + $argument, $out)) { + $argument = $out[1] . '-' . $out[2] . '-' . $out[3] . '-' . $out[4] . '-' . $out[5] . '-' . $out[6]; + } elseif (strpos($argument, ':') !== false) { + $argument = str_replace(':', '-', $argument); + } + return parent::whereClause($operator, $argument, $args, $joins); + } +} + +class AnyHardwarePropStatisticsFilter extends StatisticsFilter +{ + + public function __construct() + { + parent::__construct(null, ['~']); + } + + public function whereClause(string $operator, $argument, array &$args, array &$joins): string + { + $shw = StatisticsFilter::addHardwareJoin($args, $joins); + $val = self::getNewKey('val'); + $key1 = self::getNewKey('hw'); + $joins[] = "LEFT JOIN statistic_hw_prop $key1 ON (`$key1`.`value` LIKE :$val AND `$key1`.hwid = `$shw`.hwid)"; + $key2 = self::getNewKey('hw'); + $joins[] = "LEFT JOIN machine_x_hw_prop $key2 ON (`$key2`.`value` LIKE :$val AND `$key2`.machinehwid = mxhw.machinehwid)"; + $args[$val] = '%' . str_replace(['%', '*'], ['_', '%'], $argument) . '%'; + return "((`$key1`.`value` IS NOT NULL) OR (`$key2`.`value` IS NOT NULL))"; + } + +} + class DatabaseFilter { /** @var StatisticsFilter @@ -595,6 +820,10 @@ class DatabaseFilter private $inst; public $op; public $argument; + + /** + * Called by StatisticsFilter::bind(). + */ public function __construct(StatisticsFilter $inst, string $op, $argument) { $inst->validateOperator($op); @@ -602,16 +831,25 @@ class DatabaseFilter $this->op = $op; $this->argument = $argument; } - public function whereClause(array &$args, array &$joins) + + /** + * Called from StatisticsFilterSet::makeFragments() to build the final query. + */ + public function whereClause(array &$args, array &$joins): string { return $this->inst->whereClause($this->op, $this->argument, $args, $joins); } - public function isClass($what) + public function isClass(string $what): bool { return get_class($this->inst) === $what; } + public function getClass(): string + { + return get_class($this->inst); + } + } StatisticsFilter::initConstants(); diff --git a/modules-available/statistics/inc/statisticsfilterset.inc.php b/modules-available/statistics/inc/statisticsfilterset.inc.php index a38f9d3f..26595e93 100644 --- a/modules-available/statistics/inc/statisticsfilterset.inc.php +++ b/modules-available/statistics/inc/statisticsfilterset.inc.php @@ -9,7 +9,10 @@ class StatisticsFilterSet private $cache = false; - public function __construct($filters) + /** + * @param DatabaseFilter[] $filters + */ + public function __construct(array $filters) { $this->filters = $filters; } @@ -37,16 +40,10 @@ class StatisticsFilterSet $join = implode(' ', array_unique($joins)); $this->cache = compact('where', 'join', 'args'); } - - public function isNoId44Filter() - { - $filter = $this->hasFilter('Id44GbStatisticsFilter'); - return $filter !== false && $filter->argument == 0; - } public function filterNonClients() { - if (Module::get('runmode') === false || $this->hasFilter('IsClientStatisticsFilter') !== false) + if (Module::get('runmode') === false || $this->hasFilter('IsClientStatisticsFilter') !== null) return; $this->cache = false; // Runmode module exists, add filter @@ -55,27 +52,27 @@ class StatisticsFilterSet /** * @param string $type filter type (class name) - * @return false|DatabaseFilter The filter, false if not found + * @return ?DatabaseFilter The filter, null if not found */ - public function hasFilter($type) + public function hasFilter(string $type): ?DatabaseFilter { foreach ($this->filters as $filter) { if ($filter->isClass($type)) { return $filter; } } - return false; + return null; } /** * @param string $type filter type key/id - * @return false|DatabaseFilter The filter, false if not found + * @return ?DatabaseFilter The filter, null if not found */ - public function hasFilterKey($type) + public function hasFilterKey(string $type): ?DatabaseFilter { if (isset($this->filters[$type])) return $this->filters[$type]; - return false; + return null; } /** @@ -85,7 +82,7 @@ class StatisticsFilterSet * @param string $permission permission to use * @return bool false if no permission for any location, true otherwise */ - public function setAllowedLocationsFromPermission($permission) + public function setAllowedLocationsFromPermission(string $permission): bool { if (!Module::isAvailable('locations')) return true; @@ -108,9 +105,35 @@ class StatisticsFilterSet */ public function getAllowedLocations() { - if (isset($this->filters['permissions']->argument) && is_array($this->filters['permissions']->argument)) - return $this->filters['permissions']->argument; + if (isset($this->filters['permissions']) && is_array($this->filters['permissions']->argument)) + return (array)$this->filters['permissions']->argument; return false; } + public function suitableForUsageGraph(): bool + { + foreach ($this->filters as $filter) { + switch ($filter->getClass()) { + case 'LocationStatisticsFilter': + case 'IsClientStatisticsFilter': + break; + case 'DateStatisticsFilter': + if ($filter->op !== '>' && $filter->op !== '>=') + return false; + if (strtotime($filter->argument) + 3*86400 > time()) + return false; + break; + case 'RuntimeStatisticsFilter': + if ($filter->op !== '>' && $filter->op !== '>=') + return false; + if ($filter->argument < 3 * 24) + return false; + break; + default: + return false; + } + } + return true; + } + } diff --git a/modules-available/statistics/inc/statisticshooks.inc.php b/modules-available/statistics/inc/statisticshooks.inc.php index 746bdabf..6b9dfa21 100644 --- a/modules-available/statistics/inc/statisticshooks.inc.php +++ b/modules-available/statistics/inc/statisticshooks.inc.php @@ -5,7 +5,7 @@ class StatisticsHooks private static $row = false; - private static function getRow($machineuuid) + private static function getRow(string $machineuuid) { if (self::$row !== false) return; @@ -13,15 +13,22 @@ class StatisticsHooks ['machineuuid' => $machineuuid]); } - public static function getBaseconfigName($machineuuid) + /** + * Hook for baseconfig. + * @return false|string Client name, or false if invalid + */ + public static function getBaseconfigName(string $machineuuid) { self::getRow($machineuuid); if (self::$row === false) return false; - return self::$row['hostname'] ? self::$row['hostname'] : self::$row['clientip']; + return self::$row['hostname'] ?: self::$row['clientip']; } - public static function baseconfigLocationResolver($machineuuid) + /** + * Hook for baseconfig. + */ + public static function baseconfigLocationResolver(string $machineuuid): int { self::getRow($machineuuid); if (self::$row === false) @@ -30,16 +37,17 @@ class StatisticsHooks } /** - * Hook to get inheritance tree for all config vars - * @param int $machineuuid MachineUUID currently being edited + * Hook to get inheritance tree for all config vars. + * + * @param string $machineuuid MachineUUID currently being edited */ - public static function baseconfigInheritance($machineuuid) + public static function baseconfigInheritance(string $machineuuid): array { self::getRow($machineuuid); if (self::$row === false) return []; BaseConfig::prepareWithOverrides([ - 'locationid' => self::$row['locationid'] + 'locationid' => self::$row['locationid'] ?? 0 ]); return ConfigHolder::getRecursiveConfig(true); } diff --git a/modules-available/statistics/inc/statisticsstyling.inc.php b/modules-available/statistics/inc/statisticsstyling.inc.php index 1fd1d326..0e158026 100644 --- a/modules-available/statistics/inc/statisticsstyling.inc.php +++ b/modules-available/statistics/inc/statisticsstyling.inc.php @@ -3,19 +3,19 @@ class StatisticsStyling { - public static function ramColorClass($mb) + public static function ramColorClass(int $mb): string { - if ($mb < 1500) { + if ($mb < 2500) { return 'danger'; } - if ($mb < 2500) { + if ($mb < 5100) { return 'warning'; } return ''; } - public static function kvmColorClass($state) + public static function kvmColorClass(string $state): string { if ($state === 'DISABLED') { return 'danger'; @@ -27,7 +27,7 @@ class StatisticsStyling return ''; } - public static function hddColorClass($gb) + public static function hddColorClass(int $gb): string { if ($gb < 7) { return 'danger'; @@ -39,4 +39,24 @@ class StatisticsStyling return ''; } + /** + * Take a machine state enum value, return a matching glyphicon class. + * @param string $state State value (OFFLINE, IDLE, ...) + */ + public static function machineStateToIcon(string $state): string + { + switch ($state) { + case 'OFFLINE': + return 'glyphicon-off'; + case 'IDLE': + return 'glyphicon-ok green'; + case 'OCCUPIED': + return 'glyphicon-user red'; + case 'STANDBY': + return 'glyphicon-off green'; + default: + return 'glyphicon-question-sign'; + } + } + }
\ No newline at end of file |