From a03f1f478a5f75de79ff2348461c8e9ed9872d3f Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 7 Nov 2019 10:30:37 +0100 Subject: [statistics] Modularize --- modules-available/statistics/inc/filter.inc.php | 308 ----- modules-available/statistics/inc/filterset.inc.php | 143 --- modules-available/statistics/inc/parser.inc.php | 4 +- .../statistics/inc/statisticsfilter.inc.php | 517 +++++++++ .../statistics/inc/statisticsfilterset.inc.php | 143 +++ .../statistics/inc/statisticsstyling.inc.php | 42 + modules-available/statistics/page.inc.php | 1176 +------------------- modules-available/statistics/pages/list.inc.php | 129 +++ modules-available/statistics/pages/machine.inc.php | 436 ++++++++ modules-available/statistics/pages/summary.inc.php | 316 ++++++ 10 files changed, 1645 insertions(+), 1569 deletions(-) delete mode 100644 modules-available/statistics/inc/filter.inc.php delete mode 100644 modules-available/statistics/inc/filterset.inc.php create mode 100644 modules-available/statistics/inc/statisticsfilter.inc.php create mode 100644 modules-available/statistics/inc/statisticsfilterset.inc.php create mode 100644 modules-available/statistics/inc/statisticsstyling.inc.php create mode 100644 modules-available/statistics/pages/list.inc.php create mode 100644 modules-available/statistics/pages/machine.inc.php create mode 100644 modules-available/statistics/pages/summary.inc.php diff --git a/modules-available/statistics/inc/filter.inc.php b/modules-available/statistics/inc/filter.inc.php deleted file mode 100644 index 46de467b..00000000 --- a/modules-available/statistics/inc/filter.inc.php +++ /dev/null @@ -1,308 +0,0 @@ -column = trim($column); - $this->operator = trim($operator); - $this->argument = is_array($argument) ? $argument : trim($argument); - } - - /* returns a where clause and adds needed operators to the passed array */ - public function whereClause(&$args, &$joins) - { - $key = Filter::getNewKey($this->column); - $addendum = ''; - - /* check if we have to do some parsing*/ - if (Page_Statistics::$columns[$this->column]['type'] === 'date') { - $args[$key] = strtotime($this->argument); - } else { - $args[$key] = $this->argument; - if ($this->operator === '~' || $this->operator === '!~') { - $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]); - $addendum = " ESCAPE '='"; - } - } - - $op = $this->operator; - if ($this->operator == '~') { - $op = 'LIKE'; - } elseif ($this->operator == '!~') { - $op = 'NOT LIKE'; - } - - return $this->column . ' ' . $op . ' :' . $key . $addendum; - } - - /* parse a query into an array of filters */ - public static function parseQuery($query) - { - $operators = ['<=', '>=', '!=', '!~', '=', '~', '<', '>']; - $filters = []; - if (empty($query)) - return $filters; - foreach (explode(self::DELIMITER, $query) as $q) { - $q = trim($q); - if (empty($q)) - continue; - // Special case: User pasted UUID, turn into filter - if (preg_match('/^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/', $q)) { - $filters[] = new Filter('machineuuid', '=', $q); - continue; - } - // Special case: User pasted IP, turn into filter - if (preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $q)) { - $filters[] = new Filter('clientip', '=', $q); - continue; - } - /* find position of first operator */ - $pos = 10000; - $operator = false; - foreach ($operators as $op) { - $newpos = strpos($q, $op); - if ($newpos > -1 && ($newpos < $pos)) { - $pos = $newpos; - $operator = $op; - } - } - if ($pos == 10000) { - error_log("couldn't find operator in segment " . $q); - /* TODO */ - continue; - } - $lhs = trim(substr($q, 0, $pos)); - $rhs = trim(substr($q, $pos + strlen($operator))); - - if ($lhs === 'gbram') { - $filters[] = new RamGbFilter($operator, $rhs); - } elseif ($lhs === 'runtime') { - $filters[] = new RuntimeFilter($operator, $rhs); - } elseif ($lhs === 'state') { - $filters[] = new StateFilter($operator, $rhs); - } elseif ($lhs === 'hddgb') { - $filters[] = new Id44Filter($operator, $rhs); - } elseif ($lhs === 'location') { - $filters[] = new LocationFilter($operator, $rhs); - } elseif ($lhs === 'subnet') { - $filters[] = new SubnetFilter($operator, $rhs); - } else { - if (array_key_exists($lhs, Page_Statistics::$columns) && Page_Statistics::$columns[$lhs]['column']) { - $filters[] = new Filter($lhs, $operator, $rhs); - } else { - Message::addError('invalid-filter-key', $lhs); - } - } - } - - return $filters; - } -} - -class RamGbFilter extends Filter -{ - public function __construct($operator, $argument) - { - parent::__construct('mbram', $operator, $argument); - } - - public function whereClause(&$args, &$joins) - { - global $SIZE_RAM; - $lower = floor(Page_Statistics::findBestValue($SIZE_RAM, (int)$this->argument, false) * 1024 - 100); - $upper = ceil(Page_Statistics::findBestValue($SIZE_RAM, (int)$this->argument, true) * 1024 + 100); - if ($this->operator == '=') { - return " mbram BETWEEN $lower AND $upper"; - } elseif ($this->operator == '<') { - return " mbram < $lower"; - } elseif ($this->operator == '<=') { - return " mbram <= $upper"; - } elseif ($this->operator == '>') { - return " mbram > $upper"; - } elseif ($this->operator == '>=') { - return " mbram >= $lower"; - } elseif ($this->operator == '!=') { - return " (mbram < $lower OR mbram > $upper)"; - } else { - error_log("unimplemented operator in RamGbFilter: $this->operator"); - - return ' 1'; - } - } -} - -class RuntimeFilter extends Filter -{ - public function __construct($operator, $argument) - { - parent::__construct('lastboot', $operator, $argument); - } - - public function whereClause(&$args, &$joins) - { - global $SIZE_RAM; - $upper = time() - (int)$this->argument * 3600; - $lower = $upper - 3600; - $common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND"; - if ($this->operator == '=') { - return "$common ({$this->column} BETWEEN $lower AND $upper)"; - } elseif ($this->operator == '<') { - return "$common {$this->column} > $upper"; - } elseif ($this->operator == '<=') { - return "$common {$this->column} > $lower"; - } elseif ($this->operator == '>') { - return "$common {$this->column} < $lower"; - } elseif ($this->operator == '>=') { - return "$common {$this->column} < $upper"; - } elseif ($this->operator == '!=') { - return "$common ({$this->column} < $lower OR {$this->column} > $upper)"; - } else { - error_log("unimplemented operator in RuntimeFilter: $this->operator"); - return ' 1'; - } - } -} - -class Id44Filter extends Filter -{ - public function __construct($operator, $argument) - { - parent::__construct('id44mb', $operator, $argument); - } - - public function whereClause(&$args, &$joins) - { - global $SIZE_ID44; - if ($this->operator === '=' || $this->operator === '!=') { - $lower = floor(Page_Statistics::findBestValue($SIZE_ID44, $this->argument, false) * 1024 - 100); - $upper = ceil(Page_Statistics::findBestValue($SIZE_ID44, $this->argument, true) * 1024 + 100); - } else { - $lower = $upper = round($this->argument * 1024); - } - - if ($this->operator === '=') { - return " id44mb BETWEEN $lower AND $upper"; - } elseif ($this->operator === '!=') { - return " id44mb < $lower OR id44mb > $upper"; - } elseif ($this->operator === '<=') { - return " id44mb <= $upper"; - } elseif ($this->operator === '>=') { - return " id44mb >= $lower"; - } elseif ($this->operator === '<') { - return " id44mb < $lower"; - } elseif ($this->operator === '>') { - return " id44mb > $upper"; - } else { - error_log("unimplemented operator in Id44Filter: $this->operator"); - - return ' 1'; - } - } -} - -class StateFilter extends Filter -{ - public function __construct($operator, $argument) - { - parent::__construct(null, $operator, $argument); - } - - public function whereClause(&$args, &$joins) - { - $map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ]; - $neg = $this->operator == '!=' ? 'NOT ' : ''; - if (array_key_exists($this->argument, $map)) { - $key = Filter::getNewKey($this->column); - $args[$key] = $map[$this->argument]; - return " machine.state $neg IN ( :$key ) "; - } else { - Message::addError('invalid-filter-argument', 'state', $this->argument); - return ' 1'; - } - } -} - -class LocationFilter extends Filter -{ - public function __construct($operator, $argument) - { - parent::__construct('locationid', $operator, $argument); - } - - public function whereClause(&$args, &$joins) - { - $recursive = (substr($this->operator, -1) === '~'); - $this->operator = str_replace('~', '=', $this->operator); - - if (is_array($this->argument)) { - if ($recursive) - Util::traceError('Cannot use ~ operator for location with array'); - } else { - settype($this->argument, 'int'); - } - $neg = $this->operator === '=' ? '' : 'NOT'; - if ($this->argument === 0) { - return "machine.locationid IS $neg NULL"; - } else { - $key = Filter::getNewKey($this->column); - if ($recursive) { - $args[$key] = array_keys(Location::getRecursiveFlat($this->argument)); - } else { - $args[$key] = $this->argument; - } - return "machine.locationid $neg IN (:$key)"; - } - } -} - -class SubnetFilter extends Filter -{ - public function __construct($operator, $argument) - { - parent::__construct(null, $operator, $argument); - } - - public function whereClause(&$args, &$joins) - { - $argument = preg_replace('/[^0-9\.:]/', '', $this->argument); - return " clientip LIKE '$argument%'"; - } -} - -class IsClientFilter extends Filter -{ - public function __construct($argument) - { - parent::__construct(null, null, $argument); - } - - public function whereClause(&$args, &$joins) - { - if ($this->argument) { - $joins[] = ' LEFT JOIN runmode USING (machineuuid)'; - return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)"; - } - $joins[] = ' INNER JOIN runmode USING (machineuuid)'; - return "runmode.isclient = 0"; - } -} diff --git a/modules-available/statistics/inc/filterset.inc.php b/modules-available/statistics/inc/filterset.inc.php deleted file mode 100644 index 774bfd18..00000000 --- a/modules-available/statistics/inc/filterset.inc.php +++ /dev/null @@ -1,143 +0,0 @@ -filters = $filters; - } - - public function setSort($col, $direction) - { - $direction = ($direction === 'DESC' ? 'DESC' : 'ASC'); - - if (!is_string($col) || !array_key_exists($col, Page_Statistics::$columns)) { - /* default sorting column is clientip */ - $col = 'clientip'; - } - if ($col === $this->sortColumn && $direction === $this->sortDirection) - return; - $this->cache = false; - $this->sortDirection = $direction; - $this->sortColumn = $col; - } - - public function makeFragments(&$where, &$join, &$sort, &$args) - { - if ($this->cache !== false) { - $where = $this->cache['where']; - $join = $this->cache['join']; - $sort = $this->cache['sort']; - $args = $this->cache['args']; - return; - } - /* generate where clause & arguments */ - $where = ''; - $joins = []; - $sort = ""; - $args = []; - if (empty($this->filters)) { - $where = ' 1 '; - } else { - foreach ($this->filters as $filter) { - $sep = ($where != '' ? ' AND ' : ''); - $where .= $sep . $filter->whereClause($args, $joins); - } - } - $join = implode(' ', array_unique($joins)); - - $col = $this->sortColumn; - $isMapped = array_key_exists('map_sort', Page_Statistics::$columns[$col]); - $concreteCol = ($isMapped ? Page_Statistics::$columns[$col]['map_sort'] : $col) ; - - if ($concreteCol === 'clientip') { - $concreteCol = "INET_ATON(clientip)"; - } - - $sort = " ORDER BY " . $concreteCol . " " . $this->sortDirection - . ", machineuuid ASC"; - $this->cache = compact('where', 'join', 'sort', 'args'); - } - - public function isNoId44Filter() - { - $filter = $this->hasFilter('Id44Filter'); - return $filter !== false && $filter->argument == 0; - } - - public function getSortDirection() - { - return $this->sortDirection; - } - - public function getSortColumn() - { - return $this->sortColumn; - } - - public function filterNonClients() - { - if (Module::get('runmode') === false || $this->hasFilter('IsClientFilter') !== false) - return; - $this->cache = false; - // Runmode module exists, add filter - $this->filters[] = new IsClientFilter(true); - } - - /** - * @param string $type filter type (class name) - * @return false|Filter The filter, false if not found - */ - public function hasFilter($type) - { - foreach ($this->filters as $filter) { - if (get_class($filter) === $type) { - return $filter; - } - } - return false; - } - - /** - * Add a location filter based on the allowed permissions for the given permission. - * Returns false if the user doesn't have the given permission for any location. - * - * @param string $permission permission to use - * @return bool false if no permission for any location, true otherwise - */ - public function setAllowedLocationsFromPermission($permission) - { - $locs = User::getAllowedLocations($permission); - if (empty($locs)) - return false; - if (in_array(0, $locs)) { - if (!isset($this->filters['permissions'])) - return true; - unset($this->filters['permissions']); - } else { - $this->filters['permissions'] = new LocationFilter('=', $locs); - } - $this->cache = false; - return true; - } - - /** - * @return false|array - */ - public function getAllowedLocations() - { - if (isset($this->filters['permissions']->argument) && is_array($this->filters['permissions']->argument)) - return $this->filters['permissions']->argument; - return false; - } - -} diff --git a/modules-available/statistics/inc/parser.inc.php b/modules-available/statistics/inc/parser.inc.php index fe850109..764d18c1 100644 --- a/modules-available/statistics/inc/parser.inc.php +++ b/modules-available/statistics/inc/parser.inc.php @@ -74,6 +74,8 @@ class Parser { if (preg_match('/^\s*Size:\s*(.*?)\s*$/i', $line, $out)) { $row['extram'] = true; if (preg_match('/(\d+)\s*(\w)i?B/i', $out[1])) { + if (self::convertSize($out[1], 'M', false) < 35) + continue; // TODO: Parsing this line by line is painful. Check for other indicators, like Locator $ramslot['size'] = self::convertSize($out[1], 'G'); } elseif (!isset($row['ramslot']) || (count($row['ramslot']) < 8 && (!isset($row['ramslotcount']) || $row['ramslotcount'] <= 8))) { $ramslot['size'] = '_____'; @@ -126,7 +128,7 @@ class Parser { { if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out)) return false; - $val = (int)$out[1] * self::LOOKUP[$out[2]]; + $val = (int)$out[1] * self::LOOKUP[strtoupper($out[2])]; if (!array_key_exists($scale, self::LOOKUP)) { foreach (self::LOOKUP as $k => $v) { if ($k === '' || $val / 8 >= $v || abs($val - $v) < 50) { diff --git a/modules-available/statistics/inc/statisticsfilter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php new file mode 100644 index 00000000..1556a1e0 --- /dev/null +++ b/modules-available/statistics/inc/statisticsfilter.inc.php @@ -0,0 +1,517 @@ +column = trim($column); + $this->operator = trim($operator); + $this->argument = is_array($argument) ? $argument : trim($argument); + } + + /* returns a where clause and adds needed operators to the passed array */ + public function whereClause(&$args, &$joins) + { + $key = StatisticsFilter::getNewKey($this->column); + $addendum = ''; + + /* check if we have to do some parsing*/ + if (self::$columns[$this->column]['type'] === 'date') { + $args[$key] = strtotime($this->argument); + } else { + $args[$key] = $this->argument; + if ($this->operator === '~' || $this->operator === '!~') { + $args[$key] = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $args[$key]); + $addendum = " ESCAPE '='"; + } + } + + $op = $this->operator; + if ($this->operator == '~') { + $op = 'LIKE'; + } elseif ($this->operator == '!~') { + $op = 'NOT LIKE'; + } + + return $this->column . ' ' . $op . ' :' . $key . $addendum; + } + + /* parse a query into an array of filters */ + public static function parseQuery($query) + { + $operators = ['<=', '>=', '!=', '!~', '=', '~', '<', '>']; + $filters = []; + if (empty($query)) + return $filters; + foreach (explode(self::DELIMITER, $query) as $q) { + $q = trim($q); + if (empty($q)) + continue; + // Special case: User pasted UUID, turn into filter + if (preg_match('/^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/', $q)) { + $filters[] = new StatisticsFilter('machineuuid', '=', $q); + continue; + } + // Special case: User pasted IP, turn into filter + if (preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $q)) { + $filters[] = new StatisticsFilter('clientip', '=', $q); + continue; + } + /* find position of first operator */ + $pos = 10000; + $operator = false; + foreach ($operators as $op) { + $newpos = strpos($q, $op); + if ($newpos > -1 && ($newpos < $pos)) { + $pos = $newpos; + $operator = $op; + } + } + if ($pos == 10000) { + error_log("couldn't find operator in segment " . $q); + /* TODO */ + continue; + } + $lhs = trim(substr($q, 0, $pos)); + $rhs = trim(substr($q, $pos + strlen($operator))); + + if ($lhs === 'gbram') { + $filters[] = new RamGbStatisticsFilter($operator, $rhs); + } elseif ($lhs === 'runtime') { + $filters[] = new RuntimeStatisticsFilter($operator, $rhs); + } elseif ($lhs === 'state') { + $filters[] = new StateStatisticsFilter($operator, $rhs); + } elseif ($lhs === 'hddgb') { + $filters[] = new Id44StatisticsFilter($operator, $rhs); + } elseif ($lhs === 'location') { + $filters[] = new LocationStatisticsFilter($operator, $rhs); + } elseif ($lhs === 'subnet') { + $filters[] = new SubnetStatisticsFilter($operator, $rhs); + } else { + if (array_key_exists($lhs, self::$columns) && self::$columns[$lhs]['column']) { + $filters[] = new StatisticsFilter($lhs, $operator, $rhs); + } else { + Message::addError('invalid-filter-key', $lhs); + } + } + } + + return $filters; + } + + /** + * @param \StatisticsFilterSet $filterSet + */ + public static function renderFilterBox($show, $filterSet, $query) + { + $data = array( + 'show' => $show, + 'query' => $query, + 'delimiter' => StatisticsFilter::DELIMITER, + 'sortDirection' => $filterSet->getSortDirection(), + 'sortColumn' => $filterSet->getSortColumn(), + 'columns' => json_encode(StatisticsFilter::$columns), + ); + + if ($show === 'list') { + $data['listButtonClass'] = 'active'; + $data['statButtonClass'] = ''; + } else { + $data['listButtonClass'] = ''; + $data['statButtonClass'] = 'active'; + } + + + $locsFlat = array(); + if (Module::isAvailable('locations')) { + $allowed = $filterSet->getAllowedLocations(); + foreach (Location::getLocations() as $loc) { + $locsFlat['L' . $loc['locationid']] = array( + 'pad' => $loc['locationpad'], + 'name' => $loc['locationname'], + 'disabled' => $allowed !== false && !in_array($loc['locationid'], $allowed), + ); + } + } + + Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']); + $data['locations'] = json_encode($locsFlat); + Render::addTemplate('filterbox', $data); + } + + private static $query = false; + + public static function getQuery() + { + if (self::$query === false) { + self::$query = Request::any('filters', false, 'string'); + if (self::$query === false) { + self::$query = 'lastseen > ' . gmdate('Y-m-d', strtotime('-30 day')); + } + } + return self::$query; + } + + /* + * Simple filters that map directly to DB columns + */ + + const OP_ORDINAL = ['!=', '<=', '>=', '=', '<', '>']; + const OP_STRCMP = ['!~', '~', '=', '!=']; + const OP_NOMINAL = ['!=', '=']; + public static $columns; + + /** + * Do this here instead of const since we need to check for available modules while building array. + */ + public static function initConstants() + { + + self::$columns = [ + 'machineuuid' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'string', + 'column' => true, + ], + 'macaddr' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'string', + 'column' => true, + ], + 'firstseen' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'date', + 'column' => true, + ], + 'lastseen' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'date', + 'column' => true, + ], + 'logintime' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'date', + 'column' => true, + ], + 'realcores' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'column' => true, + ], + 'systemmodel' => [ + 'op' => self::OP_STRCMP, + 'type' => 'string', + 'column' => true, + ], + 'cpumodel' => [ + 'op' => self::OP_STRCMP, + 'type' => 'string', + 'column' => true, + ], + 'hddgb' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'column' => false, + 'map_sort' => 'id44mb' + ], + 'gbram' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'map_sort' => 'mbram', + 'column' => false, + ], + 'kvmstate' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'enum', + 'column' => true, + 'values' => ['ENABLED', 'DISABLED', 'UNSUPPORTED'] + ], + 'badsectors' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'column' => true + ], + 'clientip' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'string', + 'column' => true + ], + 'hostname' => [ + 'op' => self::OP_STRCMP, + 'type' => 'string', + 'column' => true + ], + 'subnet' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'string', + 'column' => false + ], + 'currentuser' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'string', + 'column' => true + ], + 'state' => [ + 'op' => self::OP_NOMINAL, + 'type' => 'enum', + 'column' => true, + 'values' => ['occupied', 'on', 'off', 'idle', 'standby'] + ], + 'live_swapfree' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'column' => true + ], + 'live_memfree' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'column' => true + ], + 'live_tmpfree' => [ + 'op' => self::OP_ORDINAL, + 'type' => 'int', + 'column' => true + ], + ]; + if (Module::isAvailable('locations')) { + self::$columns['location'] = [ + 'op' => self::OP_STRCMP, + 'type' => 'enum', + 'column' => false, + 'values' => array_keys(Location::getLocationsAssoc()), + ]; + } + } + +} + +class RamGbStatisticsFilter extends StatisticsFilter +{ + public function __construct($operator, $argument) + { + parent::__construct('mbram', $operator, $argument); + } + + public function whereClause(&$args, &$joins) + { + $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$this->argument, false) * 1024 - 100); + $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_RAM, (int)$this->argument, true) * 1024 + 100); + if ($this->operator == '=') { + return " mbram BETWEEN $lower AND $upper"; + } elseif ($this->operator == '<') { + return " mbram < $lower"; + } elseif ($this->operator == '<=') { + return " mbram <= $upper"; + } elseif ($this->operator == '>') { + return " mbram > $upper"; + } elseif ($this->operator == '>=') { + return " mbram >= $lower"; + } elseif ($this->operator == '!=') { + return " (mbram < $lower OR mbram > $upper)"; + } else { + error_log("unimplemented operator in RamGbFilter: $this->operator"); + + return ' 1'; + } + } +} + +class RuntimeStatisticsFilter extends StatisticsFilter +{ + public function __construct($operator, $argument) + { + parent::__construct('lastboot', $operator, $argument); + } + + public function whereClause(&$args, &$joins) + { + $upper = time() - (int)$this->argument * 3600; + $lower = $upper - 3600; + $common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND"; + if ($this->operator == '=') { + return "$common ({$this->column} BETWEEN $lower AND $upper)"; + } elseif ($this->operator == '<') { + return "$common {$this->column} > $upper"; + } elseif ($this->operator == '<=') { + return "$common {$this->column} > $lower"; + } elseif ($this->operator == '>') { + return "$common {$this->column} < $lower"; + } elseif ($this->operator == '>=') { + return "$common {$this->column} < $upper"; + } elseif ($this->operator == '!=') { + return "$common ({$this->column} < $lower OR {$this->column} > $upper)"; + } else { + error_log("unimplemented operator in RuntimeFilter: $this->operator"); + return ' 1'; + } + } +} + +class Id44StatisticsFilter extends StatisticsFilter +{ + public function __construct($operator, $argument) + { + parent::__construct('id44mb', $operator, $argument); + } + + public function whereClause(&$args, &$joins) + { + if ($this->operator === '=' || $this->operator === '!=') { + $lower = floor(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $this->argument, false) * 1024 - 100); + $upper = ceil(StatisticsFilter::findBestValue(StatisticsFilter::SIZE_ID44, $this->argument, true) * 1024 + 100); + } else { + $lower = $upper = round($this->argument * 1024); + } + + if ($this->operator === '=') { + return " id44mb BETWEEN $lower AND $upper"; + } elseif ($this->operator === '!=') { + return " id44mb < $lower OR id44mb > $upper"; + } elseif ($this->operator === '<=') { + return " id44mb <= $upper"; + } elseif ($this->operator === '>=') { + return " id44mb >= $lower"; + } elseif ($this->operator === '<') { + return " id44mb < $lower"; + } elseif ($this->operator === '>') { + return " id44mb > $upper"; + } else { + error_log("unimplemented operator in Id44Filter: $this->operator"); + + return ' 1'; + } + } +} + +class StateStatisticsFilter extends StatisticsFilter +{ + public function __construct($operator, $argument) + { + parent::__construct(null, $operator, $argument); + } + + public function whereClause(&$args, &$joins) + { + $map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ]; + $neg = $this->operator == '!=' ? 'NOT ' : ''; + if (array_key_exists($this->argument, $map)) { + $key = StatisticsFilter::getNewKey($this->column); + $args[$key] = $map[$this->argument]; + return " machine.state $neg IN ( :$key ) "; + } else { + Message::addError('invalid-filter-argument', 'state', $this->argument); + return ' 1'; + } + } +} + +class LocationStatisticsFilter extends StatisticsFilter +{ + public function __construct($operator, $argument) + { + parent::__construct('locationid', $operator, $argument); + } + + public function whereClause(&$args, &$joins) + { + $recursive = (substr($this->operator, -1) === '~'); + $this->operator = str_replace('~', '=', $this->operator); + + if (is_array($this->argument)) { + if ($recursive) + Util::traceError('Cannot use ~ operator for location with array'); + } else { + settype($this->argument, 'int'); + } + $neg = $this->operator === '=' ? '' : 'NOT'; + if ($this->argument === 0) { + return "machine.locationid IS $neg NULL"; + } else { + $key = StatisticsFilter::getNewKey($this->column); + if ($recursive) { + $args[$key] = array_keys(Location::getRecursiveFlat($this->argument)); + } else { + $args[$key] = $this->argument; + } + return "machine.locationid $neg IN (:$key)"; + } + } +} + +class SubnetStatisticsFilter extends StatisticsFilter +{ + public function __construct($operator, $argument) + { + parent::__construct(null, $operator, $argument); + } + + public function whereClause(&$args, &$joins) + { + $argument = preg_replace('/[^0-9\.:]/', '', $this->argument); + return " clientip LIKE '$argument%'"; + } +} + +class IsClientStatisticsFilter extends StatisticsFilter +{ + public function __construct($argument) + { + parent::__construct(null, null, $argument); + } + + public function whereClause(&$args, &$joins) + { + if ($this->argument) { + $joins[] = ' LEFT JOIN runmode USING (machineuuid)'; + return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)"; + } + $joins[] = ' INNER JOIN runmode USING (machineuuid)'; + return "runmode.isclient = 0"; + } + +} + +StatisticsFilter::initConstants(); \ No newline at end of file diff --git a/modules-available/statistics/inc/statisticsfilterset.inc.php b/modules-available/statistics/inc/statisticsfilterset.inc.php new file mode 100644 index 00000000..c2642850 --- /dev/null +++ b/modules-available/statistics/inc/statisticsfilterset.inc.php @@ -0,0 +1,143 @@ +filters = $filters; + } + + public function setSort($col, $direction) + { + $direction = ($direction === 'DESC' ? 'DESC' : 'ASC'); + + if (!is_string($col) || !array_key_exists($col, StatisticsFilter::$columns)) { + /* default sorting column is clientip */ + $col = 'clientip'; + } + if ($col === $this->sortColumn && $direction === $this->sortDirection) + return; + $this->cache = false; + $this->sortDirection = $direction; + $this->sortColumn = $col; + } + + public function makeFragments(&$where, &$join, &$sort, &$args) + { + if ($this->cache !== false) { + $where = $this->cache['where']; + $join = $this->cache['join']; + $sort = $this->cache['sort']; + $args = $this->cache['args']; + return; + } + /* generate where clause & arguments */ + $where = ''; + $joins = []; + $sort = ""; + $args = []; + if (empty($this->filters)) { + $where = ' 1 '; + } else { + foreach ($this->filters as $filter) { + $sep = ($where != '' ? ' AND ' : ''); + $where .= $sep . $filter->whereClause($args, $joins); + } + } + $join = implode(' ', array_unique($joins)); + + $col = $this->sortColumn; + $isMapped = array_key_exists('map_sort', StatisticsFilter::$columns[$col]); + $concreteCol = ($isMapped ? StatisticsFilter::$columns[$col]['map_sort'] : $col) ; + + if ($concreteCol === 'clientip') { + $concreteCol = "INET_ATON(clientip)"; + } + + $sort = " ORDER BY " . $concreteCol . " " . $this->sortDirection + . ", machineuuid ASC"; + $this->cache = compact('where', 'join', 'sort', 'args'); + } + + public function isNoId44Filter() + { + $filter = $this->hasFilter('Id44Filter'); + return $filter !== false && $filter->argument == 0; + } + + public function getSortDirection() + { + return $this->sortDirection; + } + + public function getSortColumn() + { + return $this->sortColumn; + } + + public function filterNonClients() + { + if (Module::get('runmode') === false || $this->hasFilter('IsClientFilter') !== false) + return; + $this->cache = false; + // Runmode module exists, add filter + $this->filters[] = new IsClientStatisticsFilter(true); + } + + /** + * @param string $type filter type (class name) + * @return false|StatisticsFilter The filter, false if not found + */ + public function hasFilter($type) + { + foreach ($this->filters as $filter) { + if (get_class($filter) === $type) { + return $filter; + } + } + return false; + } + + /** + * Add a location filter based on the allowed permissions for the given permission. + * Returns false if the user doesn't have the given permission for any location. + * + * @param string $permission permission to use + * @return bool false if no permission for any location, true otherwise + */ + public function setAllowedLocationsFromPermission($permission) + { + $locs = User::getAllowedLocations($permission); + if (empty($locs)) + return false; + if (in_array(0, $locs)) { + if (!isset($this->filters['permissions'])) + return true; + unset($this->filters['permissions']); + } else { + $this->filters['permissions'] = new LocationStatisticsFilter('=', $locs); + } + $this->cache = false; + return true; + } + + /** + * @return false|array + */ + public function getAllowedLocations() + { + if (isset($this->filters['permissions']->argument) && is_array($this->filters['permissions']->argument)) + return $this->filters['permissions']->argument; + return false; + } + +} diff --git a/modules-available/statistics/inc/statisticsstyling.inc.php b/modules-available/statistics/inc/statisticsstyling.inc.php new file mode 100644 index 00000000..1fd1d326 --- /dev/null +++ b/modules-available/statistics/inc/statisticsstyling.inc.php @@ -0,0 +1,42 @@ +=', '=', '<', '>']; - const OP_STRCMP = ['!~', '~', '=', '!=']; - public static $columns; - private $query; private $show; @@ -26,128 +10,6 @@ class Page_Statistics extends Page */ private $haveSubpage; - /** - * Do this here instead of const since we need to check for available modules while building array. - */ - public static function initConstants() - { - - Page_Statistics::$columns = [ - 'machineuuid' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'string', - 'column' => true, - ], - 'macaddr' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'string', - 'column' => true, - ], - 'firstseen' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'date', - 'column' => true, - ], - 'lastseen' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'date', - 'column' => true, - ], - 'logintime' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'date', - 'column' => true, - ], - 'realcores' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'column' => true, - ], - 'systemmodel' => [ - 'op' => Page_Statistics::OP_STRCMP, - 'type' => 'string', - 'column' => true, - ], - 'cpumodel' => [ - 'op' => Page_Statistics::OP_STRCMP, - 'type' => 'string', - 'column' => true, - ], - 'hddgb' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'column' => false, - 'map_sort' => 'id44mb' - ], - 'gbram' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'map_sort' => 'mbram', - 'column' => false, - ], - 'kvmstate' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'enum', - 'column' => true, - 'values' => ['ENABLED', 'DISABLED', 'UNSUPPORTED'] - ], - 'badsectors' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'column' => true - ], - 'clientip' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'string', - 'column' => true - ], - 'hostname' => [ - 'op' => Page_Statistics::OP_STRCMP, - 'type' => 'string', - 'column' => true - ], - 'subnet' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'string', - 'column' => false - ], - 'currentuser' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'string', - 'column' => true - ], - 'state' => [ - 'op' => Page_Statistics::OP_NOMINAL, - 'type' => 'enum', - 'column' => true, - 'values' => ['occupied', 'on', 'off', 'idle', 'standby'] - ], - 'live_swapfree' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'column' => true - ], - 'live_memfree' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'column' => true - ], - 'live_tmpfree' => [ - 'op' => Page_Statistics::OP_ORDINAL, - 'type' => 'int', - 'column' => true - ], - ]; - if (Module::isAvailable('locations')) { - Page_Statistics::$columns['location'] = [ - 'op' => Page_Statistics::OP_STRCMP, - 'type' => 'enum', - 'column' => false, - 'values' => array_keys(Location::getLocationsAssoc()), - ]; - } - } - protected function doPreprocess() { User::load(); @@ -156,63 +18,65 @@ class Page_Statistics extends Page Util::redirect('?do=Main'); } - $this->show = Request::any('show', false, 'string'); - if ($this->show === false) { - if (User::hasPermission('view.summary')) { - $this->show = 'summary'; - } elseif (User::hasPermission('view.list')) { - $this->show = 'list'; + if (Request::isGet()) { + $this->show = Request::any('show', false, 'string'); + if ($this->show === false) { + if (Request::get('uuid') !== false) { + $this->show = 'machine'; + } elseif (User::hasPermission('view.summary')) { + $this->show = 'summary'; + } elseif (User::hasPermission('view.list')) { + $this->show = 'list'; + } else { + User::assertPermission('view.summary'); + } } else { - User::assertPermission('view.summary'); + $this->show = preg_replace('/[^a-z0-9_\-]/', '', $this->show); } - } else { - $this->show = preg_replace('/[^a-z0-9_\-]/', '', $this->show); - } - - if (file_exists('modules/statistics/pages/' . $this->show . '.inc.php')) { - - require_once 'modules/statistics/pages/' . $this->show . '.inc.php'; - $this->haveSubpage = true; - SubPage::doPreprocess(); - - } else { - $action = Request::post('action'); - if ($action === 'setnotes') { - $uuid = Request::post('uuid', '', 'string'); - $res = Database::queryFirst('SELECT locationid FROM machine WHERE machineuuid = :uuid', - array('uuid' => $uuid)); - if ($res === false) { - Message::addError('unknown-machine', $uuid); - Util::redirect('?do=statistics'); - } - User::assertPermission("machine.note.edit", (int)$res['locationid']); - $text = Request::post('content', null, 'string'); - if (empty($text)) { - $text = null; - } - Database::exec('UPDATE machine SET notes = :text WHERE machineuuid = :uuid', array( - 'uuid' => $uuid, - 'text' => $text, - )); - Message::addSuccess('notes-saved'); - Util::redirect('?do=statistics&uuid=' . $uuid); - } elseif ($action === 'delmachines') { - $this->deleteMachines(); - Util::redirect('?do=statistics', true); - } elseif ($action === 'rebootmachines') { - $this->rebootControl(true); - } elseif ($action === 'shutdownmachines') { - $this->rebootControl(false); + if (file_exists('modules/statistics/pages/' . $this->show . '.inc.php')) { + require_once 'modules/statistics/pages/' . $this->show . '.inc.php'; + $this->haveSubpage = true; + SubPage::doPreprocess(); + } else { + Message::addError('main.invalid-action', $this->show); } - + return; } - if (Request::isPost()) { - // Make sure we don't render any content for POST requests - should be handled above and then - // redirected properly - Util::redirect('?do=statistics'); - } + // POST + $action = Request::post('action'); + if ($action === 'setnotes') { + $uuid = Request::post('uuid', '', 'string'); + $res = Database::queryFirst('SELECT locationid FROM machine WHERE machineuuid = :uuid', + array('uuid' => $uuid)); + if ($res === false) { + Message::addError('unknown-machine', $uuid); + Util::redirect('?do=statistics'); + } + User::assertPermission("machine.note.edit", (int)$res['locationid']); + $text = Request::post('content', null, 'string'); + if (empty($text)) { + $text = null; + } + Database::exec('UPDATE machine SET notes = :text WHERE machineuuid = :uuid', array( + 'uuid' => $uuid, + 'text' => $text, + )); + Message::addSuccess('notes-saved'); + Util::redirect('?do=statistics&uuid=' . $uuid); + } elseif ($action === 'delmachines') { + $this->deleteMachines(); + Util::redirect('?do=statistics', true); + } elseif ($action === 'rebootmachines') { + $this->rebootControl(true); + } elseif ($action === 'shutdownmachines') { + $this->rebootControl(false); + } + + // Make sure we don't render any content for POST requests - should be handled above and then + // redirected properly + Util::redirect('?do=statistics'); } /** @@ -310,881 +174,36 @@ class Page_Statistics extends Page return; } - $uuid = Request::get('uuid', false, 'string'); - if ($uuid !== false) { - $this->showMachine($uuid); - return; - } - - /* read filter */ - $this->query = Request::any('filters', false); - if ($this->query === false) { - $this->query = 'lastseen > ' . gmdate('Y-m-d', strtotime('-30 day')); - } $sortColumn = Request::any('sortColumn'); $sortDirection = Request::any('sortDirection'); - $filters = Filter::parseQuery($this->query); - $filterSet = new FilterSet($filters); + $filters = StatisticsFilter::parseQuery(StatisticsFilter::getQuery()); + $filterSet = new StatisticsFilterSet($filters); $filterSet->setSort($sortColumn, $sortDirection); if (!$filterSet->setAllowedLocationsFromPermission('view.' . $this->show)) { Message::addError('main.no-permission'); Util::redirect('?do=main'); } - - if ($this->show === 'list') { - Render::openTag('div', array('class' => 'row')); - $this->showFilter('list', $filterSet); - Render::closeTag('div'); - $this->showMachineList($filterSet); - return; - } elseif ($this->show === 'summary') { - $filterSet->filterNonClients(); - Render::openTag('div', array('class' => 'row')); - $this->showFilter('summary', $filterSet); - $this->showSummary($filterSet); - $this->showMemory($filterSet); - $this->showId44($filterSet); - $this->showKvmState($filterSet); - $this->showLatestMachines($filterSet); - $this->showSystemModels($filterSet); - Render::closeTag('div'); - } else { - Message::addError('main.value-invalid', 'show', $this->show); - } - } - - /** - * @param \FilterSet $filterSet - */ - private function showFilter($show, $filterSet) - { - $data = array( - 'show' => $show, - 'query' => $this->query, - 'delimiter' => Filter::DELIMITER, - 'sortDirection' => $filterSet->getSortDirection(), - 'sortColumn' => $filterSet->getSortColumn(), - 'columns' => json_encode(Page_Statistics::$columns), - ); - - if ($show === 'list') { - $data['listButtonClass'] = 'active'; - $data['statButtonClass'] = ''; - } else { - $data['listButtonClass'] = ''; - $data['statButtonClass'] = 'active'; - } - - - $locsFlat = array(); - if (Module::isAvailable('locations')) { - $allowed = $filterSet->getAllowedLocations(); - foreach (Location::getLocations() as $loc) { - $locsFlat['L' . $loc['locationid']] = array( - 'pad' => $loc['locationpad'], - 'name' => $loc['locationname'], - 'disabled' => $allowed !== false && !in_array($loc['locationid'], $allowed), - ); - } - } - - Permission::addGlobalTags($data['perms'], null, ['view.summary', 'view.list']); - $data['locations'] = json_encode($locsFlat); - Render::addTemplate('filterbox', $data); - - - } - private 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), - ); - } + Message::addError('main.value-invalid', 'show', $this->show); } private function redirectFirst($where, $join, $args) { + // TODO Annoying at times, restore this? $res = Database::queryFirst("SELECT machineuuid FROM machine $join WHERE ($where) LIMIT 1", $args); if ($res !== false) { Util::redirect('?do=statistics&uuid=' . $res['machineuuid']); } } - /** - * @param \FilterSet $filterSet - */ - private function showSummary($filterSet) - { - $filterSet->makeFragments($where, $join, $sort, $args); - $known = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE $where", $args); - // If we only have one machine, redirect to machine details - if ($known['val'] == 1) { - $this->redirectFirst($where, $join, $args); - } - $on = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args); - $used = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE state = 'OCCUPIED' AND ($where)", $args); - $hdd = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE badsectors >= 10 AND ($where)", $args); - if ($on['val'] != 0) { - $usedpercent = round($used['val'] / $on['val'] * 100); - } else { - $usedpercent = 0; - } - $data = array( - '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']); - } else { - $x[1] = max($x[1], array_pop($points1['data'])); - $x[2] = max($x[2], array_pop($points2['data'])); - } - $points1['data'][] = $x[1]; - $points2['data'][] = $x[2]; - ++$sum; - if ($sum === 12) { - $sum = 0; - } - } - $data['json'] = json_encode(array('labels' => $labels, 'datasets' => array($points1, $points2))); - $data['query'] = $this->query; - if (Module::get('runmode') !== false) { - $res = Database::queryFirst('SELECT Count(*) AS cnt FROM runmode'); - $data['runmode'] = $res['cnt']; - } - // Draw - Render::addTemplate('summary', $data); - } - - /** - * @param \FilterSet $filterSet - */ - private function showSystemModels($filterSet) - { - global $STATS_COLORS; - - $filterSet->makeFragments($where, $join, $sort, $args); - $res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine' - . " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args); - $lines = array(); - $json = array(); - $id = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - if (empty($row['systemmodel'])) { - continue; - } - settype($row['count'], 'integer'); - $row['id'] = 'systemid' . $id; - $row['urlsystemmodel'] = urlencode($row['systemmodel']); - $lines[] = $row; - $json[] = array( - 'color' => $STATS_COLORS[$id % count($STATS_COLORS)], - 'label' => 'systemid' . $id, - 'value' => $row['count'], - ); - ++$id; - } - $this->capChart($json, $lines, 0.92); - Render::addTemplate('cpumodels', array('rows' => $lines, 'query' => $this->query, 'json' => json_encode($json))); - } - - /** - * @param \FilterSet $filterSet - */ - private function showMemory($filterSet) - { - global $STATS_COLORS, $SIZE_RAM; - - $filterSet->makeFragments($where, $join, $sort, $args); - $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine $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($SIZE_RAM); ++$i) { - if ($SIZE_RAM[$i] < $gb) { - continue; - } - if ($SIZE_RAM[$i] - $gb >= $gb - $SIZE_RAM[$i - 1]) { - --$i; - } - $gb = $SIZE_RAM[$i]; - break; - } - if (isset($lines[$gb])) { - $lines[$gb] += $row['count']; - } else { - $lines[$gb] = $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' => $this->ramColorClass($k * 1024)); - $json[] = array( - 'color' => $STATS_COLORS[$id % count($STATS_COLORS)], - 'label' => (string)$k, - 'value' => $v, - ); - ++$id; - } - $this->capChart($json, $data['rows'], 0.92); - $data['json'] = json_encode($json); - $data['query'] = $this->query; - Render::addTemplate('memory', $data); - } - - /** - * @param \FilterSet $filterSet - */ - private function showKvmState($filterSet) - { - $filterSet->makeFragments($where, $join, $sort, $args); - $colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6'); - $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args); - $lines = array(); - $json = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $lines[] = $row; - $json[] = array( - 'color' => isset($colors[$row['kvmstate']]) ? $colors[$row['kvmstate']] : '#000', - 'label' => $row['kvmstate'], - 'value' => $row['count'], - ); - } - Render::addTemplate('kvmstate', array('rows' => $lines, 'query' => $this->query,'json' => json_encode($json))); - } - - /** - * @param \FilterSet $filterSet - */ - private function showId44($filterSet) - { - global $STATS_COLORS, $SIZE_ID44; - - $filterSet->makeFragments($where, $join, $sort, $args); - $res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY id44mb", $args); - $lines = array(); - $total = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $total += $row['count']; - $gb = (int)ceil($row['id44mb'] / 1024); - for ($i = 1; $i < count($SIZE_ID44); ++$i) { - if ($SIZE_ID44[$i] < $gb) { - continue; - } - if ($SIZE_ID44[$i] - $gb >= $gb - $SIZE_ID44[$i - 1]) { - --$i; - } - $gb = $SIZE_ID44[$i]; - break; - } - if (isset($lines[$gb])) { - $lines[$gb] += $row['count']; - } else { - $lines[$gb] = $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' => $this->hddColorClass($k)); - if ($k === 0) { - $color = '#e55'; - } else { - $color = $STATS_COLORS[$id++ % count($STATS_COLORS)]; - } - $json[] = array( - 'color' => $color, - 'label' => (string)$k, - 'value' => $v, - ); - } - $this->capChart($json, $data['rows'], 0.95); - $data['json'] = json_encode($json); - $data['query'] = $this->query; - Render::addTemplate('id44', $data); - } - - /** - * @param \FilterSet $filterSet - */ - private function showLatestMachines($filterSet) - { - $filterSet->makeFragments($where, $join, $sort, $args); - $args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10; - - $res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine $join" - . " WHERE firstseen > :cutoff AND $where ORDER BY firstseen DESC LIMIT 32", $args); - $rows = array(); - $count = 0; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - 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'] = $this->ramColorClass($row['mbram']); - $row['kvmclass'] = $this->kvmColorClass($row['kvmstate']); - $row['hddclass'] = $this->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)); - } - - /** - * @param \FilterSet $filterSet - */ - private function showMachineList($filterSet) - { - Module::isAvailable('js_stupidtable'); - $filterSet->makeFragments($where, $join, $sort, $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) '; - } - } - $res = Database::simpleQuery('SELECT machineuuid, locationid, macaddr, clientip, lastseen,' - . ' logintime, state, realcores, mbram, kvmstate, cpumodel, id44mb, hostname, notes IS NOT NULL AS hasnotes,' - . ' badsectors ' . $xtra . ' FROM machine' - . " $join WHERE $where $sort", $args); - $rows = array(); - $singleMachine = 'none'; - // 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) - // or add a notice to the confirmation dialog of the according action (nicer but a little more work) - $deleteAllowedLocations = User::getAllowedLocations("machine.delete"); - $rebootAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot'); - $shutdownAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot'); - // Only make client clickable if user is allowed to view details page - $detailsAllowedLocations = User::getAllowedLocations("machine.view-details"); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - if ($singleMachine === 'none') { - $singleMachine = $row['machineuuid']; - } else { - $singleMachine = false; - } - $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); - $octets = explode('.', $row['clientip']); - if (count($octets) === 4) { - $row['subnet'] = "$octets[0].$octets[1].$octets[2]."; - $row['lastoctet'] = $octets[3]; - } - $row['ramclass'] = $this->ramColorClass($row['mbram']); - $row['kvmclass'] = $this->kvmColorClass($row['kvmstate']); - $row['hddclass'] = $this->hddColorClass($row['gbtmp']); - if (empty($row['hostname'])) { - $row['hostname'] = $row['clientip']; - } - if (isset($row['data'])) { - if (!preg_match('/^(Disk.* bytes|Disk.*\d{5,} sectors)/m', $row['data'])) { - $row['nohdd'] = true; - } - } - $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); - if ($data !== false) { - $row['moduleName'] = $data['moduleName']; - $row['modeName'] = $data['modeName']; - } - if (!$row['isclient'] && $row['state'] === 'IDLE') { - $row['state'] = 'OCCUPIED'; - } - } - $row['state_' . $row['state']] = true; - $row['locationname'] = Location::getName($row['locationid']); - $rows[] = $row; - } - if ($singleMachine !== false && $singleMachine !== 'none') { - Util::redirect('?do=statistics&uuid=' . $singleMachine); - } - $data = array( - 'rowCount' => count($rows), - 'rows' => $rows, - 'query' => $this->query, - 'delimiter' => Filter::DELIMITER, - 'sortDirection' => $filterSet->getSortDirection(), - 'sortColumn' => $filterSet->getSortColumn(), - 'columns' => json_encode(Page_Statistics::$columns), - 'showList' => 1, - 'show' => 'list', - 'redirect' => $_SERVER['QUERY_STRING'], - 'rebootcontrol' => (Module::get('rebootcontrol') !== false), - 'canReboot' => !empty($rebootAllowedLocations), - 'canShutdown' => !empty($shutdownAllowedLocations), - 'canDelete' => !empty($deleteAllowedLocations), - ); - Render::addTemplate('clientlist', $data); - } - - private function ramColorClass($mb) - { - if ($mb < 1500) { - return 'danger'; - } - if ($mb < 2500) { - return 'warning'; - } - - return ''; - } - - private function kvmColorClass($state) - { - if ($state === 'DISABLED') { - return 'danger'; - } - if ($state === 'UNKNOWN' || $state === 'UNSUPPORTED') { - return 'warning'; - } - - return ''; - } - - private function hddColorClass($gb) - { - if ($gb < 7) { - return 'danger'; - } - if ($gb < 25) { - return 'warning'; - } - - return ''; - } - - public static function findBestValue($array, $value, $up) - { - $best = 0; - for ($i = 0; $i < count($array); ++$i) { - if (abs($array[$i] - $value) < abs($array[$best] - $value)) { - $best = $i; - } - } - if (!$up && $best === 0) { - return $array[0]; - } - if ($up && $best + 1 === count($array)) { - return $array[$best]; - } - if ($up) { - return ($array[$best] + $array[$best + 1]) / 2; - } - - return ($array[$best] + $array[$best - 1]) / 2; - } - - private function fillSessionInfo(&$row) - { - if (!empty($row['currentuser'])) { - $row['username'] = $row['currentuser']; - if (strlen($row['currentsession']) === 36 && Module::isAvailable('dozmod')) { - $lecture = Database::queryFirst("SELECT lectureid, displayname FROM sat.lecture WHERE lectureid = :lectureid", - array('lectureid' => $row['currentsession'])); - if ($lecture !== false) { - $row['currentsession'] = $lecture['displayname']; - $row['lectureid'] = $lecture['lectureid']; - } - $row['session'] = $row['currentsession']; - return; - } - } - $res = Database::simpleQuery('SELECT dateline, username, data FROM statistic' - . " WHERE clientip = :ip AND typeid = '.vmchooser-session-name'" - . ' AND dateline BETWEEN :start AND :end', array( - 'ip' => $row['clientip'], - 'start' => $row['logintime'] - 60, - 'end' => $row['logintime'] + 300, - )); - $session = false; - while ($r = $res->fetch(PDO::FETCH_ASSOC)) { - if ($session === false || abs($session['dateline'] - $row['logintime']) > abs($r['dateline'] - $row['logintime'])) { - $session = $r; - } - } - if ($session !== false) { - $row['session'] = $session['data']; - if (empty($row['currentuser'])) { - $row['username'] = $session['username']; - } - } - } - - private function showMachine($uuid) - { - $client = Database::queryFirst('SELECT machineuuid, locationid, macaddr, clientip, firstseen, lastseen, logintime, lastboot, state, - mbram, live_tmpsize, live_tmpfree, live_swapsize, live_swapfree, live_memsize, live_memfree, Length(position) AS hasroomplan, - kvmstate, cpumodel, id44mb, 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; - } - 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'); - die($client['data']); - } - // Runmode - if (Module::isAvailable('runmode')) { - $data = RunMode::getRunMode($uuid, RunMode::DATA_STRINGS); - if ($data !== false) { - $client += $data; - } - } - // Rebootcontrol - if (Module::get('rebootcontrol') !== false) { - $client['canReboot'] = (User::hasPermission('.rebootcontrol.action.reboot', (int)$client['locationid'])); - $client['canShutdown'] = (User::hasPermission('.rebootcontrol.action.shutdown', (int)$client['locationid'])); - $client['rebootcontrol'] = $client['canReboot'] || $client['canShutdown']; - } - // Baseconfig - if (Module::get('baseconfig') !== false - && User::hasPermission('.baseconfig.view', (int)$client['locationid'])) { - $cvs = Database::queryFirst('SELECT Count(*) AS cnt FROM setting_machine WHERE machineuuid = :uuid', ['uuid' => $uuid]); - $client['overriddenVars'] = is_array($cvs) ? $cvs['cnt'] : 0; - $client['hasBaseconfig'] = true; - } - if (!isset($client['isclient'])) { - $client['isclient'] = true; - } - // Mangle fields - $NOW = time(); - if (!$client['isclient']) { - if ($client['state'] === 'IDLE') { - $client['state'] = 'OCCUPIED'; - } - } else { - if ($client['state'] === 'OCCUPIED') { - $this->fillSessionInfo($client); - } - } - $client['state_' . $client['state']] = true; - $client['firstseen_s'] = date('d.m.Y H:i', $client['firstseen']); - $client['lastseen_s'] = date('d.m.Y H:i', $client['lastseen']); - $client['logintime_s'] = date('d.m.Y H:i', $client['logintime']); - if ($client['lastboot'] == 0) { - $client['lastboot_s'] = '-'; - } else { - $uptime = $NOW - $client['lastboot']; - $client['lastboot_s'] = date('d.m.Y H:i', $client['lastboot']); - if ($client['state'] === 'IDLE' || $client['state'] === 'OCCUPIED') { - $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); - foreach (['tmp', 'swap', 'mem'] as $item) { - if ($client['live_' . $item . 'size'] == 0) - continue; - $client['live_' . $item . 'percent'] = round(($client['live_' . $item . 'free'] / $client['live_' . $item . 'size']) * 100, 2); - $client['live_' . $item . 'free_s'] = Util::readableFileSize($client['live_' . $item . 'free'], -1, 2); - } - $client['ramclass'] = $this->ramColorClass($client['mbram']); - $client['kvmclass'] = $this->kvmColorClass($client['kvmstate']); - $client['hddclass'] = $this->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]); - } - } - } - unset($client['data']); - // BIOS update check - if (!empty($client['biosrevision'])) { - $mainboard = $client['mobomanufacturer'] . '##' . $client['mobomodel']; - $system = $client['pcmanufacturer'] . '##' . $client['pcmodel']; - $ret = $this->checkBios($mainboard, $system, $client['biosdate'], $client['biosrevision']); - if ($ret === false) { // Not loaded, use AJAX - $params = [ - 'mainboard' => $mainboard, - 'system' => $system, - 'date' => $client['biosdate'], - 'revision' => $client['biosrevision'], - ]; - $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); - } - } - // Get locations - if (Module::isAvailable('locations')) { - $locs = Location::getLocationsAssoc(); - $next = (int)$client['locationid']; - $output = array(); - while (isset($locs[$next])) { - array_unshift($output, $locs[$next]); - $next = $locs[$next]['parentlocationid']; - } - $client['locations'] = $output; - } - // Screens TODO Move everything else to hw table instead of blob parsing above - // `devicetype`, `devicename`, `subid`, `machineuuid` - $res = Database::simpleQuery("SELECT m.hwid, h.hwname, m.devpath AS connector, m.disconnecttime," - . " p.value AS resolution, q.prop AS projector FROM machine_x_hw m" - . " INNER JOIN statistic_hw h ON (m.hwid = h.hwid AND h.hwtype = :screen)" - . " 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)); - $client['screens'] = array(); - $ports = array(); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - if ($row['disconnecttime'] != 0) - continue; - $ports[] = $row['connector']; - $client['screens'][] = $row; - } - array_multisort($ports, SORT_ASC, $client['screens']); - Permission::addGlobalTags($client['perms'], null, ['hardware.projectors.edit', 'hardware.projectors.view']); - // Throw output at user - Render::addTemplate('machine-main', $client); - // Sessions - $NOW = time(); - $cutoff = $NOW - 86400 * 7; - //if ($cutoff < $client['firstseen']) $cutoff = $client['firstseen']; - $scale = 100 / ($NOW - $cutoff); - $res = Database::simpleQuery('SELECT dateline, typeid, data FROM statistic' - . " WHERE dateline > :cutoff AND typeid IN (:sessionLength, :offlineLength) AND machineuuid = :uuid ORDER BY dateline ASC", array( - 'cutoff' => $cutoff - 86400 * 14, - 'uuid' => $uuid, - 'sessionLength' => Statistics::SESSION_LENGTH, - 'offlineLength' => Statistics::OFFLINE_LENGTH, - )); - $spans['rows'] = array(); - $spans['graph'] = ''; - $last = false; - $first = true; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - 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) { - // Special case: offline before - $spans['graph'] .= '
 
'; - } - $first = false; - if ($row['dateline'] + $row['data'] < $cutoff || $row['data'] > 864000) { - continue; - } - if ($last !== false && abs($last['dateline'] - $row['dateline']) < 30 - && abs($last['data'] - $row['data']) < 30 - ) { - continue; - } - if ($last !== false && $last['dateline'] + $last['data'] > $row['dateline']) { - $point = $last['dateline'] + $last['data']; - $row['data'] -= ($point - $row['dateline']); - $row['dateline'] = $point; - } - if ($row['dateline'] < $cutoff) { - $row['data'] -= ($cutoff - $row['dateline']); - $row['dateline'] = $cutoff; - } - $row['from'] = Util::prettyTime($row['dateline']); - $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']); - if ($row['typeid'] === Statistics::OFFLINE_LENGTH) { - $row['glyph'] = 'off'; - $color = '#444'; - } elseif ($row['typeid'] === Statistics::SUSPEND_LENGTH) { - $row['glyph'] = 'pause'; - $color = '#686'; - } else { - $row['glyph'] = 'user'; - $color = '#e77'; - } - $spans['graph'] .= '
 
