setAllowedLocationsFromPermission('view.summary')) { Message::addError('main.no-permission'); Util::redirect('?do=main'); } // Prepare chart colors self::$STATS_COLORS = []; for ($i = 0; $i < 10; ++$i) { self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex( (int)((($i + 1) * ($i + 1)) / .3922)), dechex((int)(abs((5 - $i) * 51)))); } $filterSet->filterNonClients(); StatisticsFilter::renderFilterBox('summary', $filterSet); Render::openTag('div', array('class' => 'row')); self::showSummary($filterSet); self::showMemory($filterSet); self::showId44($filterSet); self::showKvmState($filterSet); self::showLatestMachines($filterSet); self::showSystemModels($filterSet); Render::closeTag('div'); } 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); $on = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args); $used = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE state = 'OCCUPIED' AND ($where)", $args); $hdd = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE badsectors >= 10 AND ($where)", $args); if ($on['val'] != 0) { $usedpercent = round($used['val'] / $on['val'] * 100); } else { $usedpercent = 0; } $data = [ 'known' => $known['val'], 'online' => $on['val'], 'used' => $used['val'], 'usedpercent' => $usedpercent, 'badhdd' => $hdd['val'], ]; // Graph $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 { if (is_array($locFilter->argument)) { $locations = $locFilter->argument; } else { $locations = [$locFilter->argument]; } $op = $locFilter->op; } //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; } } 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); $data['runmode'] = $res['cnt']; } // Draw Render::addTemplate('summary', $data); } 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 = []; $json = []; $id = 0; foreach ($res as $row) { if (empty($row['systemmodel'])) { continue; } settype($row['count'], 'integer'); $row['urlsystemmodel'] = urlencode($row['systemmodel']); $row['idx'] = count($lines); $lines[] = $row; $json[] = [ 'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)], 'value' => $row['count'], ]; ++$id; } self::capChart($json, $lines, 0.92); Render::addTemplate('cpumodels', ['rows' => $lines, 'json' => json_encode($json)]); } 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 = []; 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 = ['rows' => []]; $json = []; $id = 0; foreach (array_reverse($lines, true) as $k => $v) { $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)], 'value' => $v, ]; ++$id; } self::capChart($json, $data['rows'], 0.92); $data['json'] = json_encode($json); Render::addTemplate('memory', $data); } private static function showKvmState(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $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 = []; $json = []; foreach ($res as $row) { $row['idx'] = count($lines); $lines[] = $row; $json[] = array( 'color' => $colors[$row['kvmstate']] ?? '#000', 'value' => $row['count'], ); } Render::addTemplate('kvmstate', array('rows' => $lines, 'json' => json_encode($json))); } 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; foreach ($res as $row) { $total += $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'][] = [ 'idx' => count($data['rows']), 'gb' => $k, 'count' => $v, 'class' => StatisticsStyling::hddColorClass($k), ]; if ($k === 0) { $color = '#e55'; } else { $color = self::$STATS_COLORS[$id++ % count(self::$STATS_COLORS)]; } $json[] = array( 'color' => $color, 'value' => $v, ); } self::capChart($json, $data['rows'], 0.95); $data['json'] = json_encode($json); Render::addTemplate('id44', $data); } private static function showLatestMachines(StatisticsFilterSet $filterSet): void { $filterSet->makeFragments($where, $join, $args); $args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10; $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; foreach ($res as $row) { if (empty($row['hostname'])) { $row['hostname'] = $row['clientip']; } $row['firstseen_int'] = $row['firstseen']; $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((int)$row['mbram']); $row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']); $row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']); $row['kvmicon'] = $row['kvmstate'] === 'ENABLED' ? '✓' : '✗'; if (++$count > 5) { $row['collapse'] = 'collapse'; } $rows[] = $row; } Render::addTemplate('newclients', array('rows' => $rows, 'openbutton' => $count > 5)); } /* * HELPERS */ private static function capChart(array &$json, array &$rows, float $cutoff, float $minSlice = 0.015): void { $total = 0; foreach ($json as $entry) { $total += $entry['value']; } if ($total === 0) { return; } $cap = ceil($total * $cutoff); $accounted = 0; $id = 0; foreach ($json as $entry) { if (($accounted >= $cap || $entry['value'] / $total < $minSlice) && $id >= 3) { break; } ++$id; $accounted += $entry['value']; } for ($i = $id; $i < count($rows); ++$i) { $rows[$i]['collapse'] = 'collapse'; } $json = array_slice($json, 0, $id); if ($accounted / $total < 0.99) { $json[] = array( 'color' => '#eee', 'label' => 'invalid', 'value' => ($total - $accounted), ); } } /** * @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; } }