summaryrefslogtreecommitdiffstats
path: root/modules-available/statistics/inc
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/statistics/inc')
-rw-r--r--modules-available/statistics/inc/devicetype.inc.php6
-rw-r--r--modules-available/statistics/inc/hardwareinfo.inc.php249
-rw-r--r--modules-available/statistics/inc/hardwareparser.inc.php789
-rw-r--r--modules-available/statistics/inc/hardwareparserlegacy.inc.php285
-rw-r--r--modules-available/statistics/inc/hardwarequery.inc.php169
-rw-r--r--modules-available/statistics/inc/hardwarequerycolumn.inc.php94
-rw-r--r--modules-available/statistics/inc/parser.inc.php410
-rw-r--r--modules-available/statistics/inc/pciid.inc.php82
-rw-r--r--modules-available/statistics/inc/statistics.inc.php21
-rw-r--r--modules-available/statistics/inc/statisticsfilter.inc.php382
-rw-r--r--modules-available/statistics/inc/statisticsfilterset.inc.php57
-rw-r--r--modules-available/statistics/inc/statisticshooks.inc.php24
-rw-r--r--modules-available/statistics/inc/statisticsstyling.inc.php30
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