'; - if ($client['isclient']) { - $spans['rows'][] = $row; - } - $last = $row; - } - if ($first && $client['lastboot'] > $cutoff) { - // Special case: offline before - $spans['graph'] .= '
 
'; - } elseif ($first) { - // Not seen in last two weeks - $spans['graph'] .= '
 
'; - } - if ($client['state'] === 'OCCUPIED') { - $spans['graph'] .= '
 
'; - $spans['rows'][] = [ - 'from' => Util::prettyTime($client['logintime']), - 'duration' => '-', - 'glyph' => 'user', - ]; - $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']); - } elseif ($client['state'] === 'OFFLINE') { - $spans['graph'] .= '
 
'; - $spans['rows'][] = [ - 'from' => Util::prettyTime($client['lastseen']), - 'duration' => '-', - 'glyph' => 'off', - ]; - } elseif ($client['state'] === 'STANDBY') { - $spans['graph'] .= '
 
'; - $spans['rows'][] = [ - 'from' => Util::prettyTime($client['lastseen']), - 'duration' => '-', - 'glyph' => 'pause', - ]; - } - $t = explode('-', date('Y-n-j-G', $cutoff)); - if ($t[3] >= 8 && $t[3] <= 22) { - $start = mktime(22, 0, 0, $t[1], $t[2], $t[0]); - } else { - $start = mktime(22, 0, 0, $t[1], $t[2] - 1, $t[0]); - } - for ($i = $start; $i < $NOW; $i += 86400) { - $spans['graph'] .= '
 
'; - } - 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['isclient'] = $client['isclient']; - Render::addTemplate('machine-usage', $spans); - // Any hdds? - if (!empty($hdds['hdds'])) { - Render::addTemplate('machine-hdds', $hdds); - } - // Client log - if (Module::get('syslog') !== false) { - $lres = Database::simpleQuery('SELECT logid, dateline, logtypeid, clientip, description, extra FROM clientlog' - . ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid'])); - $count = 0; - $log = array(); - while ($row = $lres->fetch(PDO::FETCH_ASSOC)) { - if (substr($row['description'], -5) === 'on :0' && strpos($row['description'], 'root logged') === false) { - continue; - } - $row['date'] = Util::prettyTime($row['dateline']); - $row['icon'] = $this->eventToIconName($row['logtypeid']); - $log[] = $row; - if (++$count === 10) { - break; - } - } - Render::addTemplate('syslog', array( - 'machineuuid' => $client['machineuuid'], - 'list' => $log, - )); - } - // Notes - if (User::hasPermission('machine.note.*', (int)$client['locationid'])) { - Permission::addGlobalTags($client['perms'], (int)$client['locationid'], ['machine.note.edit']); - Render::addTemplate('machine-notes', $client); - } - } - - private function eventToIconName($event) - { - switch ($event) { - case 'session-open': - return 'glyphicon-log-in'; - case 'session-close': - return 'glyphicon-log-out'; - case 'partition-swap': - return 'glyphicon-info-sign'; - case 'partition-temp': - case 'smartctl-realloc': - return 'glyphicon-exclamation-sign'; - default: - return 'glyphicon-minus'; - } - } - - protected function doAjax() { if (!User::load()) return; if (Request::any('action') === 'bios') { - $this->ajaxCheckBios(); + require_once 'modules/statistics/pages/machine.inc.php'; + SubPage::ajaxCheckBios(); return; } @@ -1251,81 +270,4 @@ class Page_Statistics extends Page ), true); } - const BIOS_CACHE = '/tmp/bwlp-bios.json'; - - private function ajaxCheckBios() - { - $mainboard = Request::any('mainboard', false, 'string'); - $system = Request::any('system', false, 'string'); - $date = Request::any('date', false, 'string'); - $revision = Request::any('revision', false, 'string'); - $reply = $this->checkBios($mainboard, $system, $date, $revision); - if ($reply === false) { - $data = Download::asString(CONFIG_BIOS_URL, 3, $err); - if ($err < 200 || $err >= 300) { - $reply = ['error' => 'HTTP: ' . $err]; - } else { - file_put_contents(self::BIOS_CACHE, $data); - $data = json_decode($data, true); - $reply = $this->checkBios($mainboard, $system, $date, $revision, $data); - } - } - if ($reply === false) { - $reply = ['error' => 'Internal Error']; - } - if (isset($reply['status']) && $reply['status'] === 0) - exit; // Show nothing, 0 means OK - die(Render::parse('machine-bios-update', $reply)); - } - - private function checkBios($mainboard, $system, $date, $revision, $json = null) - { - if ($json === null) { - if (!file_exists(self::BIOS_CACHE) || filemtime(self::BIOS_CACHE) + 3600 < time()) - return false; - $json = json_decode(file_get_contents(self::BIOS_CACHE), true); - } - 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'])) { - $match =& $json['system'][$system]; - } elseif (isset($json['mainboard'][$mainboard]) && isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) { - $match =& $json['mainboard'][$mainboard]; - } else { - return ['status' => 0]; - } - $key = $match['match']; - if ($key === 'revision') { - $cmp = function ($item) { $s = explode('.', $item); return $s[0] * 0x10000 + $s[1]; }; - $reference = $cmp($revision); - } elseif ($key === 'date') { - $cmp = function ($item) { $s = explode('.', $item); return $s[2] * 10000 + $s[1] * 100 + $s[0]; }; - $reference = $cmp($date); - } else { - return ['error' => 'Invalid comparison key: ' . $key]; - } - $retval = ['fixes' => []]; - $level = 0; - foreach ($match['fixes'] as $fix) { - if ($cmp($fix[$key]) > $reference) { - class_exists('Dictionary'); // Trigger setup of lang stuff - $lang = isset($fix['text'][LANG]) ? LANG : 'en'; - $fix['text'] = $fix['text'][$lang]; - $retval['fixes'][] = $fix; - $level = max($level, $fix['level']); - } - } - $retval['url'] = $match['url']; - $retval['status'] = $level; - if ($level > 5) { - $retval['class'] = 'danger'; - } elseif ($level > 3) { - $retval['class'] = 'warning'; - } else { - $retval['class'] = 'info'; - } - return $retval; - } } - -Page_Statistics::initConstants(); diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php new file mode 100644 index 00000000..d4f97d6e --- /dev/null +++ b/modules-available/statistics/pages/list.inc.php @@ -0,0 +1,129 @@ +setSort($sortColumn, $sortDirection); + + if (!$filterSet->setAllowedLocationsFromPermission('view.list')) { + Message::addError('main.no-permission'); + Util::redirect('?do=main'); + } + Render::openTag('div', array('class' => 'row')); + StatisticsFilter::renderFilterBox('list', $filterSet, StatisticsFilter::getQuery()); + Render::closeTag('div'); + self::showMachineList($filterSet); + } + + + /** + * @param \StatisticsFilterSet $filterSet + */ + private static function showMachineList($filterSet) + { + Module::isAvailable('js_stupidtable'); + $filterSet->makeFragments($where, $join, $sort, $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) '; + } + } + $res = Database::simpleQuery('SELECT machineuuid, locationid, macaddr, clientip, lastseen,' + . ' logintime, state, realcores, mbram, kvmstate, cpumodel, id44mb, hostname, notes IS NOT NULL AS hasnotes,' + . ' badsectors ' . $xtra . ' FROM machine' + . " $join WHERE $where $sort", $args); + $rows = array(); + $singleMachine = 'none'; + // 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) + // or add a notice to the confirmation dialog of the according action (nicer but a little more work) + $deleteAllowedLocations = User::getAllowedLocations("machine.delete"); + $rebootAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot'); + $shutdownAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot'); + // Only make client clickable if user is allowed to view details page + $detailsAllowedLocations = User::getAllowedLocations("machine.view-details"); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if ($singleMachine === 'none') { + $singleMachine = $row['machineuuid']; + } else { + $singleMachine = false; + } + $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); + $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['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']); + $row['hddclass'] = StatisticsStyling::hddColorClass($row['gbtmp']); + if (empty($row['hostname'])) { + $row['hostname'] = $row['clientip']; + } + if (isset($row['data'])) { + if (!preg_match('/^(Disk.* bytes|Disk.*\d{5,} sectors)/m', $row['data'])) { + $row['nohdd'] = true; + } + } + $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); + if ($data !== false) { + $row['moduleName'] = $data['moduleName']; + $row['modeName'] = $data['modeName']; + } + if (!$row['isclient'] && $row['state'] === 'IDLE') { + $row['state'] = 'OCCUPIED'; + } + } + $row['state_' . $row['state']] = true; + $row['locationname'] = Location::getName($row['locationid']); + $rows[] = $row; + } + if ($singleMachine !== false && $singleMachine !== 'none') { + Util::redirect('?do=statistics&uuid=' . $singleMachine); + } + $data = array( + 'rowCount' => count($rows), + 'rows' => $rows, + 'query' => StatisticsFilter::getQuery(), + 'delimiter' => StatisticsFilter::DELIMITER, + 'sortDirection' => $filterSet->getSortDirection(), + 'sortColumn' => $filterSet->getSortColumn(), + 'columns' => json_encode(StatisticsFilter::$columns), + 'showList' => 1, + 'show' => 'list', + 'redirect' => $_SERVER['QUERY_STRING'], + 'rebootcontrol' => (Module::get('rebootcontrol') !== false), + 'canReboot' => !empty($rebootAllowedLocations), + 'canShutdown' => !empty($shutdownAllowedLocations), + 'canDelete' => !empty($deleteAllowedLocations), + ); + Render::addTemplate('clientlist', $data); + } + +} \ No newline at end of file diff --git a/modules-available/statistics/pages/machine.inc.php b/modules-available/statistics/pages/machine.inc.php new file mode 100644 index 00000000..9c42160d --- /dev/null +++ b/modules-available/statistics/pages/machine.inc.php @@ -0,0 +1,436 @@ + $row['currentsession'])); + if ($lecture !== false) { + $row['currentsession'] = $lecture['displayname']; + $row['lectureid'] = $lecture['lectureid']; + } + $row['session'] = $row['currentsession']; + return; + } + } + $res = Database::simpleQuery('SELECT dateline, username, data FROM statistic' + . " WHERE clientip = :ip AND typeid = '.vmchooser-session-name'" + . ' AND dateline BETWEEN :start AND :end', array( + 'ip' => $row['clientip'], + 'start' => $row['logintime'] - 60, + 'end' => $row['logintime'] + 300, + )); + $session = false; + while ($r = $res->fetch(PDO::FETCH_ASSOC)) { + if ($session === false || abs($session['dateline'] - $row['logintime']) > abs($r['dateline'] - $row['logintime'])) { + $session = $r; + } + } + if ($session !== false) { + $row['session'] = $session['data']; + if (empty($row['currentuser'])) { + $row['username'] = $session['username']; + } + } + } + + private static function showMachine($uuid) + { + $client = Database::queryFirst('SELECT machineuuid, locationid, macaddr, clientip, firstseen, lastseen, logintime, lastboot, state, + mbram, live_tmpsize, live_tmpfree, live_swapsize, live_swapfree, live_memsize, live_memfree, Length(position) AS hasroomplan, + kvmstate, cpumodel, id44mb, 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; + } + 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'); + die($client['data']); + } + // Runmode + if (Module::isAvailable('runmode')) { + $data = RunMode::getRunMode($uuid, RunMode::DATA_STRINGS); + if ($data !== false) { + $client += $data; + } + } + // Rebootcontrol + if (Module::get('rebootcontrol') !== false) { + $client['canReboot'] = (User::hasPermission('.rebootcontrol.action.reboot', (int)$client['locationid'])); + $client['canShutdown'] = (User::hasPermission('.rebootcontrol.action.shutdown', (int)$client['locationid'])); + $client['rebootcontrol'] = $client['canReboot'] || $client['canShutdown']; + } + // Baseconfig + if (Module::get('baseconfig') !== false + && User::hasPermission('.baseconfig.view', (int)$client['locationid'])) { + $cvs = Database::queryFirst('SELECT Count(*) AS cnt FROM setting_machine WHERE machineuuid = :uuid', ['uuid' => $uuid]); + $client['overriddenVars'] = is_array($cvs) ? $cvs['cnt'] : 0; + $client['hasBaseconfig'] = true; + } + if (!isset($client['isclient'])) { + $client['isclient'] = true; + } + // Mangle fields + $NOW = time(); + if (!$client['isclient']) { + if ($client['state'] === 'IDLE') { + $client['state'] = 'OCCUPIED'; + } + } else { + if ($client['state'] === 'OCCUPIED') { + self::fillSessionInfo($client); + } + } + $client['state_' . $client['state']] = true; + $client['firstseen_s'] = date('d.m.Y H:i', $client['firstseen']); + $client['lastseen_s'] = date('d.m.Y H:i', $client['lastseen']); + $client['logintime_s'] = date('d.m.Y H:i', $client['logintime']); + if ($client['lastboot'] == 0) { + $client['lastboot_s'] = '-'; + } else { + $uptime = $NOW - $client['lastboot']; + $client['lastboot_s'] = date('d.m.Y H:i', $client['lastboot']); + if ($client['state'] === 'IDLE' || $client['state'] === 'OCCUPIED') { + $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); + foreach (['tmp', 'swap', 'mem'] as $item) { + if ($client['live_' . $item . 'size'] == 0) + continue; + $client['live_' . $item . 'percent'] = round(($client['live_' . $item . 'free'] / $client['live_' . $item . 'size']) * 100, 2); + $client['live_' . $item . 'free_s'] = Util::readableFileSize($client['live_' . $item . 'free'], -1, 2); + } + $client['ramclass'] = StatisticsStyling::ramColorClass($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]); + } + } + } + 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 ($ret === false) { // Not loaded, use AJAX + $params = [ + 'mainboard' => $mainboard, + 'system' => $system, + 'date' => $client['biosdate'], + 'revision' => $client['biosrevision'], + ]; + $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); + } + } + // Get locations + if (Module::isAvailable('locations')) { + $locs = Location::getLocationsAssoc(); + $next = (int)$client['locationid']; + $output = array(); + while (isset($locs[$next])) { + array_unshift($output, $locs[$next]); + $next = $locs[$next]['parentlocationid']; + } + $client['locations'] = $output; + } + // Screens TODO Move everything else to hw table instead of blob parsing above + // `devicetype`, `devicename`, `subid`, `machineuuid` + $res = Database::simpleQuery("SELECT m.hwid, h.hwname, m.devpath AS connector, m.disconnecttime," + . " p.value AS resolution, q.prop AS projector FROM machine_x_hw m" + . " INNER JOIN statistic_hw h ON (m.hwid = h.hwid AND h.hwtype = :screen)" + . " 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)); + $client['screens'] = array(); + $ports = array(); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if ($row['disconnecttime'] != 0) + continue; + $ports[] = $row['connector']; + $client['screens'][] = $row; + } + array_multisort($ports, SORT_ASC, $client['screens']); + Permission::addGlobalTags($client['perms'], null, ['hardware.projectors.edit', 'hardware.projectors.view']); + // Throw output at user + Render::addTemplate('machine-main', $client); + // Sessions + $NOW = time(); + $cutoff = $NOW - 86400 * 7; + //if ($cutoff < $client['firstseen']) $cutoff = $client['firstseen']; + $scale = 100 / ($NOW - $cutoff); + $res = Database::simpleQuery('SELECT dateline, typeid, data FROM statistic' + . " WHERE dateline > :cutoff AND typeid IN (:sessionLength, :offlineLength) AND machineuuid = :uuid ORDER BY dateline ASC", array( + 'cutoff' => $cutoff - 86400 * 14, + 'uuid' => $uuid, + 'sessionLength' => Statistics::SESSION_LENGTH, + 'offlineLength' => Statistics::OFFLINE_LENGTH, + )); + $spans['rows'] = array(); + $spans['graph'] = ''; + $last = false; + $first = true; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + 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) { + // Special case: offline before + $spans['graph'] .= '
 
