summaryrefslogblamecommitdiffstats
path: root/modules-available/statistics/pages/summary.inc.php
blob: 71faff47e4b1e47653dec30df43cf5620404905c (plain) (tree)
1
2
3
4
5
6
7
8
9
10









                                                       


                                                                           



                                         
                                                          
                                                               












                                                                                                                                               
                                                                         
                                                                











                                                     
                                                                           
         
                                                                



                                                                                                                                                  




                                                                              
                         




                                                      
                  
                        
                                









                                                                                                                             
                                





                                                                            
                         


































                                                                                                        
                         
                 











                                                                                                           
                                                       

                                                                                                                                    










                                                            
                                                                
                                                                                                                                     
                                                                                                                    

                            
                        
                                        



                                                          
                                                                                
                                                    
                                        
                                   
                                                                                                 
                                                         
                          


                                                    














                                                                                                   






                                                      


                                                                                                    
                            
                                        

                                                                                                              

                              

                                       

                                                                   






                                                                                       
                                                                                                 
                                              
                          



                                                           







                                                        
                                                                
                                                                                                                    

                                                                                                       

                            
                                        
                                                    

                                        
                                                                               


                                                         
                                                                                                       






                                                    
                                                                
                                                                                                                                            

                                 
                                        
                                                
                                                                                                                     
                                                                         





                                                                   





                                                                                






                                                                                                 




                                                           





                                                   
                                                                                  
         
                                                                

                                                                          

                                                                                                                                      

                                                                          

                                
                                        
























































                                                                                                                                       
























                                                                                          
 
<?php

class SubPage
{

	private static $STATS_COLORS;

	public static function doPreprocess()
	{
		User::assertPermission('view.summary');
		if (!Module::isAvailable('js_chart')) {
			ErrorHandler::traceError('js_chart not available');
		}
	}

	public static function doRender()
	{
		$filters = StatisticsFilter::parseQuery();
		$filterSet = new StatisticsFilterSet($filters);

		if (!$filterSet->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((($i + 1) * ($i + 1)) / .3922), dechex(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');
	}

	/**
	 * @param \StatisticsFilterSet $filterSet
	 */
	private static function showSummary(StatisticsFilterSet $filterSet)
	{
		$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
		// 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");
			$labels = [];
			$points1 = [];
			$points2 = [];
			$lectures = [];
			// Get max from 3 consecutive values, which should be 3*5 = 15m
			$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, true);
		}
		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);
	}

	/**
	 * @param \StatisticsFilterSet $filterSet
	 */
	private static function showSystemModels($filterSet)
	{
		$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;
	}

	/**
	 * @param \StatisticsFilterSet $filterSet
	 */
	private static function showMemory($filterSet)
	{
		$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);
	}

	/**
	 * @param \StatisticsFilterSet $filterSet
	 */
	private static function showKvmState($filterSet)
	{
		$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)));
	}

	/**
	 * @param \StatisticsFilterSet $filterSet
	 */
	private static function showId44($filterSet)
	{
		$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);
	}

	/**
	 * @param \StatisticsFilterSet $filterSet
	 */
	private static function showLatestMachines(StatisticsFilterSet $filterSet)
	{
		$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($row['mbram']);
			$row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']);
			$row['hddclass'] = StatisticsStyling::hddColorClass($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(&$json, &$rows, $cutoff, $minSlice = 0.015)
	{
		$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
	 * @param ?string $op
	 */
	private static function mangleStatsJson(array $json, $locations, $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;
	}

}