From 67e6b6c9981207d7d658f2ad2bf1c39b75c099c7 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 21 Sep 2021 16:52:06 +0200 Subject: [passthrough] New module for managing hardware passthrough for QEMU --- .../minilinux/inc/linuxbootentryhook.inc.php | 20 ++-- modules-available/passthrough/config.json | 7 ++ .../passthrough/inc/passthrough.inc.php | 53 +++++++++ modules-available/passthrough/install.inc.php | 9 ++ modules-available/passthrough/page.inc.php | 128 +++++++++++++++++++++ .../passthrough/permissions/permissions.json | 3 + .../passthrough/templates/hardware-list.html | 125 ++++++++++++++++++++ modules-available/statistics/api.inc.php | 11 +- .../statistics/inc/hardwareinfo.inc.php | 69 +++++++++++ .../statistics/inc/hardwareparser.inc.php | 83 ++++++++----- .../statistics/inc/hardwarequery.inc.php | 73 ++++++++++-- modules-available/statistics/inc/pciid.inc.php | 76 ++++++++++++ modules-available/statistics/install.inc.php | 11 +- modules-available/statistics/page.inc.php | 78 ++++--------- modules-available/statistics/pages/hints.inc.php | 77 +++++++++++++ .../templates/hints-ram-underclocked.html | 44 +++++++ .../statistics/templates/hints-ram-upgrade.html | 31 +++++ 17 files changed, 793 insertions(+), 105 deletions(-) create mode 100644 modules-available/passthrough/config.json create mode 100644 modules-available/passthrough/inc/passthrough.inc.php create mode 100644 modules-available/passthrough/install.inc.php create mode 100644 modules-available/passthrough/page.inc.php create mode 100644 modules-available/passthrough/permissions/permissions.json create mode 100644 modules-available/passthrough/templates/hardware-list.html create mode 100644 modules-available/statistics/inc/pciid.inc.php create mode 100644 modules-available/statistics/pages/hints.inc.php create mode 100644 modules-available/statistics/templates/hints-ram-underclocked.html create mode 100644 modules-available/statistics/templates/hints-ram-upgrade.html diff --git a/modules-available/minilinux/inc/linuxbootentryhook.inc.php b/modules-available/minilinux/inc/linuxbootentryhook.inc.php index e57336f0..9e10bd6e 100644 --- a/modules-available/minilinux/inc/linuxbootentryhook.inc.php +++ b/modules-available/minilinux/inc/linuxbootentryhook.inc.php @@ -15,7 +15,7 @@ class LinuxBootEntryHook extends BootEntryHook return Dictionary::translateFileModule('minilinux', 'module', 'module_name', true); } - public function extraFields() + public function extraFields(): array { /* For translate module: * Dictionary::translate('ipxe-kcl-extra'); @@ -32,7 +32,7 @@ class LinuxBootEntryHook extends BootEntryHook /** * @return HookEntryGroup[] */ - protected function groupsInternal() + protected function groupsInternal(): array { /* * Dictionary::translate('default_boot_entry'); @@ -76,7 +76,7 @@ class LinuxBootEntryHook extends BootEntryHook * @param $localData * @return BootEntry the actual boot entry instance for given entry, false if invalid id */ - public function getBootEntryInternal($localData) + public function getBootEntryInternal($localData): BootEntry { $id = $localData['id']; if ($id === 'default') { // Special case @@ -125,7 +125,7 @@ class LinuxBootEntryHook extends BootEntryHook return BootEntry::newStandardBootEntry($bios, $efi, $arch); } - private function generateExecData($effectiveId, $remoteData, $localData) + private function generateExecData($effectiveId, $remoteData, $localData): ExecData { $exec = new ExecData(); // Defaults @@ -146,9 +146,7 @@ class LinuxBootEntryHook extends BootEntryHook if (!empty($localData['debug'])) { // Debug boot enabled $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, - isset($remoteData['debugCommandLineModifier']) - ? $remoteData['debugCommandLineModifier'] - : '-vga -quiet -splash -loglevel loglevel=7' + $remoteData['debugCommandLineModifier'] ?? '-vga -quiet -splash -loglevel loglevel=7' ); } // disable all CPU sidechannel attack mitigations etc. @@ -156,6 +154,14 @@ class LinuxBootEntryHook extends BootEntryHook $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, 'noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off mitigations=off'); } + // GVT, PCI Pass-thru etc. + if (Module::isAvailable('statistics')) { + $hwextra = HardwareInfo::getKclModifications(); + if (!empty($hwextra)) { + $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, $hwextra); + } + } + // User-supplied modifications if (!empty($localData['kcl-extra'])) { $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, $localData['kcl-extra']); } diff --git a/modules-available/passthrough/config.json b/modules-available/passthrough/config.json new file mode 100644 index 00000000..3f65995f --- /dev/null +++ b/modules-available/passthrough/config.json @@ -0,0 +1,7 @@ +{ + "category": "main.settings-client", + "dependencies": [ + "statistics", + "locations" + ] +} \ No newline at end of file diff --git a/modules-available/passthrough/inc/passthrough.inc.php b/modules-available/passthrough/inc/passthrough.inc.php new file mode 100644 index 00000000..524aea5e --- /dev/null +++ b/modules-available/passthrough/inc/passthrough.inc.php @@ -0,0 +1,53 @@ + $id, + 'ptname' => $id, + 'selected' => ($row['@PASSTHROUGH'] === $id ? 'selected' : ''), + ]; + } + return $out; + } + static $list = false; + if ($list === false) { + $list = Database::queryKeyValueList("SELECT groupid, title FROM passthrough_group ORDER BY groupid"); + self::ensurePrepopulated($list); + } + $row['custom_groups'] = true; + foreach ($list as $id => $title) { + if ($id === 'GPU' || $id === 'GVT') + continue; + $item = ['ptid' => $id, 'ptname' => $id . ' (' . $title . ')']; + if ($row['@PASSTHROUGH'] === $id) { + $item['selected'] = 'selected'; + } + $out[] = $item; + } + return $out; + } + + private static function ensurePrepopulated(&$list) + { + $want = [ + 'GPU' => '[Special] GPU passthrough default group', + 'GVT' => '[Special] Intel GVT-g default group', + ]; + foreach ($want as $id => $title) { + if (!isset($list[$id])) { + Database::exec("INSERT INTO passthrough_group (groupid, title) VALUES (:id, :title) + ON DUPLICATE KEY UPDATE title = VALUES(title)", + ['id' => $id, 'title' => $title]); + $list[$id] = $title; + } + } + } + +} \ No newline at end of file diff --git a/modules-available/passthrough/install.inc.php b/modules-available/passthrough/install.inc.php new file mode 100644 index 00000000..e08be38b --- /dev/null +++ b/modules-available/passthrough/install.inc.php @@ -0,0 +1,9 @@ +saveHwList(); + } + if (Request::isPost()) { + Util::redirect('?do=passthrough'); + } + } + + private function saveHwList() + { + $newgroups = Request::post('newgroup', [], 'array'); + foreach ($newgroups as $id => $title) { + $id = strtoupper(preg_replace('/[^a-z0-9_\-]/i', '', $id)); + if (empty($id)) + continue; + Database::exec("INSERT IGNORE INTO passthrough_group (groupid, title) + VALUES (:group, :title)", + ['group' => $id, 'title' => $title]); + } + $groups = Request::post('ptgroup', Request::REQUIRED, 'array'); + $insert = []; + $delete = []; + foreach ($groups as $hwid => $group) { + if (empty($group)) { + $delete[] = $hwid; + } else { + $insert[] = [$hwid, '@PASSTHROUGH', $group]; + } + } + if (!empty($insert)) { + Database::exec("INSERT INTO statistic_hw_prop (hwid, prop, `value`) VALUES :list + ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)", + ['list' => $insert]); + } + if (!empty($delete)) { + Database::exec("DELETE FROM statistic_hw_prop WHERE hwid IN (:list) AND prop = '@PASSTHROUGH'", ['list' => $delete]); + } + Message::addSuccess('list-updated'); + Util::redirect('?do=passthrough&show=hwlist'); + } + + /* + * + */ + + protected function doRender() + { + $show = Request::get('show'); + if ($show === 'hwlist') { + $this->showHardwareList(); + } else { + Util::redirect('?do=passthrough&show=hwlist'); + } + } + + private function showHardwareList() + { + $q = new HardwareQuery(HardwareInfo::PCI_DEVICE, null, false); + $q->addGlobalColumn('vendor'); + $q->addGlobalColumn('device'); + $q->addGlobalColumn('class'); + $q->addGlobalColumn('@PASSTHROUGH'); + $rows = []; + foreach ($q->query('shw.hwid') as $row) { + $row['ptlist'] = Passthrough::getGroupDropdown($row); + $rows[] = $row; + } + // Sort Graphics Cards first, rest by class, vendor, device + usort($rows, function ($row1, $row2) { + $a = $row1['class']; + $b = $row2['class']; + if ($a === $b) + return hexdec($row1['vendor'].$row1['device']) - hexdec($row2['vendor'] . $row2['device']); + if ($a === '0300') + return -1; + if ($b === '0300') + return 1; + return hexdec($a) - hexdec($b); + }); + $finalRows = []; + $missing = []; + $lastClass = ''; + foreach ($rows as $row) { + if ($row['class'] !== $lastClass) { + // Add class row header + $lastClass = $row['class']; + $finalRows[] = [ + 'class' => $row['class'], + 'class_name' => PciId::getPciId(PciId::DEVCLASS, $row['class'], true) ?: 'Unknown', + ]; + } + $row['vendor_name'] = PciId::getPciId(PciId::VENDOR, $row['vendor'] ?? ''); + $row['device_name'] = PciId::getPciId(PciId::DEVICE, $row['vendor'] . ':' . $row['device']); + $finalRows[] = $row; + // Build up query + if ($row['vendor_name'] === false) { + $missing[$row['vendor']] = true; + } + if ($row['device_name'] === false) { + $missing[$row['vendor'] . ':' . $row['device']] = true; + } + } + Render::addTemplate('hardware-list', [ + 'list' => $finalRows, + 'missing_ids' => json_encode(array_keys($missing)), + ]); + } + + /* + * + */ + + protected function doAjax() + { + // + } + +} \ No newline at end of file diff --git a/modules-available/passthrough/permissions/permissions.json b/modules-available/passthrough/permissions/permissions.json new file mode 100644 index 00000000..d0932f1e --- /dev/null +++ b/modules-available/passthrough/permissions/permissions.json @@ -0,0 +1,3 @@ +{ + "view": false +} \ No newline at end of file diff --git a/modules-available/passthrough/templates/hardware-list.html b/modules-available/passthrough/templates/hardware-list.html new file mode 100644 index 00000000..d331acb5 --- /dev/null +++ b/modules-available/passthrough/templates/hardware-list.html @@ -0,0 +1,125 @@ +
+ + + + + + + + + + + + {{#list}} + + {{#class_name}} + + {{/class_name}} + {{^class_name}} + + + + + {{/class_name}} + + {{/list}} + +
{{lang_deviceIdNumeric}}{{lang_deviceName}}{{lang_useCount}}{{lang_passthroughGroup}}
+ {{class}}{{class_name}} + {{vendor}}:{{device}} + + + + +
+ {{device_name}} +
+
+ {{vendor_name}} +
+
{{connected_count}} + +
+
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php index 0a194f2e..52dbe284 100644 --- a/modules-available/statistics/api.inc.php +++ b/modules-available/statistics/api.inc.php @@ -1,15 +1,19 @@ addCompare(false, 'Memory Slot Occupied', '>=', true, 'Memory Slot Count'); $x->addWhere(true, 'vendor', '=', '8086'); - $x->addColumn(true, 'device'); + $x->addGlobalColumn('device'); $res = $x->query(); foreach ($res as $row) { error_log(json_encode($row)); } exit; + */ HardwareParser::parseMachine('0A5D9E23-80F4-9C43-912C-96D80AE7E80B', file_get_contents('/tmp/bla.json')); echo 'Kweries: ' . Database::getQueryCount(); @@ -79,7 +83,7 @@ if ($type[0] === '~') { $hostname = ''; } $data = Util::cleanUtf8(Request::post('json', '', 'string')); - $hasJson = !empty($data); + $hasJson = !empty($data) && $data[0] === '{'; if (!$hasJson) { $data = Util::cleanUtf8(Request::post('data', '', 'string')); } @@ -340,7 +344,6 @@ if ($type[0] === '~') { 'hwid' => $hwid, 'machineuuid' => $uuid, 'devpath' => $port, - 'serial' => '', ), array('disconnecttime' => 0)); $validProps = array(); if (count($screen) > 1) { @@ -383,7 +386,7 @@ if ($type[0] === '~') { // Some screens connected, make sure old entries get removed Database::exec("UPDATE machine_x_hw x, statistic_hw h SET x.disconnecttime = UNIX_TIMESTAMP() - WHERE (x.hwid, x.devpath) NOT IN (:pairs) AND x.disconnecttime = 0 AND h.hwtype = :type + WHERE (x.hwid, x.devpath) NOT IN (:pairs) AND x.hwid = h.hwid AND x.disconnecttime = 0 AND h.hwtype = :type AND x.machineuuid = :uuid", array( 'pairs' => $keepPair, 'uuid' => $uuid, diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php index 5ef94365..6a6c74cd 100644 --- a/modules-available/statistics/inc/hardwareinfo.inc.php +++ b/modules-available/statistics/inc/hardwareinfo.inc.php @@ -13,4 +13,73 @@ class HardwareInfo const HDD = 'HDD'; const CPU = 'CPU'; + /** + * 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 + * @return string + */ + public static function getKclModifications($uuid = null, $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 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) + return ''; + $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true); + // TODO: Get list of enabled pass through groups for this client's location + $hw->addWhere(true, '@PASSTHROUGH', 'IN', ['GPU', 'GVT']); + $hw->addGlobalColumn('vendor'); + $hw->addGlobalColumn('device'); + $hw->addLocalColumn('slot'); + $res = $hw->query(); + $passthrough = []; + $slots = []; + $gvt = false; + foreach ($res as $row) { + if ($row['@PASSTHROUGH'] === 'GVT') { + $gvt = true; + } else { + $passthrough[$row['vendor'] . ':' . $row['device']] = 1; + error_log('Passthouorgh: ' . $row['vendor'] . ':' . $row['device']); + $slots[preg_replace('/\.[0-9]+$/', '', $row['slot'])] = 1; + } + } + $kcl = ''; + if ($gvt || !empty($passthrough)) { + $kcl = '-iommu -intel_iommu iommu=pt intel_iommu=on'; // TODO AMD + } + if (!empty($passthrough)) { + foreach (array_keys($slots) as $slot) { + error_log('Querying slot ' . $slot); + $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true); + $hw->addWhere(false, 'slot', 'LIKE', $slot . '.%'); + $hw->addGlobalColumn('vendor'); + $hw->addGlobalColumn('device'); + foreach ($hw->query() as $row) { + $passthrough[$row['vendor'] . ':' . $row['device']] = 1; + error_log('Extra PT: ' . $row['vendor'] . ':' . $row['device']); + } + } + $kcl .= ' vfio-pci.ids=' . implode(',', array_keys($passthrough)); + } + if ($gvt) { + $kcl .= ' i915.enable_gvt=1'; + } + return $kcl; + } + } diff --git a/modules-available/statistics/inc/hardwareparser.inc.php b/modules-available/statistics/inc/hardwareparser.inc.php index d356e226..15534749 100644 --- a/modules-available/statistics/inc/hardwareparser.inc.php +++ b/modules-available/statistics/inc/hardwareparser.inc.php @@ -308,29 +308,29 @@ class HardwareParser 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) { + $res = PciId::getPciId('CLASS', $class); + if ($res === false) { $pci[$entry['class']]['lookupClass'] = 'do-lookup'; $pci[$entry['class']]['class'] = $class; } else { - $pci[$entry['class']]['class'] = $res['value']; + $pci[$entry['class']]['class'] = $res; } } $new = array( 'ven' => $entry['ven'], 'dev' => $entry['ven'] . ':' . $entry['dev'], ); - $res = Page_Statistics::getPciId('VENDOR', $new['ven']); - if ($res === false || $res['dateline'] < $NOW) { + $res = PciId::getPciId('VENDOR', $new['ven']); + if ($res === false) { $new['lookupVen'] = 'do-lookup'; } else { - $new['ven'] = $res['value']; + $new['ven'] = $res; } - $res = Page_Statistics::getPciId('DEVICE', $new['dev']); - if ($res === false || $res['dateline'] < $NOW) { + $res = PciId::getPciId('DEVICE', $new['dev']); + if ($res === false) { $new['lookupDev'] = 'do-lookup'; } else { - $new['dev'] = $res['value'] . ' (' . $new['dev'] . ')'; + $new['dev'] = $res . ' (' . $new['dev'] . ')'; } $pci[$entry['class']]['entries'][] = $new; } @@ -444,8 +444,8 @@ class HardwareParser 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|size/', $key)) { + if (is_numeric($val) && preg_match('/^-?[0-9]+$/', $val) + && preg_match('/used|occupied|count|number|size|temperature/', $key)) { return (int)$val; } // Date @@ -538,7 +538,7 @@ class HardwareParser { $ret = []; foreach ($data as $key => $vals) { - $val = trim($vals['values'][0]); + $val = trim($vals['values'][0] ?? 'NULL'); if ($val === '[Empty]' || $val === 'NULL') continue; $val = preg_replace('/[^a-z0-9]/', '', strtolower($val)); @@ -567,7 +567,7 @@ class HardwareParser */ private static function markDisconnected(string $uuid, string $dbType, array $excludedHwIds) { - error_log("Marking disconnected for $dbType from " . implode(', ', $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() @@ -630,10 +630,12 @@ class HardwareParser $globalMainboardExtra['Memory Slot Count'] += $mem['Number Of Devices']; } if (isset($mem['Maximum Capacity'])) { - $globalMainboardExtra['Memory Maximum Capacity'] += self::convertSize($mem['Maximum Capacity'], 'M', false); + $globalMainboardExtra['Memory Maximum Capacity'] + += self::convertSize($mem['Maximum Capacity'], 'M', false); } } - $globalMainboardExtra['Memory Maximum Capacity'] = self::convertSize($globalMainboardExtra['Memory Maximum Capacity'] . ' MB'); + $globalMainboardExtra['Memory Maximum Capacity'] + = self::convertSize($globalMainboardExtra['Memory Maximum Capacity'] . ' MB', 'G'); // BIOS section - need to cross-match this with mainboard or system model, as it doesn't have a meaningful // identifier on its own $bios = self::prepareDmiProperties(self::getDmiHandles($data, 0)[0]); @@ -651,8 +653,14 @@ class HardwareParser } } // Using the general helper function - $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, HardwareInfo::RAM_MODULE, function (array $flat): bool { - return self::convertSize(($flat['Size'] ?? 0), '', false) > 65 * 1024 * 1024; + $capa = 0; + $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, HardwareInfo::RAM_MODULE, function (array $flat) use (&$capa): bool { + $size = self::convertSize(($flat['Size'] ?? 0), '', false); + if ($size > 65 * 1024 * 1024) { + $capa += $size; + return true; + } + return false; }, ['Locator'], ['Data Width', @@ -669,6 +677,7 @@ class HardwareParser ); // Fake RAM slots used/total into this $localMainboardExtra['Memory Slot Occupied'] = $ramModCount; + $localMainboardExtra['Memory Installed Capacity'] = self::convertSize($capa, 'G', true); self::updateHwTypeFromDmi($uuid, $data, 2, HardwareInfo::MAINBOARD, ['Manufacturer', 'Product Name'], [], ['Manufacturer', 'Product Name', 'Type', 'Version'], @@ -714,15 +723,18 @@ class HardwareParser // ---- lspci ------------------------------------ $pciHwIds = []; foreach (($data['lspci'] ?? []) as $dev) { - $hwid = self::writeGlobalHardwareData(HardwareInfo::PCI_DEVICE, - self::propsFromArray($dev, 'vendor', 'device', 'rev', 'class')); + $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')); + self::propsFromArray($dev, 'slot', 'subsystem', 'subsystem_vendor', 'iommu_group')); $pciHwIds[] = $mappingId; } self::markDisconnected($uuid, HardwareInfo::PCI_DEVICE, $pciHwIds); // ---- Disks ------------------------------------0Y3R3K $hddHwIds = []; + $id44 = $id45 = 0; foreach (($data['drives'] ?? []) as $dev) { if (empty($dev['readlink'])) continue; @@ -739,11 +751,12 @@ class HardwareParser if (!isset($smart['rotation_rate']) && isset($lsblk['rota']) && !$lsblk['rota']) { $smart['rotation_rate'] = 0; } + $size = $lsblk['size'] ?? $smart['user_capacity']['bytes'] ?? 'unknown'; $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' => $lsblk['size'] ?? $smart['user_capacity']['bytes'] ?? '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, @@ -781,6 +794,7 @@ class HardwareParser }, ARRAY_FILTER_USE_BOTH); } // Partitions - find special ones + $used = 0; if (isset($dev['sfdisk']['partitiontable'])) { $table['partition_table'] = $dev['sfdisk']['partitiontable']['label'] ?? 'none'; switch ($dev['sfdisk']['partitiontable']['unit'] ?? 'sectors') { @@ -795,6 +809,13 @@ class HardwareParser } $i = 0; foreach (($dev['sfdisk']['partitiontable']['partitions'] ?? []) as $part) { + if (!isset($part['size'])) + continue; + $type = hexdec($part['type'] ?? '0'); + if ($type === 0x0 || $type === 0x5 || $type === 0xf || $type === 0x15 || $type === 0x1f + || $type === 0x85 || $type === 0xc5 || $type == 0xcf) + continue; // Extended partition, ignore + $used += $part['size'] * $fac; $id = 'part_' . $i . '_'; foreach (['start', 'size', 'type', 'uuid', 'name'] as $item) { if (!isset($part[$item])) @@ -811,19 +832,23 @@ class HardwareParser 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 += self::propsFromArray($smart + ($lsblk ?? []), + 'serial_number', 'firmware_version', + 'interface_speed//current//string', + 'smart_status//passed', 'temperature//current', 'temperature//min', 'temperature//max'); $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['readlink'], - self::propsFromArray($smart + ($lsblk ?? []), - 'serial_number', 'firmware_version', - 'interface_speed//current//string', - 'smart_status//passed', 'temperature//current', 'temperature//min', 'temperature//max') + $table); + $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 '@%'", [ @@ -836,8 +861,12 @@ class HardwareParser self::markDisconnected($uuid, HardwareInfo::HDD, $hddHwIds); // // Mark parse date - Database::exec("UPDATE machine SET dataparsetime = UNIX_TIMESTAMP() WHERE machineuuid = :uuid", - ['uuid' => $uuid]); + Database::exec("UPDATE machine SET dataparsetime = UNIX_TIMESTAMP(), id44mb = :id44, id45mb = :id45 + WHERE machineuuid = :uuid", [ + 'uuid' => $uuid, + 'id44' => round($id44 / (1024 * 1024)), + 'id45' => round($id45 / (1024 * 1024)), + ]); } /** diff --git a/modules-available/statistics/inc/hardwarequery.inc.php b/modules-available/statistics/inc/hardwarequery.inc.php index 7ccde2f6..6b5662d4 100644 --- a/modules-available/statistics/inc/hardwarequery.inc.php +++ b/modules-available/statistics/inc/hardwarequery.inc.php @@ -13,16 +13,18 @@ class HardwareQuery * @param string $type type form HardwareInfo * @param ?string $uuid */ - public function __construct($type, $uuid = null, $connectedOnly = true) + public function __construct(string $type, $uuid = null, $connectedOnly = true) { if ($connectedOnly) { - $this->where[] = 'mxhw.disconnecttime = 0'; + $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->joins[] = "INNER JOIN statistic_hw shw ON (mxhw.hwid = shw.hwid AND shw.hwtype = :hwtype)"; + $this->where[] = 'shw.hwtype = :hwtype'; $this->args['hwtype'] = $type; } @@ -44,7 +46,13 @@ class HardwareQuery } } - public function addWhere(bool $global, string $prop, string $op, string $value) + /** + * @param bool $global + * @param string $prop + * @param string $op + * @param string|string[] $value + */ + public function addWhere(bool $global, string $prop, string $op, $value) { if (isset($this->columns[$prop])) return; @@ -54,12 +62,24 @@ class HardwareQuery $vid = $this->id(); $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value'; $this->joins[$prop] = "INNER JOIN $table $tid ON ($srcTable.$column = $tid.$column AND - $tid.prop = :$pid AND $tid.`$valueCol` $op :$vid)"; + $tid.prop = :$pid AND $tid.`$valueCol` $op (:$vid))"; $this->args[$pid] = $prop; $this->args[$vid] = $value; $this->columns[$prop] = "$tid.`value` AS `$prop`"; } + public function addMachineWhere(string $column, string $op, $value) + { + if (isset($this->columns[$column])) + return; + $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value'; + $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 addCompare(bool $global1, string $prop1, string $op, string $global2, string $prop2) { $this->fillTableVars($global1, $srcTable1, $table1, $column1); @@ -76,6 +96,17 @@ class HardwareQuery $this->args[$pid1] = $prop1; $this->args[$pid2] = $prop2; $this->columns[$prop1] = "$tid1.`value` AS `$prop1`"; + $this->columns[$prop2] = "$tid2.`value` AS `$prop2`"; + } + + public function addGlobalColumn(string $prop) + { + $this->addColumn(true, $prop); + } + + public function addLocalColumn(string $prop) + { + $this->addColumn(false, $prop); } public function addColumn(bool $global, string $prop) @@ -85,21 +116,41 @@ class HardwareQuery $this->fillTableVars($global, $srcTable, $table, $column); $tid = $this->id(); $pid = $this->id(); - $this->joins[$prop] = "INNER JOIN $table $tid ON ($srcTable.$column = $tid.$column AND $tid.prop = :$pid)"; + $this->joins[$prop] = "LEFT JOIN $table $tid ON ($srcTable.$column = $tid.$column AND $tid.prop = :$pid)"; $this->args[$pid] = $prop; $this->columns[$prop] = "$tid.`value` AS `$prop`"; } + public function addMachineColumn(string $column) + { + 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() + public function query(string $groupBy = '') { - $this->columns[] = 'mxhw.machineuuid'; - $query = 'SELECT ' . implode(', ', $this->columns) - . ' FROM machine_x_hw mxhw ' + $columns = $this->columns; + $columns[] = 'mxhw.machineuuid'; + $columns[] = 'shw.hwid'; + if (empty($groupBy) || $groupBy === '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 $groupBy"; + } + $query = 'SELECT ' . implode(', ', $columns) + . ' FROM statistic_hw shw ' . implode(' ', $this->joins) - . ' WHERE ' . implode(' AND ', $this->where); + . ' WHERE ' . implode(' AND ', $this->where) + . $groupBy; return Database::simpleQuery($query, $this->args); } diff --git a/modules-available/statistics/inc/pciid.inc.php b/modules-available/statistics/inc/pciid.inc.php new file mode 100644 index 00000000..65edf570 --- /dev/null +++ b/modules-available/statistics/inc/pciid.inc.php @@ -0,0 +1,76 @@ + $cat, 'id' => $id)); + if ($row !== false && $row['dateline'] >= time()) { + return $cache[$key] = $row['value']; + } + if (!$dnsQuery) + return false; + // Unknown, query + if ($cat === self::DEVICE && preg_match('/^([a-f0-9]{4}):([a-f0-9]{4})$/', $id, $out)) { + $host = $out[2] . '.' . $out[1]; + } elseif ($cat === self::VENDOR && preg_match('/^([a-f0-9]{4})$/', $id, $out)) { + $host = $out[1]; + } elseif ($cat === self::DEVCLASS && preg_match('/^c\.([a-f0-9]{2})([a-f0-9]{2})$/', $id, $out)) { + $host = $out[2] . '.' . $out[1] . '.c'; + } else { + error_log("getPciId called with unknown format: ($cat) ($id)"); + return false; + } + $res = dns_get_record($host . '.pci.id.ucw.cz', DNS_TXT); + if (!is_array($res)) + return false; + 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/install.inc.php b/modules-available/statistics/install.inc.php index 5856c81e..1b664d04 100644 --- a/modules-available/statistics/install.inc.php +++ b/modules-available/statistics/install.inc.php @@ -40,6 +40,7 @@ $res[] = tableCreate('machine', " `cpumodel` varchar(120) NOT NULL, `systemmodel` varchar(120) NOT NULL DEFAULT '', `id44mb` int(10) unsigned NOT NULL, + `id45mb` int(10) unsigned NOT NULL, `badsectors` int(10) unsigned NOT NULL, `data` mediumblob NOT NULL, `dataparsetime` int(10) unsigned NOT NULL DEFAULT 0, @@ -339,7 +340,15 @@ if (!tableHasColumn('machine', 'dataparsetime')) { $ret = Database::exec("ALTER TABLE `machine` ADD COLUMN `dataparsetime` int(10) unsigned NOT NULL DEFAULT '0' AFTER `data`"); if ($ret === false) { - finalResponse(UPDATE_FAILED, 'Adding mem-stat columns to machine table failed: ' . Database::lastError()); + finalResponse(UPDATE_FAILED, 'Adding dateparsetime column to machine table failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} +if (!tableHasColumn('machine', 'id45mb')) { + $ret = Database::exec("ALTER TABLE `machine` + ADD COLUMN `id45mb` int(10) unsigned NOT NULL AFTER `id44mb`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding id45mb column to machine table failed: ' . Database::lastError()); } $res[] = UPDATE_DONE; } diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php index 3e4aa9ce..04d9a515 100644 --- a/modules-available/statistics/page.inc.php +++ b/modules-available/statistics/page.inc.php @@ -24,10 +24,12 @@ class Page_Statistics extends Page /* Dictionary::translate('submenu_projectors'); Dictionary::translate('submenu_replace'); + Dictionary::translate('submenu_hints'); */ - foreach (['projectors', 'replace'] as $section) { - Dashboard::addSubmenu('?do=statistics&show=' . $section, Dictionary::translate('submenu_' . $section, true)); + foreach (['projectors', 'replace', 'hints'] as $section) { + Dashboard::addSubmenu('?do=statistics&show=' . $section, + Dictionary::translate('submenu_' . $section, true)); } $this->show = Request::any('show', false, 'string'); @@ -251,11 +253,24 @@ class Page_Statistics extends Page { if (!User::load()) return; - if (Request::any('action') === 'bios') { + $action = Request::any('action'); + if ($action === 'bios') { require_once 'modules/statistics/pages/machine.inc.php'; SubPage::ajaxCheckBios(); return; } + if ($action === 'json-lookup') { + $reply = []; + foreach (Request::post('list', [], 'array') as $item) { + $name = PciId::getPciId(PciId::AUTO, $item, true); + if ($name === false) { + $name = '?????'; + } + $reply[$item] = $name; + } + header('Content-Type: application/json'); + die(json_encode($reply)); + } $param = Request::any('lookup', false, 'string'); if ($param === false) { @@ -263,61 +278,14 @@ class Page_Statistics extends Page } $add = ''; if (preg_match('/^([a-f0-9]{4}):([a-f0-9]{4})$/', $param, $out)) { - $cat = 'DEVICE'; - $host = $out[2] . '.' . $out[1]; $add = ' (' . $param . ')'; - } elseif (preg_match('/^([a-f0-9]{4})$/', $param, $out)) { - $cat = 'VENDOR'; - $host = $out[1]; - } elseif (preg_match('/^c\.([a-f0-9]{2})([a-f0-9]{2})$/', $param, $out)) { - $cat = 'CLASS'; - $host = $out[2] . '.' . $out[1] . '.c'; - } else { - die('Invalid format requested'); - } - $cached = Page_Statistics::getPciId($cat, $param); - if ($cached !== false && $cached['dateline'] > time()) { - echo $cached['value'], $add; - exit; - } - $res = dns_get_record($host . '.pci.id.ucw.cz', DNS_TXT); - if (is_array($res)) { - foreach ($res as $entry) { - if (isset($entry['txt']) && substr($entry['txt'], 0, 2) === 'i=') { - $string = substr($entry['txt'], 2); - Page_Statistics::setPciId($cat, $param, $string); - echo $string, $add; - exit; - } - } } - if ($cached !== false) { - echo $cached['value'], $add; - exit; + $cached = PciId::getPciId(PciId::AUTO, $param, true); + if ($cached === false) { + $cached = 'Unknown'; } - die('Not found'); - } - - public static function getPciId($cat, $id) - { - static $cache = []; - $key = $cat . '-' . $id; - if (isset($cache[$key])) - return $cache[$key]; - return $cache[$key] = Database::queryFirst('SELECT value, dateline FROM pciid WHERE category = :cat AND id = :id LIMIT 1', - array('cat' => $cat, 'id' => $id)); - } - - private static function setPciId($cat, $id, $value) - { - 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' => $value, - 'timeout' => time() + mt_rand(10, 30) * 86400, - ), true); + echo $cached, $add; + exit; } } diff --git a/modules-available/statistics/pages/hints.inc.php b/modules-available/statistics/pages/hints.inc.php new file mode 100644 index 00000000..5c6acfbb --- /dev/null +++ b/modules-available/statistics/pages/hints.inc.php @@ -0,0 +1,77 @@ +addLocalColumn('Memory Slot Occupied'); + $q->addGlobalColumn('Memory Slot Count'); + $q->addGlobalColumn('Memory Maximum Capacity'); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addWhere(false, 'Memory Installed Capacity', '<', 8 * 1024 * 1024 * 1024); + $list = []; + foreach ($q->query() as $row) { + if (HardwareParser::convertSize($row['Memory Installed Capacity'], 'M', false) + >= HardwareParser::convertSize($row['Memory Maximum Capacity'], 'M', false)) { + $row['size_class'] = 'danger'; + $list[] = $row; + } elseif ($row['Memory Slot Occupied'] < $row['Memory Slot Count']) { + $row['count_class'] = 'success'; + array_unshift($list, $row); + } else { + $list[] = $row; + } + } + Render::addTemplate('hints-ram-upgrade', ['list' => $list]); + } + + private static function showMemorySlow() + { + $q = new HardwareQuery(HardwareInfo::RAM_MODULE); + $q->addLocalColumn('Locator'); + $q->addLocalColumn('Bank Locator'); + $q->addGlobalColumn('Form Factor'); + $q->addGlobalColumn('Type'); + $q->addGlobalColumn('Size'); + $q->addGlobalColumn('Manufacturer'); + $q->addLocalColumn('Serial Number'); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addCompare(true, 'Speed', '>', false, 'Configured Memory Speed'); + $list = $q->query()->fetchAll(); + Render::addTemplate('hints-ram-underclocked', ['list' => $list]); + } + + private static function showUnusedSpace() + { + $q = new HardwareQuery(HardwareInfo::HDD); + $q->addWhere(false, 'unused', '>', 2000000000); // 2 GB + $q->addMachineWhere('id44mb', '<', 20000); // 20 GB + $id44 = $q->query()->fetchAll(); + $q = new HardwareQuery(HardwareInfo::HDD); + $q->addWhere(false, 'unused', '>', 25000000000); // 25 GB + $q->addMachineWhere('id44mb', '>', 20000); // 20 GB + $q->addMachineWhere('id45mb', '<', 20000); // 20 GB + $id45 = $q->query()->fetchAll(); + Render::addTemplate('hints-hdd-grow', [ + 'id44' => $id44, + 'id45' => $id45, + ]); + } + +} \ No newline at end of file diff --git a/modules-available/statistics/templates/hints-ram-underclocked.html b/modules-available/statistics/templates/hints-ram-underclocked.html new file mode 100644 index 00000000..e1f19c4f --- /dev/null +++ b/modules-available/statistics/templates/hints-ram-underclocked.html @@ -0,0 +1,44 @@ +

{{lang_ramUnderclocked}}

+ +

{{lang_ramUnderclockedText}}

+ + + + + + + + + + + + + + {{#list}} + + + + + + + + + {{/list}} + +
{{lang_machine}}{{lang_type}}{lang_speedCurrent}}{{lang_speedDesign}}{{lang_manufacturer}}{{lang_serialNumber}}
+ + {{hostname}}{{^hostname}}{{clientip}}{{/hostname}} + +
{{machineuuid}}
+
+ {{Type}} {{Form Factor}} +
{{Size}}
+
+ {{Configured Memory Speed}} + + {{Speed}} + + {{Manufacturer}} + + {{Serial Number}} +
\ No newline at end of file diff --git a/modules-available/statistics/templates/hints-ram-upgrade.html b/modules-available/statistics/templates/hints-ram-upgrade.html new file mode 100644 index 00000000..be0d2532 --- /dev/null +++ b/modules-available/statistics/templates/hints-ram-upgrade.html @@ -0,0 +1,31 @@ +

{{lang_ramUpgrade}}

+ +

{{lang_ramUpgradeText}}

+ + + + + + + + + + + {{#list}} + + + + + + {{/list}} + +
{{lang_machine}}{{lang_installedCountMax}}{{lang_ramSizeCurrentMax}}
+ + {{hostname}}{{^hostname}}{{clientip}}{{/hostname}} + +
{{machineuuid}}
+
+ {{Memory Slot Occupied}} / {{Memory Slot Count}} + + {{Memory Installed Capacity}} / {{Memory Maximum Capacity}} +
\ No newline at end of file -- cgit v1.2.3-55-g7522