'; + } + $first = false; + if ($row['dateline'] + $row['data'] < $cutoff || $row['data'] > 864000) { + continue; + } + if ($last !== false && abs($last['dateline'] - $row['dateline']) < 30 + && abs($last['data'] - $row['data']) < 30 + ) { + continue; + } + if ($last !== false && $last['dateline'] + $last['data'] > $row['dateline']) { + $point = $last['dateline'] + $last['data']; + $row['data'] -= ($point - $row['dateline']); + $row['dateline'] = $point; + } + if ($row['dateline'] < $cutoff) { + $row['data'] -= ($cutoff - $row['dateline']); + $row['dateline'] = $cutoff; + } + $row['from'] = Util::prettyTime($row['dateline']); + $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']); + if ($row['typeid'] === Statistics::OFFLINE_LENGTH) { + $row['glyph'] = 'off'; + $color = '#444'; + } elseif ($row['typeid'] === Statistics::SUSPEND_LENGTH) { + $row['glyph'] = 'pause'; + $color = '#686'; + } else { + $row['glyph'] = 'user'; + $color = '#e77'; + } + $spans['graph'] .= '
 
'; + if ($client['isclient']) { + $spans['rows'][] = $row; + } + $last = $row; + } + if ($first && $client['lastboot'] > $cutoff) { + // Special case: offline before + $spans['graph'] .= '
 
