diff options
Diffstat (limited to 'modules-available/statistics/pages')
-rw-r--r-- | modules-available/statistics/pages/hints.inc.php | 221 | ||||
-rw-r--r-- | modules-available/statistics/pages/list.inc.php | 134 | ||||
-rw-r--r-- | modules-available/statistics/pages/machine.inc.php | 420 | ||||
-rw-r--r-- | modules-available/statistics/pages/projectors.inc.php | 6 | ||||
-rw-r--r-- | modules-available/statistics/pages/replace.inc.php | 24 | ||||
-rw-r--r-- | modules-available/statistics/pages/summary.inc.php | 268 |
6 files changed, 870 insertions, 203 deletions
diff --git a/modules-available/statistics/pages/hints.inc.php b/modules-available/statistics/pages/hints.inc.php new file mode 100644 index 00000000..bfb28c24 --- /dev/null +++ b/modules-available/statistics/pages/hints.inc.php @@ -0,0 +1,221 @@ +<?php + +class SubPage +{ + + public static function doPreprocess() + { + User::assertPermission('hints'); + } + + public static function doRender() + { + $locs = User::getAllowedLocations('hints'); + if (in_array(0, $locs)) { + $locs = []; + } + self::showLegacyCpu($locs); + self::showMemoryUpgrade($locs); + self::showSlowNics($locs); + self::showUnusedSpace($locs); + self::showMemorySlow($locs); + } + + private static function isNonClientRunmode(string $machineUuid): bool + { + static $cache = null; + if ($cache === null) { + if (!Module::isAvailable('runmode')) { + $cache = []; + } else { + $cache = RunMode::getAllClients(false, false); + } + } + return isset($cache[$machineUuid]); + } + + /** + * Machines that have less than 8GB of RAM. Highlight those + * that still have free memory slots. + */ + private static function showMemoryUpgrade(array $locs) + { + $q = new HardwareQuery(HardwareInfo::MAINBOARD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineWhere('lastseen', '>', strtotime('-60 days')); + $q->addLocalColumn('Memory Slot Occupied'); + $q->addGlobalColumn('Memory Slot Count'); + $q->addGlobalColumn('Memory Maximum Capacity'); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addMachineColumn('state'); + $q->addLocalColumn('Memory Installed Capacity')->addCondition('<', 8 * 1024 * 1024 * 1024); + $list = []; + foreach ($q->query() as $row) { + if (self::isNonClientRunmode($row['machineuuid'])) + continue; + if (HardwareParser::convertSize($row['Memory Installed Capacity'], 'M', false) + >= HardwareParser::convertSize($row['Memory Maximum Capacity'], 'M', false)) { + $row['size_class'] = 'danger'; + } + if ($row['Memory Slot Occupied'] >= $row['Memory Slot Count']) { + $row['count_class'] = 'warning'; + } + $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']); + $list[] = $row; + } + if (empty($list)) + return; + ArrayUtil::sortByColumn($list, 'hostname'); + Render::addTemplate('hints-ram-upgrade', ['list' => $list]); + } + + /** + * Show machines where RAM modules are running slower + * than their design speed. + */ + private static function showMemorySlow(array $locs) + { + $q = new HardwareQuery(HardwareInfo::RAM_MODULE); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineWhere('lastseen', '>', strtotime('-60 days')); + //$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->addMachineColumn('state'); + $col = $q->addGlobalColumn('Speed'); + $col->addCondition('>', $q->addLocalColumn('Configured Memory Speed')); + $list = []; + foreach ($q->query(['machineuuid', 'Size', 'Manufacturer', 'Speed', 'Configured Memory Speed']) as $row) { + // Sometimes configured speed reports as 2666 while rated speed is 2667 + // Cast as these have a MT/s suffic, triggering a PHP notice about malformed numbers + if ((int)$row['Configured Memory Speed'] + 33 >= (int)$row['Speed']) + continue; + $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']); + $list[] = $row; + } + if (empty($list)) + return; + ArrayUtil::sortByColumn($list, 'hostname'); + Render::addTemplate('hints-ram-underclocked', ['list' => $list]); + } + + /** + * Show machines that have unpartitioned space available, + * and no ID44 or ID45. + */ + private static function showUnusedSpace(array $locs) + { + $id44 = $id45 = []; + // ID44 + $q = new HardwareQuery(HardwareInfo::HDD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineWhere('lastseen', '>', strtotime('-60 days')); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addLocalColumn('unused')->addCondition('>', 2000000000); // 2 GB + $q->addMachineWhere('id44mb', '<', 20000); // 20 GB + $q->addMachineColumn('state'); + foreach ($q->query() as $row) { + $row['unused_s'] = Util::readableFileSize($row['unused']); + $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2); + $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']); + $id44[] = $row; + } + // ID45 + $q = new HardwareQuery(HardwareInfo::HDD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineWhere('lastseen', '>', strtotime('-60 days')); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addLocalColumn('unused')->addCondition('>', 50000000000); // 50 GB + $q->addMachineWhere('id44mb', '>', 20000); // 20 GB + $q->addMachineWhere('id45mb', '<', 20000); // 20 GB + $q->addMachineColumn('state'); + // Only suggest SSD based systems, caching on spinning rust is usually slower than GBit + $q->addGlobalColumn('rotation_rate')->addCondition('=', 0); + foreach ($q->query() as $row) { + $row['unused_s'] = Util::readableFileSize($row['unused']); + $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2); + $row['id45mb_s'] = Util::readableFileSize($row['id45mb'], -1, 2); + $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']); + $id45[] = $row; + } + if (empty($id44) && empty($id45)) + return; + ArrayUtil::sortByColumn($id44, 'hostname'); + ArrayUtil::sortByColumn($id45, 'hostname'); + Render::addTemplate('hints-hdd-grow', [ + 'id44' => $id44, + 'id45' => $id45, + ]); + } + + private static function showSlowNics(array $locs) + { + $list = []; + $q = new HardwareQuery(HardwareInfo::MAINBOARD); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineWhere('lastseen', '>', strtotime('-60 days')); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addMachineColumn('state'); + $q->addLocalColumn('nic-speed')->addCondition('<', 1000); + $q->addLocalColumn('nic-duplex'); + foreach ($q->query() as $row) { + if ($row['nic-speed'] == 0) { + $row['nic-speed'] = '???'; + } + $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']); + $list[] = $row; + } + if (empty($list)) + return; + ArrayUtil::sortByColumn($list, 'hostname'); + Render::addTemplate('hints-nic-speed', ['list' => $list]); + } + + /** + * Show machines that have a CPU that is only supported by VMware 12.5.x, + * but not newer versions. + */ + private static function showLegacyCpu(array $locs) + { + $list = []; + $q = new HardwareQuery(HardwareInfo::CPU); + if (!empty($locs)) { + $q->addMachineWhere('locationid', 'IN', $locs); + } + $q->addMachineWhere('lastseen', '>', strtotime('-60 days')); + $q->addMachineColumn('clientip'); + $q->addMachineColumn('hostname'); + $q->addMachineColumn('state'); + $q->addMachineColumn('cpumodel'); + $q->addGlobalColumn('vmx-legacy')->addCondition('<>', 0); + foreach ($q->query() as $row) { + $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']); + $list[] = $row; + } + if (empty($list)) + return; + ArrayUtil::sortByColumn($list, 'hostname'); + Render::addTemplate('hints-cpu-legacy', ['list' => $list]); + } + +}
\ No newline at end of file diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php index e9af994a..f08cd71c 100644 --- a/modules-available/statistics/pages/list.inc.php +++ b/modules-available/statistics/pages/list.inc.php @@ -22,31 +22,44 @@ class SubPage } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showMachineList($filterSet) + private static function showMachineList(StatisticsFilterSet $filterSet): void { Module::isAvailable('js_stupidtable'); $filterSet->makeFragments($where, $join, $args); $xtra = ''; - if ($filterSet->isNoId44Filter()) { - $xtra .= ', data'; - } if (Module::isAvailable('runmode')) { $xtra .= ', runmode.module AS rmmodule, runmode.isclient'; if (strpos($join, 'runmode') === false) { - $join .= ' LEFT JOIN runmode USING (machineuuid) '; + $join .= ' LEFT JOIN runmode ON (m.machineuuid = runmode.machineuuid) '; } } - $res = Database::simpleQuery("SELECT m.machineuuid, m.locationid, m.macaddr, m.clientip, m.lastseen, + $allRows = Database::queryAll("SELECT m.machineuuid, m.locationid, m.macaddr, m.clientip, m.lastseen, m.logintime, m.state, m.currentuser, m.currentrunmode, m.realcores, m.mbram, m.kvmstate, m.cpumodel, m.id44mb, - m.hostname, m.notes IS NOT NULL AS hasnotes, + m.id45mb, m.hostname, m.notes IS NOT NULL AS hasnotes, m.badsectors, Count(s.machineuuid) AS confvars $xtra FROM machine m - LEFT JOIN setting_machine s USING (machineuuid) + LEFT JOIN setting_machine s ON (m.machineuuid = s.machineuuid) $join WHERE $where GROUP BY m.machineuuid", $args); - $rows = array(); - $singleMachine = 'none'; + // If filter results in just one result, redirect to machine details + if (count($allRows) === 1) { + Util::redirect('?do=statistics&uuid=' . $allRows[0]['machineuuid']); + } + // Gather additional info that would be ugly to fetch via joins above + $uuids = array_column($allRows, 'machineuuid'); + $machineWithHdds = Database::queryKeyValueList("SELECT mxx.machineuuid, Count(s.hwid) AS num + FROM statistic_hw s + INNER JOIN machine_x_hw AS mxx ON (s.hwid = mxx.hwid AND s.hwtype = :type + AND mxx.disconnecttime = 0 AND mxx.machineuuid IN (:ids)) + GROUP BY mxx.machineuuid", + ['type' => HardwareInfo::HDD, 'ids' => $uuids]); + $machineNicSpeed = Database::queryKeyValueList("SELECT mxx.machineuuid, Max(mxhp.`numeric`) AS num + FROM statistic_hw s + INNER JOIN machine_x_hw AS mxx ON (s.hwid = mxx.hwid AND s.hwtype = :type + AND mxx.disconnecttime = 0 AND mxx.machineuuid IN (:ids)) + INNER JOIN machine_x_hw_prop mxhp ON (mxx.machinehwid = mxhp.machinehwid AND mxhp.prop = :prop) + GROUP BY mxx.machineuuid", + ['type' => HardwareInfo::MAINBOARD, 'prop' => 'nic-speed', 'ids' => $uuids]); + $machineWithConfigOverrides = Database::queryKeyValueList("SELECT machineuuid, Count(machineuuid) AS num + FROM setting_machine WHERE machineuuid IN (:ids) GROUP BY machineuuid", ['ids' => $uuids]); // TODO: Cannot disable checkbox for those where user has no permission, since we got multiple actions now // We should pass these lists to the output and add some JS magic // Either disable the delete/reboot/... buttons as soon as at least one "forbidden" client is selected (potentially annoying) @@ -56,38 +69,54 @@ class SubPage $shutdownAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot'); $wolAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.wol'); $execAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.exec'); + $benchmarkAllowedLocations = User::getAllowedLocations('.vmstore.benchmark'); // Only make client clickable if user is allowed to view details page $detailsAllowedLocations = User::getAllowedLocations("machine.view-details"); $location = self::buildLocationLookup(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - if ($singleMachine === 'none') { - $singleMachine = $row['machineuuid']; - } else { - $singleMachine = false; - } + $rows = []; + $colValCount = []; // Count unique values for several columns + foreach ($allRows as &$row) { + settype($row['locationid'], 'int'); $row['link_details'] = in_array($row['locationid'], $detailsAllowedLocations); //$row['firstseen'] = Util::prettyTime($row['firstseen']); $row['lastseen_int'] = $row['lastseen']; $row['lastseen'] = Util::prettyTime($row['lastseen']); //$row['lastboot'] = Util::prettyTime($row['lastboot']); - $row['gbram'] = round(ceil($row['mbram'] / 512) / 2, 1); // Trial and error until we got "expected" rounding.. - $row['gbtmp'] = round($row['id44mb'] / 1024); + $row['gbram'] = Dictionary::number(ceil($row['mbram'] / 512) / 2, 1); // Trial and error until we got "expected" rounding.. + $row['gbtmp'] = Dictionary::number($row['id44mb'] / 1024); + $row['gbpersist'] = Dictionary::number($row['id45mb'] / 1024); $octets = explode('.', $row['clientip']); if (count($octets) === 4) { $row['subnet'] = "$octets[0].$octets[1].$octets[2]"; $row['lastoctet'] = $octets[3]; } - $row['ramclass'] = StatisticsStyling::ramColorClass($row['mbram']); + $row['ramclass'] = StatisticsStyling::ramColorClass((int)$row['mbram']); $row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']); - $row['hddclass'] = StatisticsStyling::hddColorClass($row['gbtmp']); + $row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']); if (empty($row['hostname'])) { $row['hostname'] = $row['clientip']; } - if (isset($row['data'])) { - if (!preg_match('#^Disk.* /dev/[^d].* (bytes$|sectors,)#m', $row['data'])) { - $row['nohdd'] = true; - } + if (isset($machineWithConfigOverrides[$row['machineuuid']])) { + $row['confvars'] = $machineWithConfigOverrides[$row['machineuuid']]; + } + if (isset($machineWithHdds[$row['machineuuid']])) { + $row['hddcount'] = $machineWithHdds[$row['machineuuid']]; + } else if ($row['id44mb'] > 0) { + // This might be a machine that wasn't booted with a recent system, and hence doesn't have HWinfo in DB + // If we have ID44 space in our main table, we most likely got an HDD, so fake a count of 1 + $row['hddcount'] = 1; + } + if (!isset($machineNicSpeed[$row['machineuuid']])) { + $row['nic-speed'] = 0; + $row['nic-speed_s'] = '???'; + } else { + $row['nic-speed'] = $machineNicSpeed[$row['machineuuid']]; + $row['nic-speed_s'] = Dictionary::number($machineNicSpeed[$row['machineuuid']]); } + if (isset($row['data']) && !$row['data']) { + $row['nohdd'] = true; + } + // Shorten CPU names a bit for prettier display in small column $row['cpumodel'] = preg_replace('/\(R\)|\(TM\)|\bintel\b|\bamd\b|\bcpu\b|dual-core|\bdual\s+core\b|\bdual\b|\bprocessor\b/i', ' ', $row['cpumodel']); if (!empty($row['rmmodule'])) { $data = RunMode::getRunMode($row['machineuuid'], RunMode::DATA_STRINGS); @@ -116,10 +145,48 @@ class SubPage if ($row['locationid'] > 0) { $row['location'] = $location[$row['locationid']]; } - $rows[] = $row; + foreach (['locationid', 'cpumodel', 'nic-speed_s', 'gbram', 'gbtmp'] as $key) { + if (!isset($colValCount[$key][$row[$key]])) { + $colValCount[$key][$row[$key]] = []; + } + $colValCount[$key][$row[$key]][] = $row['machineuuid']; + } + $rows[] =& $row; } - if ($singleMachine !== false && $singleMachine !== 'none') { - Util::redirect('?do=statistics&uuid=' . $singleMachine); + // Now if all machines are from the same location, try to load the roomplan + // Also, collect all properties that are the same across all machines for display in the sidebar + $roomsvg = null; + $side = []; + if (!empty($rows) && !empty($colValCount)) { + if (count($colValCount['locationid']) === 1 + && ($lid = array_key_first($colValCount['locationid'])) > 0 + && Module::isAvailable('roomplanner')) { + $roomsvg = PvsGenerator::generateSvg($lid, false, 0, 1, true, $colValCount['locationid'][$lid]); + } + // Handle our selected attributes + foreach (['locationid', 'cpumodel', 'nic-speed_s', 'gbram', 'gbtmp'] as $key) { + if (count($colValCount[$key]) === 1) { + $val = array_key_first($colValCount[$key]); + // Suffixes are not localized, but hopefully generic enough for now + switch ($key) { + case 'locationid': + if (!isset($location[$val])) + continue 2; + $val = $location[$val]['name']; + break; + case 'gbram': + $val .= ' GiB RAM'; + break; + case 'gbtmp': + $val .= ' GiB ID-44'; + break; + case 'nic-speed_s': + $val .= ' MBit/s'; + break; + } + $side[] = $val; + } + } } $data = array( 'rowCount' => count($rows), @@ -133,11 +200,14 @@ class SubPage 'canDelete' => !empty($deleteAllowedLocations), 'canWol' => !empty($wolAllowedLocations), 'canExec' => !empty($execAllowedLocations), + 'canBenchmark' => !empty($benchmarkAllowedLocations), + 'roomsvg' => $roomsvg, + 'sidebar' => $side, ); Render::addTemplate('clientlist', $data); } - private static function buildLocationLookup() + private static function buildLocationLookup(): array { $ret = []; $i = 0; @@ -147,4 +217,4 @@ class SubPage return $ret; } -} +}
\ No newline at end of file diff --git a/modules-available/statistics/pages/machine.inc.php b/modules-available/statistics/pages/machine.inc.php index ea545b16..1d46b523 100644 --- a/modules-available/statistics/pages/machine.inc.php +++ b/modules-available/statistics/pages/machine.inc.php @@ -5,7 +5,9 @@ class SubPage public static function doPreprocess() { - + if (!Module::isAvailable('js_chart')) { + ErrorHandler::traceError('js_chart not available'); + } } public static function doRender() @@ -36,7 +38,7 @@ class SubPage 'end' => $row['logintime'] + 300, )); $session = false; - while ($r = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $r) { if ($session === false || abs($session['dateline'] - $row['logintime']) > abs($r['dateline'] - $row['logintime'])) { $session = $r; } @@ -54,22 +56,106 @@ class SubPage $client = Database::queryFirst('SELECT machineuuid, locationid, macaddr, clientip, firstseen, lastseen, logintime, lastboot, state, mbram, live_tmpsize, live_tmpfree, live_id45size, live_id45free, live_swapsize, live_swapfree, live_memsize, live_memfree, live_cpuload, live_cputemp, - Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, data, hostname, currentuser, currentsession, notes + Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, id45mb, data, hostname, currentuser, currentsession, notes FROM machine WHERE machineuuid = :uuid', array('uuid' => $uuid)); if ($client === false) { Message::addError('unknown-machine', $uuid); return; } - if (Module::isAvailable('locations') && !Location::isLeaf($client['locationid'])) { - $client['hasroomplan'] = false; + Render::setTitle(empty($client['hostname']) ? $client['clientip'] : $client['hostname']); + $locations = []; + if ($client['locationid'] > 0 && Module::isAvailable('locations')) { + if (!Location::isLeaf($client['locationid'])) { + $client['hasroomplan'] = false; + } + $locations = Location::getLocationRootChain($client['locationid']); + } + if ($client['locationid'] && $client['hasroomplan'] && Module::isAvailable('roomplanner')) { + $client['roomsvg'] = PvsGenerator::generateSvg($client['locationid'], $client['machineuuid'], + 0, 1, true); } User::assertPermission('machine.view-details', (int)$client['locationid']); // Hack: Get raw collected data if (Request::get('raw', false)) { - Header('Content-Type: text/plain; charset=utf-8'); + Header('Content-Type: application/json'); die($client['data']); } + // Parse data + $hdds = array(); + if ($client['data'][0] === '{') { + $json = json_decode($client['data'], true); + if (is_array($json)) { + $client += self::parseJson($uuid, $json); + $hdds['hdds'] = self::queryHddData($uuid); + } + } else { + self::parseLegacy($client, $hdds); + } + unset($client['data']); + // Get rid of configured speed, if equal to maximum speed + foreach ($client['ram'] as &$item) { + if (isset($item['Configured Memory Speed']) && $item['Configured Memory Speed'] === $item['Speed']) { + unset($item['Configured Memory Speed']); + } + } + unset($item); + // PCI + // 1) get passthrough groups + $passthroughTypes = []; + if (!empty($locations)) { + $hw = new HardwareQuery(HardwareInfo::PCI_DEVICE, $uuid, 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->addGlobalColumn('rev'); + $res = $hw->query(); + foreach ($res as $row) { + $devId = $row['vendor'] . ':' . $row['device'] . ':' . $row['rev']; + if (!isset($passthroughTypes[$devId])) { + $passthroughTypes[$devId] = []; + } + $passthroughTypes[$devId][$row['@PASSTHROUGH']] = $row['@PASSTHROUGH']; + } + } + // 2) Sort and mangle list + $client['lspci1'] = $client['lspci2'] = []; + foreach ($client['lspci'] as $item) { + $devId = $item['vendor'] . ':' . $item['device']; + $item['vendor_s'] = PciId::getPciId(PciId::VENDOR, $item['vendor']); + $item['device_s'] = PciId::getPciId(PciId::DEVICE, $item['vendor'] . $item['device']); + if ($item['vendor_s'] === false) { + $pciLookup[$item['vendor']] = true; + } + if ($item['device_s'] === false) { + $pciLookup[$devId] = true; + } + // Passthrough enabled? + if (isset($passthroughTypes[$devId . ':' . ($item['rev'] ?? '')])) { + $item['pt'] = implode(', ', $passthroughTypes[$devId . ':' . ($item['rev'] ?? '')]); + } + $class = $item['class']; + if ($class === '0300' || $class === '0200' || $class === '0403' || !empty($item['pt'])) { + $dst =& $client['lspci1']; + } else { + $dst =& $client['lspci2']; + } + if (!isset($dst[$class])) { + $dst[$class] = [ + 'class' => $class, + 'class_s' => PciId::getPciId(PciId::DEVCLASS, $class, true), + 'entries' => [], + ]; + } + $dst[$class]['entries'][] = $item; + } + unset($dst, $client['lspci']); + ksort($client['lspci1']); + ksort($client['lspci2']); + $client['lspci1'] = array_values($client['lspci1']); + $client['lspci2'] = array_values($client['lspci2']); // Runmode if (Module::isAvailable('runmode')) { $data = RunMode::getRunMode($uuid, RunMode::DATA_STRINGS); @@ -119,8 +205,9 @@ class SubPage $client['lastboot_s'] .= ' (Up ' . floor($uptime / 86400) . 'd ' . gmdate('H:i', $uptime) . ')'; } } - $client['gbram'] = round(ceil($client['mbram'] / 512) / 2, 1); - $client['gbtmp'] = round($client['id44mb'] / 1024); + $client['gbram'] = Dictionary::number(ceil($client['mbram'] / 512) / 2, 1); + $client['gbtmp'] = Dictionary::number($client['id44mb'] / 1024); + $client['gbid45'] = Dictionary::number($client['id45mb'] / 1024); foreach (['tmp', 'id45', 'swap', 'mem'] as $item) { if ($client['live_' . $item . 'size'] == 0) continue; @@ -132,53 +219,59 @@ class SubPage $client['live_cpuidle'] = 100 - $client['live_cpuload']; } $client['live_cputemppercent'] = max(0, min(100, 110 - $client['live_cputemp'])); - $client['ramclass'] = StatisticsStyling::ramColorClass($client['mbram']); + $client['ramclass'] = StatisticsStyling::ramColorClass((int)$client['mbram']); $client['kvmclass'] = StatisticsStyling::kvmColorClass($client['kvmstate']); - $client['hddclass'] = StatisticsStyling::hddColorClass($client['gbtmp']); - // Parse the giant blob of data - if (strpos($client['data'], "\r") !== false) { - $client['data'] = str_replace("\r", "\n", $client['data']); - } - $hdds = array(); - if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) { - foreach ($out as $section) { - if ($section[1] === 'CPU') { - Parser::parseCpu($client, $section[2]); - } - if ($section[1] === 'dmidecode') { - Parser::parseDmiDecode($client, $section[2]); - } - if ($section[1] === 'Partition tables') { - Parser::parseHdd($hdds, $section[2]); - } - if ($section[1] === 'PCI ID') { - $client['lspci1'] = $client['lspci2'] = array(); - Parser::parsePci($client['lspci1'], $client['lspci2'], $section[2]); - } - if (isset($hdds['hdds']) && $section[1] === 'smartctl') { - // This currently requires that the partition table section comes first... - Parser::parseSmartctl($hdds['hdds'], $section[2]); - } - } + $client['hddclass'] = StatisticsStyling::hddColorClass((int)$client['gbtmp']); + // Format HDD data to strings + foreach ($hdds['hdds'] as &$hdd) { + $hdd['smart_status_failed'] = !($client['smart_status//passed'] ?? 1); + self::mangleHdd($hdd); } - unset($client['data']); // BIOS update check - if (!empty($client['biosrevision'])) { - $mainboard = $client['mobomanufacturer'] . '##' . $client['mobomodel']; - $system = $client['pcmanufacturer'] . '##' . $client['pcmodel']; - $ret = self::checkBios($mainboard, $system, $client['biosdate'], $client['biosrevision']); + if (!empty($client['bios']['BIOS Revision']) || !empty($client['bios']['Release Date'])) { + if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})#', $client['bios']['Release Date'] ?? '', $out)) { + $client['bios']['Release Date'] = $out[2] . '.' . $out[1] . '.' . $out[3]; + } + $mainboard = ($client['mainboard']['Manufacturer'] ?? '') . '##' . ($client['mainboard']['Product Name'] ?? ''); + $system = ($client['system']['Manufacturer'] ?? '') . '##' . ($client['system']['Product Name'] ?? ''); + $ret = self::checkBios($mainboard, $system, + $client['bios']['Release Date'] ?? null, + $client['bios']['BIOS Revision'] ?? null); if ($ret === false) { // Not loaded, use AJAX $params = [ 'mainboard' => $mainboard, 'system' => $system, - 'date' => $client['biosdate'], - 'revision' => $client['biosrevision'], + 'date' => $client['bios']['Release Date'] ?? null, + 'revision' => $client['bios']['BIOS Revision'] ?? null, ]; $client['biosurl'] = '?do=statistics&action=bios&' . http_build_query($params); } elseif (!isset($ret['status']) || $ret['status'] !== 0) { $client['bioshtml'] = Render::parse('machine-bios-update', $ret); } } + // Last booted system. The boot-system entry is created when the client fetches the config, so + // early on, *before* we get the ~poweron event. But in the ~poweron event, the client provides the + // kernel uptime, which is subtracted from what we write to lastboot, so it is actually *before* + // boot-system. + $os = Database::queryFirst("SELECT `data` AS `system`, `dateline` + FROM statistic + WHERE (dateline >= :lastboot) AND typeid = 'boot-system' AND machineuuid = :uuid + ORDER BY dateline ASC LIMIT 1", + ['lastboot' => $client['lastboot'], 'uuid' => $uuid]); + if ($os !== false) { + $client['minilinux'] = $os['system']; + $graphical = Database::queryFirst("SELECT `dateline` + FROM statistic + WHERE (dateline >= :lastboot) AND typeid = 'graphical-startup' AND machineuuid = :uuid + ORDER BY dateline ASC LIMIT 1", + ['lastboot' => $client['lastboot'], 'uuid' => $uuid]); + if ($graphical !== false) { + $boottime = $graphical['dateline'] - $client['lastboot']; + if ($boottime < 400) { // Sanity-check + $client['boottime_s'] = gmdate('i:s', $boottime); + } + } + } // Get locations if (Module::isAvailable('locations')) { $locs = Location::getLocationsAssoc(); @@ -198,10 +291,10 @@ class SubPage . " LEFT JOIN machine_x_hw_prop p ON (m.machinehwid = p.machinehwid AND p.prop = 'resolution')" . " LEFT JOIN statistic_hw_prop q ON (m.hwid = q.hwid AND q.prop = 'projector')" . " WHERE m.machineuuid = :uuid", - array('screen' => DeviceType::SCREEN, 'uuid' => $uuid)); + array('screen' => HardwareInfo::SCREEN, 'uuid' => $uuid)); $client['screens'] = array(); $ports = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if ($row['disconnecttime'] != 0) continue; $ports[] = $row['connector']; @@ -211,6 +304,10 @@ class SubPage Permission::addGlobalTags($client['perms'], null, ['hardware.projectors.edit', 'hardware.projectors.view']); // Throw output at user Render::addTemplate('machine-main', $client); + if (!empty($pciLookup)) { + Render::addTemplate('js-pciquery', + ['missing_ids' => json_encode(array_keys($pciLookup))]); + } // Sessions $NOW = time(); $cutoff = $NOW - 86400 * 7; @@ -227,7 +324,7 @@ class SubPage $spans['graph'] = ''; $last = false; $first = true; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if (!$client['isclient'] && $row['typeid'] === Statistics::SESSION_LENGTH) continue; // Don't differentiate between session and idle for non-clients if ($first && $row['dateline'] > $cutoff && $client['lastboot'] > $cutoff) { @@ -284,7 +381,6 @@ class SubPage 'duration' => '-', 'glyph' => 'user', ]; - $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']); } elseif ($client['state'] === 'OFFLINE') { $spans['graph'] .= '<div style="background:#444;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%"> </div>'; $spans['rows'][] = [ @@ -311,8 +407,8 @@ class SubPage } if (count($spans['rows']) > 10) { $spans['hasrows2'] = true; - $spans['rows2'] = array_slice($spans['rows'], ceil(count($spans['rows']) / 2)); - $spans['rows'] = array_slice($spans['rows'], 0, ceil(count($spans['rows']) / 2)); + $spans['rows2'] = array_slice($spans['rows'], (int)ceil(count($spans['rows']) / 2)); + $spans['rows'] = array_slice($spans['rows'], 0, (int)ceil(count($spans['rows']) / 2)); } $spans['isclient'] = $client['isclient']; Render::addTemplate('machine-usage', $spans); @@ -326,7 +422,7 @@ class SubPage . ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid'])); $count = 0; $log = array(); - while ($row = $lres->fetch(PDO::FETCH_ASSOC)) { + foreach ($lres as $row) { if (substr($row['description'], -5) === 'on :0' && strpos($row['description'], 'root logged') === false) { continue; } @@ -349,7 +445,197 @@ class SubPage } } - private static function eventToIconName($event) + private static function parseLegacy(array &$client, array &$hdds) + { + // Parse the giant blob of data + if (strpos($client['data'], "\r") !== false) { + $client['data'] = str_replace("\r", "\n", $client['data']); + } + if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) { + foreach ($out as $section) { + if ($section[1] === 'CPU') { + HardwareParserLegacy::parseCpu($client, $section[2]); + } + if ($section[1] === 'dmidecode') { + HardwareParserLegacy::parseDmiDecode($client, $section[2]); + } + if ($section[1] === 'Partition tables') { + HardwareParserLegacy::parseHdd($hdds, $section[2]); + } + if ($section[1] === 'PCI ID') { + $client['lspci'] = HardwareParserLegacy::parsePci($section[2]); + } + if (isset($hdds['hdds']) && $section[1] === 'smartctl') { + // This currently requires that the partition table section comes first... + HardwareParserLegacy::parseSmartctl($hdds['hdds'], $section[2]); + } + } + } + } + + private static function parseJson(string $uuid, array $json): array + { + $return = [ + 'lspci' => $json['lspci'] ?? [], + 'ram' => array_map(function($item) { + return HardwareParser::prepareDmiProperties($item); + }, HardwareParser::getDmiHandles($json, 17)), + ]; + foreach ($return['ram'] as $ram) { + if (!empty($ram['Form Factor']) && !empty($ram['Type'])) { + $return['ramtype'] = $ram['Type'] . '-' . $ram['Form Factor']; + break; + } + } + $need = [ + 'bios' => 0, + 'system' => 1, + 'mainboard' => 2, + ]; + foreach ($need as $name => $id) { + $return[$name] = HardwareParser::prepareDmiProperties( + HardwareParser::getDmiHandles($json, $id)[0] ?? []); + } + $q = new HardwareQuery(HardwareInfo::MAINBOARD, $uuid); + $q->addGlobalColumn('Memory Maximum Capacity'); + $q->addGlobalColumn('Memory Slot Count'); + $q->addLocalColumn('cpu-sockets'); + $q->addLocalColumn('cpu-cores'); + $q->addLocalColumn('cpu-threads'); + $q->addLocalColumn('nic-speed'); + $q->addLocalColumn('nic-duplex'); + $res = $q->query()->fetch(); + if (is_array($res)) { + $return += $res; + } + return $return; + } + + private static function queryHddData(string $uuid): array + { + $hdds = []; + $ret = Database::simpleQuery("SELECT mp.`machinehwid`, mp.`prop`, mp.`value`, mp.`numeric` + FROM machine_x_hw_prop mp + INNER JOIN machine_x_hw mxhw ON (mp.machinehwid = mxhw.machinehwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0) + INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type) + UNION SELECT mxhw.`machinehwid`, hwp.`prop`, hwp.`value`, hwp.`numeric` + FROM statistic_hw_prop hwp + INNER JOIN machine_x_hw mxhw ON (hwp.hwid = mxhw.hwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0) + INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type) + ", + ['type' => HardwareInfo::HDD, 'uuid' => $uuid]); + foreach ($ret as $row) { + if (!isset($hdds[$row['machinehwid']])) { + $hdds[$row['machinehwid']] = ['partitions' => []]; + } + $hdd =& $hdds[$row['machinehwid']]; + if (preg_match('/^(attr_[0-9]+)_(.*)$/', $row['prop'], $out)) { + // SMART attributes + if (!isset($hdd[$out[1]])) { + $hdd[$out[1]] = []; + } + $hdd[$out[1]][$out[2]] = $row['numeric'] ?? $row['value']; + } elseif (preg_match('/^part_([0-9]+)_(.*)$/', $row['prop'], $out)) { + // Partitions + if (!isset($hdd['partitions'][$out[1]])) { + $hdd['partitions'][$out[1]] = ['id' => 'dev-' . count($hdds) . '-' . $out[1], 'index' => $out[1]]; + } + $hdd['partitions'][$out[1]][$out[2]] = $row['numeric'] ?? $row['value']; + } else { + $hdd[$row['prop']] = $row['numeric'] ?? $row['value']; + } + } + $result = []; + foreach ($hdds as $k => &$hdd) { + if (substr($hdd['dev'] ?? '/dev/sr', 0, 7) === '/dev/sr') + continue; + $hdd['devid'] = 'k' . $k; + $hdd['partitions'] = array_values($hdd['partitions']); + $result[] = $hdd; + } + return $result; + } + + private static function mangleHdd(array &$hdd) + { + static $hddidx = 0; + if (!isset($hdd['size']) || !is_numeric($hdd['size'])) { + $hdd['size'] = 0; + } + $hdd['hddidx'] = $hddidx++; + $hours = $hdd['power_on_time//hours'] ?? $hdd['attr_9']['raw'] ?? $hdd['power_on_hours'] + ?? $hdd['power_on_time']['hours'] ?? null; + if ($hours !== null) { + $hdd['PowerOnTime'] = ''; + $val = (int)str_replace('.', '', $hours); + if ($val > 8760) { + $hdd['PowerOnTime'] .= floor($val / 8760) . 'Y, '; + $val %= 8760; + } + if ($val > 720) { + $hdd['PowerOnTime'] .= floor($val / 720) . 'M, '; + $val %= 720; + } + if ($val > 24) { + $hdd['PowerOnTime'] .= floor($val / 24) . 'd, '; + $val %= 24; + } + $hdd['PowerOnTime'] .= $val . 'h'; + } + // Sort by start for building pie-chart + $xx = array_column($hdd['partitions'], 'start'); + array_multisort($xx, SORT_ASC, SORT_NUMERIC, + $hdd['partitions']); + $used = 0; + $json = []; + $lastEnd = 0; + $minDisplaySize = $hdd['size'] / 150; + $i = 0; + foreach ($hdd['partitions'] as &$part) { + $dist = $part['start'] - $lastEnd; + if ($dist > $minDisplaySize) { + $json[] = ['value' => $dist, 'color' => '#aaa']; + $i++; + } + if ($part['size'] > $minDisplaySize) { + $json[] = ['value' => $part['size'], 'color' => self::typeToColor($part)]; + $part['idx'] = $i++; + } + $part['size_s'] = Util::readableFileSize($part['size']); + $used += $part['size']; + $lastEnd = $part['start'] + $part['size']; + if (!isset($part['name']) || isset($part['slxtype'])) { + $part['name'] = self::partTypeToName($part['slxtype'] ?? $part['type']); + } + } + $dist = $hdd['size'] - $lastEnd; + if ($dist > $minDisplaySize) { + $json[] = ['value' => $dist, 'color' => '#aaa']; + } + $hdd['json'] = json_encode($json); + $hdd['size_s'] = Util::readableFileSize($hdd['size']); + if ($hdd['size'] - $used > 1000000000) { + $hdd['unused_s'] = Util::readableFileSize($hdd['size'] - $used); + } + // Finally sort by index for table display + array_multisort(array_column($hdd['partitions'], 'index'), SORT_ASC, + $hdd['partitions']); + } + + private static function typeToColor(array $part): string + { + switch ($part['slxtype'] ?? $part['type']) { + case 44: + return '#5c1'; + case 45: + return '#0d7'; + case 82: + return '#48f'; + } + return '#e55'; + } + + private static function eventToIconName($event): string { switch ($event) { case 'session-open': @@ -393,7 +679,7 @@ class SubPage die(Render::parse('machine-bios-update', $reply)); } - private static function checkBios($mainboard, $system, $date, $revision, $json = null) + private static function checkBios(string $mainboard, string $system, ?string $date, ?string $revision, $json = null) { if ($json === null) { if (!file_exists(self::BIOS_CACHE) || filemtime(self::BIOS_CACHE) + 3600 < time()) @@ -402,18 +688,18 @@ class SubPage } if (!is_array($json) || !isset($json['system'])) return ['error' => 'Malformed JSON, no system key']; - if (isset($json['system'][$system]) && isset($json['system'][$system]['fixes']) && isset($json['system'][$system]['match'])) { + if (isset($json['system'][$system]['fixes']) && isset($json['system'][$system]['match'])) { $match =& $json['system'][$system]; - } elseif (isset($json['mainboard'][$mainboard]) && isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) { + } elseif (isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) { $match =& $json['mainboard'][$mainboard]; } else { return ['status' => 0]; } $key = $match['match']; - if ($key === 'revision') { + if ($key === 'revision' && $revision !== null) { $cmp = function ($item) { $s = explode('.', $item); return $s[0] * 0x10000 + $s[1]; }; $reference = $cmp($revision); - } elseif ($key === 'date') { + } elseif ($key === 'date' && $date !== null) { $cmp = function ($item) { $s = explode('.', $item); return $s[2] * 10000 + $s[1] * 100 + $s[0]; }; $reference = $cmp($date); } else { @@ -442,4 +728,26 @@ class SubPage return $retval; } -}
\ No newline at end of file + /** + * @param string $type MBR-type or GPT UUID + * @return string Name of partition type if known, otherwise, $type is returned + */ + private static function partTypeToName(string $type): string + { + switch ($type) { + case '44': + case '45': + return 'OpenSLX-ID' . $type; + case '82': + return 'Linux Swap'; + case '83': + return 'Linux'; + case '7': + return 'NTFS/Windows'; + case 'ef': + return 'EFI'; + } + return HardwareInfo::GPT[$type] ?? $type; + } + +} diff --git a/modules-available/statistics/pages/projectors.inc.php b/modules-available/statistics/pages/projectors.inc.php index cc808cf0..01be2971 100644 --- a/modules-available/statistics/pages/projectors.inc.php +++ b/modules-available/statistics/pages/projectors.inc.php @@ -16,7 +16,7 @@ class SubPage User::assertPermission('hardware.projectors.edit'); $hwid = Request::post('hwid', false, 'int'); if ($hwid === false) { - Util::traceError('Param hwid missing'); + ErrorHandler::traceError('Param hwid missing'); } if ($action === 'addprojector') { Database::exec('INSERT IGNORE INTO statistic_hw_prop (hwid, prop, value)' @@ -49,10 +49,10 @@ class SubPage . " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)" . " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array( 'projector' => 'projector', - 'screen' => DeviceType::SCREEN, + 'screen' => HardwareInfo::SCREEN, )); $data = array( - 'projectors' => $res->fetchAll(PDO::FETCH_ASSOC) + 'projectors' => $res->fetchAll() ); Render::addTemplate('projector-list', $data); } diff --git a/modules-available/statistics/pages/replace.inc.php b/modules-available/statistics/pages/replace.inc.php index 9c16aed7..50bfd6cf 100644 --- a/modules-available/statistics/pages/replace.inc.php +++ b/modules-available/statistics/pages/replace.inc.php @@ -5,6 +5,7 @@ class SubPage public static function doPreprocess() { + User::assertPermission('replace'); $action = Request::post('action', false, 'string'); if ($action === 'replace') { self::handleReplace(); @@ -17,11 +18,13 @@ class SubPage private static function handleReplace() { $replace = Request::post('replace', false, 'array'); - if ($replace === false || empty($replace)) { + if (empty($replace)) { Message::addError('main.parameter-empty', 'replace'); return; } $list = []; + $allowed = User::getAllowedLocations('replace'); + // Loop through passed machines, filter out unsuited pairs (both in use) and those without permission foreach ($replace as $p) { $split = explode('x', $p); if (count($split) !== 2) { @@ -29,13 +32,13 @@ class SubPage continue; } $entry = ['old' => $split[0], 'new' => $split[1]]; - $old = Database::queryFirst('SELECT lastseen FROM machine WHERE machineuuid = :old', + $old = Database::queryFirst('SELECT locationid, lastseen FROM machine WHERE machineuuid = :old', ['old' => $entry['old']]); if ($old === false) { Message::addError('unknown-machine', $entry['old']); continue; } - $new = Database::queryFirst('SELECT firstseen FROM machine WHERE machineuuid = :new', + $new = Database::queryFirst('SELECT locationid, firstseen FROM machine WHERE machineuuid = :new', ['new' => $entry['new']]); if ($new === false) { Message::addError('unknown-machine', $entry['new']); @@ -45,6 +48,16 @@ class SubPage Message::addWarning('ignored-both-in-use', $entry['old'], $entry['new']); continue; } + if (!in_array(0, $allowed)) { + if (!in_array($old['locationid'], $allowed)) { + Message::addWarning('ignored-no-permission', $entry['old']); + continue; + } + if (!in_array($new['locationid'], $allowed)) { + Message::addWarning('ignored-no-permission', $entry['new']); + continue; + } + } $entry['datelimit'] = min($new['firstseen'], $old['lastseen']); $list[] = $entry; } @@ -106,7 +119,10 @@ class SubPage FROM machine old INNER JOIN machine new ON (old.clientip = new.clientip AND old.lastseen < new.firstseen AND old.lastseen > $oldCutoff AND new.firstseen > $newCutoff) ORDER BY oldhost ASC, oldip ASC"); $list = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $allowed = User::getAllowedLocations('replace'); + foreach ($res as $row) { + if (!in_array(0, $allowed) && (!in_array($row['oldlid'], $allowed) || !in_array($row['newlid'], $allowed))) + continue; $row['oldlastseen_s'] = Util::prettyTime($row['oldlastseen']); $row['newfirstseen_s'] = Util::prettyTime($row['newfirstseen']); $list[] = $row; diff --git a/modules-available/statistics/pages/summary.inc.php b/modules-available/statistics/pages/summary.inc.php index ce67070e..905f5d90 100644 --- a/modules-available/statistics/pages/summary.inc.php +++ b/modules-available/statistics/pages/summary.inc.php @@ -8,6 +8,9 @@ class SubPage public static function doPreprocess() { User::assertPermission('view.summary'); + if (!Module::isAvailable('js_chart')) { + ErrorHandler::traceError('js_chart not available'); + } } public static function doRender() @@ -23,7 +26,9 @@ class SubPage // Prepare chart colors self::$STATS_COLORS = []; for ($i = 0; $i < 10; ++$i) { - self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex((($i + 1) * ($i + 1)) / .3922), dechex(abs((5 - $i) * 51))); + self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex( + (int)((($i + 1) * ($i + 1)) / .3922)), + dechex((int)(abs((5 - $i) * 51)))); } $filterSet->filterNonClients(); @@ -38,10 +43,7 @@ class SubPage Render::closeTag('div'); } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showSummary($filterSet) + private static function showSummary(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE $where", $args); @@ -53,36 +55,82 @@ class SubPage } else { $usedpercent = 0; } - $data = array( + $data = [ 'known' => $known['val'], 'online' => $on['val'], 'used' => $used['val'], 'usedpercent' => $usedpercent, 'badhdd' => $hdd['val'], - ); + ]; // Graph - $cutoff = time() - 2 * 86400; - $res = Database::simpleQuery("SELECT dateline, data FROM statistic WHERE typeid = '~stats' AND dateline > $cutoff ORDER BY dateline ASC"); - $labels = array(); - $points1 = array('data' => array(), 'label' => 'Online', 'fillColor' => '#efe', 'strokeColor' => '#aea', 'pointColor' => '#7e7', 'pointStrokeColor' => '#fff', 'pointHighlightFill' => '#fff', 'pointHighlightStroke' => '#7e7'); - $points2 = array('data' => array(), 'label' => 'In use', 'fillColor' => '#fee', 'strokeColor' => '#eaa', 'pointColor' => '#e77', 'pointStrokeColor' => '#fff', 'pointHighlightFill' => '#fff', 'pointHighlightStroke' => '#e77'); - $sum = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $x = explode('#', $row['data']); - if ($sum === 0) { - $labels[] = date('H:i', $row['dateline']); + $labels = []; + $points1 = []; + $points2 = []; + $lectures = []; + // Get locations + if ($filterSet->suitableForUsageGraph()) { + $locFilter = $filterSet->hasFilter('LocationStatisticsFilter'); + if ($locFilter === null + || ($locFilter->op === '~' && ($locFilter->argument == 0 + || (is_array($locFilter->argument) && in_array(0, $locFilter->argument))))) { + $locations = null; + $op = null; + } elseif ($locFilter->op === '~') { + $locations = array_keys(Location::getRecursiveFlat($locFilter->argument)); + $op = $locFilter->op; } else { - $x[1] = max($x[1], array_pop($points1['data'])); - $x[2] = max($x[2], array_pop($points2['data'])); + if (is_array($locFilter->argument)) { + $locations = $locFilter->argument; + } else { + $locations = [$locFilter->argument]; + } + $op = $locFilter->op; } - $points1['data'][] = $x[1]; - $points2['data'][] = $x[2]; - ++$sum; - if ($sum === 12) { - $sum = 0; + //error_log($op . ' ' . print_r($locations, true)); + $cutoff = time() - 2 * 86400; + $res = Database::simpleQuery("SELECT dateline, data FROM statistic + WHERE typeid = '~stats' AND dateline > $cutoff ORDER BY dateline DESC"); + // Get max from 4 consecutive values, which should be 4*5 = 20m + $sum = 0; + foreach ($res as $row) { + if ($row['data'][0] === '{') { + $x = json_decode($row['data'], true); + if (!is_array($x) || !isset($x['usage'])) + continue; + $x = self::mangleStatsJson($x, $locations, $op); + } else if ($locations === null) { + $x = explode('#', $row['data']); + if (count($x) < 3) + continue; + $x[] = 0; + } else { + continue; + } + if ($sum % 4 === 0) { + $labels[] = date('H:i', $row['dateline']); + } else { + $x[1] = max($x[1], array_pop($points1)); + $x[2] = max($x[2], array_pop($points2)); + $x[3] += array_pop($lectures); + } + $points1[] = $x[1]; + $points2[] = $x[2]; + $lectures[] = $x[3]; + ++$sum; } } - $data['json'] = json_encode(array('labels' => $labels, 'datasets' => array($points1, $points2))); + if (!empty($points1) && max($points1) > 0) { + $labels = array_reverse($labels); + $points1 = array_reverse($points1); + $points2 = array_reverse($points2); + $lectures = array_reverse($lectures); + $data['json'] = json_encode(['labels' => $labels, + 'datasets' => [ + ['data' => $points1, 'label' => 'Online', 'borderColor' => '#8f3'], + ['data' => $points2, 'label' => 'In use', 'borderColor' => '#e76'], + ]]); + $data['markings'] = json_encode($lectures); + } if (Module::get('runmode') !== false) { $res = Database::queryFirst('SELECT Count(*) AS cnt FROM machine m INNER JOIN runmode r USING (machineuuid)' . " $join WHERE $where", $args); @@ -92,74 +140,71 @@ class SubPage Render::addTemplate('summary', $data); } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showSystemModels($filterSet) + private static function showSystemModels(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine m' . " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args); - $lines = array(); - $json = array(); + $lines = []; + $json = []; $id = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if (empty($row['systemmodel'])) { continue; } settype($row['count'], 'integer'); - $row['id'] = 'systemid' . $id; $row['urlsystemmodel'] = urlencode($row['systemmodel']); + $row['idx'] = count($lines); $lines[] = $row; - $json[] = array( + $json[] = [ 'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)], - 'label' => 'systemid' . $id, 'value' => $row['count'], - ); + ]; ++$id; } self::capChart($json, $lines, 0.92); - Render::addTemplate('cpumodels', array('rows' => $lines, 'json' => json_encode($json))); + Render::addTemplate('cpumodels', ['rows' => $lines, 'json' => json_encode($json)]); } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showMemory($filterSet) + private static function alignBySteps(int $value, array $steps): int + { + for ($i = 1; $i < count($steps); ++$i) { + if ($steps[$i] < $value) { + continue; + } + if ($steps[$i] - $value >= $value - $steps[$i - 1]) { + --$i; + } + return $steps[$i]; + } + return $value; + } + + private static function showMemory(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY mbram", $args); - $lines = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $gb = (int)ceil($row['mbram'] / 1024); - for ($i = 1; $i < count(StatisticsFilter::SIZE_RAM); ++$i) { - if (StatisticsFilter::SIZE_RAM[$i] < $gb) { - continue; - } - if (StatisticsFilter::SIZE_RAM[$i] - $gb >= $gb - StatisticsFilter::SIZE_RAM[$i - 1]) { - --$i; - } - $gb = StatisticsFilter::SIZE_RAM[$i]; - break; - } - if (isset($lines[$gb])) { - $lines[$gb] += $row['count']; - } else { - $lines[$gb] = $row['count']; - } + $lines = []; + foreach ($res as $row) { + $gb = self::alignBySteps((int)ceil($row['mbram'] / 1024), StatisticsFilter::SIZE_RAM); + $lines[$gb] = ($lines[$gb] ?? 0) + $row['count']; } asort($lines); - $data = array('rows' => array()); - $json = array(); + $data = ['rows' => []]; + $json = []; $id = 0; foreach (array_reverse($lines, true) as $k => $v) { - $data['rows'][] = array('gb' => $k, 'count' => $v, 'class' => StatisticsStyling::ramColorClass($k * 1024)); - $json[] = array( + $data['rows'][] = [ + 'idx' => count($data['rows']), + 'gb' => $k, + 'count' => $v, + 'class' => StatisticsStyling::ramColorClass($k * 1024), + ]; + $json[] = [ 'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)], - 'label' => (string)$k, 'value' => $v, - ); + ]; ++$id; } self::capChart($json, $data['rows'], 0.92); @@ -167,62 +212,47 @@ class SubPage Render::addTemplate('memory', $data); } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showKvmState($filterSet) + private static function showKvmState(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); - $colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6'); + $colors = ['UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6']; $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args); - $lines = array(); - $json = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $lines = []; + $json = []; + foreach ($res as $row) { + $row['idx'] = count($lines); $lines[] = $row; $json[] = array( - 'color' => isset($colors[$row['kvmstate']]) ? $colors[$row['kvmstate']] : '#000', - 'label' => $row['kvmstate'], + 'color' => $colors[$row['kvmstate']] ?? '#000', 'value' => $row['count'], ); } Render::addTemplate('kvmstate', array('rows' => $lines, 'json' => json_encode($json))); } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showId44($filterSet) + private static function showId44(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY id44mb", $args); $lines = array(); $total = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $total += $row['count']; - $gb = (int)ceil($row['id44mb'] / 1024); - for ($i = 1; $i < count(StatisticsFilter::SIZE_ID44); ++$i) { - if (StatisticsFilter::SIZE_ID44[$i] < $gb) { - continue; - } - if (StatisticsFilter::SIZE_ID44[$i] - $gb >= $gb - StatisticsFilter::SIZE_ID44[$i - 1]) { - --$i; - } - $gb = StatisticsFilter::SIZE_ID44[$i]; - break; - } - if (isset($lines[$gb])) { - $lines[$gb] += $row['count']; - } else { - $lines[$gb] = $row['count']; - } + $gb = self::alignBySteps((int)ceil($row['id44mb'] / 1024), StatisticsFilter::SIZE_PARTITION); + $lines[$gb] = ($lines[$gb] ?? 0) + $row['count']; } asort($lines); $data = array('rows' => array()); $json = array(); $id = 0; foreach (array_reverse($lines, true) as $k => $v) { - $data['rows'][] = array('gb' => $k, 'count' => $v, 'class' => StatisticsStyling::hddColorClass($k)); + $data['rows'][] = [ + 'idx' => count($data['rows']), + 'gb' => $k, + 'count' => $v, + 'class' => StatisticsStyling::hddColorClass($k), + ]; if ($k === 0) { $color = '#e55'; } else { @@ -230,7 +260,6 @@ class SubPage } $json[] = array( 'color' => $color, - 'label' => (string)$k, 'value' => $v, ); } @@ -239,19 +268,18 @@ class SubPage Render::addTemplate('id44', $data); } - /** - * @param \StatisticsFilterSet $filterSet - */ - private static function showLatestMachines($filterSet) + private static function showLatestMachines(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10; - $res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine m $join" - . " WHERE firstseen > :cutoff AND $where ORDER BY firstseen DESC LIMIT 32", $args); + $res = Database::simpleQuery("SELECT m.machineuuid, m.clientip, m.hostname, m.firstseen, m.mbram, m.kvmstate, m.id44mb + FROM machine m $join + WHERE firstseen > :cutoff AND $where + ORDER BY firstseen DESC LIMIT 32", $args); $rows = array(); $count = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if (empty($row['hostname'])) { $row['hostname'] = $row['clientip']; } @@ -259,9 +287,9 @@ class SubPage $row['firstseen'] = Util::prettyTime($row['firstseen']); $row['gbram'] = round(round($row['mbram'] / 500) / 2, 1); // Trial and error until we got "expected" rounding.. $row['gbtmp'] = round($row['id44mb'] / 1024); - $row['ramclass'] = StatisticsStyling::ramColorClass($row['mbram']); + $row['ramclass'] = StatisticsStyling::ramColorClass((int)$row['mbram']); $row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']); - $row['hddclass'] = StatisticsStyling::hddColorClass($row['gbtmp']); + $row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']); $row['kvmicon'] = $row['kvmstate'] === 'ENABLED' ? '✓' : '✗'; if (++$count > 5) { $row['collapse'] = 'collapse'; @@ -277,7 +305,7 @@ class SubPage - private static function capChart(&$json, &$rows, $cutoff, $minSlice = 0.015) + private static function capChart(array &$json, array &$rows, float $cutoff, float $minSlice = 0.015): void { $total = 0; foreach ($json as $entry) { @@ -309,4 +337,28 @@ class SubPage } } + /** + * @param array $json decoded json ~stats data + * @param ?int[] $locations + */ + private static function mangleStatsJson(array $json, ?array $locations, ?string $op): array + { + // Total, On, InUse, Lectures + $retval = [0, 0, 0, 0]; + foreach ($json['usage'] as $lid => $data) { + $lid = (int)$lid; + if ($locations === null + || ($op === '!=' && !in_array($lid, $locations)) + || ($op !== '!=' && in_array($lid, $locations))) { + $retval[0] += $data['t']; + $retval[1] += $data['o'] ?? 0; + $retval[2] += $data['u'] ?? 0; + if (isset($data['event'])) { + $retval[3] += 1; + } + } + } + return $retval; + } + } |