'; + } elseif ($first) { + // Not seen in last two weeks + $spans['graph'] .= '
 
'; + } + if ($client['state'] === 'OCCUPIED') { + $spans['graph'] .= '
 
'; + $spans['rows'][] = [ + 'from' => Util::prettyTime($client['logintime']), + 'duration' => '-', + 'glyph' => 'user', + ]; + $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']); + } elseif ($client['state'] === 'OFFLINE') { + $spans['graph'] .= '
 
'; + $spans['rows'][] = [ + 'from' => Util::prettyTime($client['lastseen']), + 'duration' => '-', + 'glyph' => 'off', + ]; + } elseif ($client['state'] === 'STANDBY') { + $spans['graph'] .= '
 
'; + $spans['rows'][] = [ + 'from' => Util::prettyTime($client['lastseen']), + 'duration' => '-', + 'glyph' => 'pause', + ]; + } + $t = explode('-', date('Y-n-j-G', $cutoff)); + if ($t[3] >= 8 && $t[3] <= 22) { + $start = mktime(22, 0, 0, $t[1], $t[2], $t[0]); + } else { + $start = mktime(22, 0, 0, $t[1], $t[2] - 1, $t[0]); + } + for ($i = $start; $i < $NOW; $i += 86400) { + $spans['graph'] .= '
 
'; + } + 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['isclient'] = $client['isclient']; + Render::addTemplate('machine-usage', $spans); + // Any hdds? + if (!empty($hdds['hdds'])) { + Render::addTemplate('machine-hdds', $hdds); + } + // Client log + if (Module::get('syslog') !== false) { + $lres = Database::simpleQuery('SELECT logid, dateline, logtypeid, clientip, description, extra FROM clientlog' + . ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid'])); + $count = 0; + $log = array(); + while ($row = $lres->fetch(PDO::FETCH_ASSOC)) { + if (substr($row['description'], -5) === 'on :0' && strpos($row['description'], 'root logged') === false) { + continue; + } + $row['date'] = Util::prettyTime($row['dateline']); + $row['icon'] = self::eventToIconName($row['logtypeid']); + $log[] = $row; + if (++$count === 10) { + break; + } + } + Render::addTemplate('syslog', array( + 'machineuuid' => $client['machineuuid'], + 'list' => $log, + )); + } + // Notes + if (User::hasPermission('machine.note.*', (int)$client['locationid'])) { + Permission::addGlobalTags($client['perms'], (int)$client['locationid'], ['machine.note.edit']); + Render::addTemplate('machine-notes', $client); + } + } + + private static function eventToIconName($event) + { + switch ($event) { + case 'session-open': + return 'glyphicon-log-in'; + case 'session-close': + return 'glyphicon-log-out'; + case 'partition-swap': + return 'glyphicon-info-sign'; + case 'partition-temp': + case 'smartctl-realloc': + return 'glyphicon-exclamation-sign'; + default: + return 'glyphicon-minus'; + } + } + + const BIOS_CACHE = '/tmp/bwlp-bios.json'; + + public static function ajaxCheckBios() + { + $mainboard = Request::any('mainboard', false, 'string'); + $system = Request::any('system', false, 'string'); + $date = Request::any('date', false, 'string'); + $revision = Request::any('revision', false, 'string'); + $reply = self::checkBios($mainboard, $system, $date, $revision); + if ($reply === false) { + $data = Download::asString(CONFIG_BIOS_URL, 3, $err); + if ($err < 200 || $err >= 300) { + $reply = ['error' => 'HTTP: ' . $err]; + } else { + file_put_contents(self::BIOS_CACHE, $data); + $data = json_decode($data, true); + $reply = self::checkBios($mainboard, $system, $date, $revision, $data); + } + } + if ($reply === false) { + $reply = ['error' => 'Internal Error']; + } + if (isset($reply['status']) && $reply['status'] === 0) + exit; // Show nothing, 0 means OK + die(Render::parse('machine-bios-update', $reply)); + } + + private static function checkBios($mainboard, $system, $date, $revision, $json = null) + { + if ($json === null) { + if (!file_exists(self::BIOS_CACHE) || filemtime(self::BIOS_CACHE) + 3600 < time()) + return false; + $json = json_decode(file_get_contents(self::BIOS_CACHE), true); + } + 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'])) { + $match =& $json['system'][$system]; + } elseif (isset($json['mainboard'][$mainboard]) && isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) { + $match =& $json['mainboard'][$mainboard]; + } else { + return ['status' => 0]; + } + $key = $match['match']; + if ($key === 'revision') { + $cmp = function ($item) { $s = explode('.', $item); return $s[0] * 0x10000 + $s[1]; }; + $reference = $cmp($revision); + } elseif ($key === 'date') { + $cmp = function ($item) { $s = explode('.', $item); return $s[2] * 10000 + $s[1] * 100 + $s[0]; }; + $reference = $cmp($date); + } else { + return ['error' => 'Invalid comparison key: ' . $key]; + } + $retval = ['fixes' => []]; + $level = 0; + foreach ($match['fixes'] as $fix) { + if ($cmp($fix[$key]) > $reference) { + class_exists('Dictionary'); // Trigger setup of lang stuff + $lang = isset($fix['text'][LANG]) ? LANG : 'en'; + $fix['text'] = $fix['text'][$lang]; + $retval['fixes'][] = $fix; + $level = max($level, $fix['level']); + } + } + $retval['url'] = $match['url']; + $retval['status'] = $level; + if ($level > 5) { + $retval['class'] = 'danger'; + } elseif ($level > 3) { + $retval['class'] = 'warning'; + } else { + $retval['class'] = 'info'; + } + return $retval; + } + +} \ No newline at end of file diff --git a/modules-available/statistics/pages/summary.inc.php b/modules-available/statistics/pages/summary.inc.php new file mode 100644 index 00000000..be41b231 --- /dev/null +++ b/modules-available/statistics/pages/summary.inc.php @@ -0,0 +1,316 @@ +setSort($sortColumn, $sortDirection); + + 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(); + Render::openTag('div', array('class' => 'row')); + StatisticsFilter::renderFilterBox('summary', $filterSet, StatisticsFilter::getQuery()); + 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($filterSet) + { + $filterSet->makeFragments($where, $join, $sort, $args); + $known = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE $where", $args); + $on = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE state IN ('IDLE', 'OCCUPIED') AND ($where)", $args); + $used = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE state = 'OCCUPIED' AND ($where)", $args); + $hdd = Database::queryFirst("SELECT Count(*) AS val FROM machine $join WHERE badsectors >= 10 AND ($where)", $args); + if ($on['val'] != 0) { + $usedpercent = round($used['val'] / $on['val'] * 100); + } else { + $usedpercent = 0; + } + $data = array( + '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']); + } else { + $x[1] = max($x[1], array_pop($points1['data'])); + $x[2] = max($x[2], array_pop($points2['data'])); + } + $points1['data'][] = $x[1]; + $points2['data'][] = $x[2]; + ++$sum; + if ($sum === 12) { + $sum = 0; + } + } + $data['json'] = json_encode(array('labels' => $labels, 'datasets' => array($points1, $points2))); + $data['query'] = StatisticsFilter::getQuery(); + if (Module::get('runmode') !== false) { + $res = Database::queryFirst('SELECT Count(*) AS cnt FROM runmode'); + $data['runmode'] = $res['cnt']; + } + // Draw + Render::addTemplate('summary', $data); + } + + /** + * @param \StatisticsFilterSet $filterSet + */ + private static function showSystemModels($filterSet) + { + $filterSet->makeFragments($where, $join, $sort, $args); + $res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine' + . " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args); + $lines = array(); + $json = array(); + $id = 0; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if (empty($row['systemmodel'])) { + continue; + } + settype($row['count'], 'integer'); + $row['id'] = 'systemid' . $id; + $row['urlsystemmodel'] = urlencode($row['systemmodel']); + $lines[] = $row; + $json[] = array( + '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, 'query' => StatisticsFilter::getQuery(), 'json' => json_encode($json))); + } + + /** + * @param \StatisticsFilterSet $filterSet + */ + private static function showMemory($filterSet) + { + $filterSet->makeFragments($where, $join, $sort, $args); + $res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine $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']; + } + } + 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::ramColorClass($k * 1024)); + $json[] = array( + 'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)], + 'label' => (string)$k, + 'value' => $v, + ); + ++$id; + } + self::capChart($json, $data['rows'], 0.92); + $data['json'] = json_encode($json); + $data['query'] = StatisticsFilter::getQuery(); + Render::addTemplate('memory', $data); + } + + /** + * @param \StatisticsFilterSet $filterSet + */ + private static function showKvmState($filterSet) + { + $filterSet->makeFragments($where, $join, $sort, $args); + $colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6'); + $res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args); + $lines = array(); + $json = array(); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $lines[] = $row; + $json[] = array( + 'color' => isset($colors[$row['kvmstate']]) ? $colors[$row['kvmstate']] : '#000', + 'label' => $row['kvmstate'], + 'value' => $row['count'], + ); + } + Render::addTemplate('kvmstate', array('rows' => $lines, 'query' => StatisticsFilter::getQuery(),'json' => json_encode($json))); + } + + /** + * @param \StatisticsFilterSet $filterSet + */ + private static function showId44($filterSet) + { + $filterSet->makeFragments($where, $join, $sort, $args); + $res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine $join WHERE $where GROUP BY id44mb", $args); + $lines = array(); + $total = 0; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $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']; + } + } + 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)); + if ($k === 0) { + $color = '#e55'; + } else { + $color = self::$STATS_COLORS[$id++ % count(self::$STATS_COLORS)]; + } + $json[] = array( + 'color' => $color, + 'label' => (string)$k, + 'value' => $v, + ); + } + self::capChart($json, $data['rows'], 0.95); + $data['json'] = json_encode($json); + $data['query'] = StatisticsFilter::getQuery(); + Render::addTemplate('id44', $data); + } + + /** + * @param \StatisticsFilterSet $filterSet + */ + private static function showLatestMachines($filterSet) + { + $filterSet->makeFragments($where, $join, $sort, $args); + $args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10; + + $res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine $join" + . " WHERE firstseen > :cutoff AND $where ORDER BY firstseen DESC LIMIT 32", $args); + $rows = array(); + $count = 0; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + 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), + ); + } + } + +} \ No newline at end of file -- cgit v1.2.3-55-g7522