summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2019-11-07 10:30:37 +0100
committerSimon Rettberg2019-11-07 10:30:37 +0100
commita03f1f478a5f75de79ff2348461c8e9ed9872d3f (patch)
treebc552f1456733f017ae6fda320ea80af6de76af5
parent[translation] Get rid of up_json_encode (diff)
downloadslx-admin-a03f1f478a5f75de79ff2348461c8e9ed9872d3f.tar.gz
slx-admin-a03f1f478a5f75de79ff2348461c8e9ed9872d3f.tar.xz
slx-admin-a03f1f478a5f75de79ff2348461c8e9ed9872d3f.zip
[statistics] Modularize
-rw-r--r--modules-available/statistics/inc/parser.inc.php4
-rw-r--r--modules-available/statistics/inc/statisticsfilter.inc.php (renamed from modules-available/statistics/inc/filter.inc.php)267
-rw-r--r--modules-available/statistics/inc/statisticsfilterset.inc.php (renamed from modules-available/statistics/inc/filterset.inc.php)16
-rw-r--r--modules-available/statistics/inc/statisticsstyling.inc.php42
-rw-r--r--modules-available/statistics/page.inc.php1176
-rw-r--r--modules-available/statistics/pages/list.inc.php129
-rw-r--r--modules-available/statistics/pages/machine.inc.php436
-rw-r--r--modules-available/statistics/pages/summary.inc.php316
8 files changed, 1231 insertions, 1155 deletions
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/filter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php
index 46de467b..1556a1e0 100644
--- a/modules-available/statistics/inc/filter.inc.php
+++ b/modules-available/statistics/inc/statisticsfilter.inc.php
@@ -3,19 +3,43 @@
/* base class with rudimentary SQL generation abilities.
* WARNING: argument is escaped, but $column and $operator are passed unfiltered into SQL */
-class Filter
+class StatisticsFilter
{
/**
* Delimiter for js_selectize filters
*/
const DELIMITER = '~,~';
+ const SIZE_ID44 = array(0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 2000, 4000);
+ const SIZE_RAM = array(1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024);
+
public $column;
public $operator;
public $argument;
private static $keyCounter = 0;
+ 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;
+ }
+
public static function getNewKey($colname)
{
return $colname . '_' . (self::$keyCounter++);
@@ -31,11 +55,11 @@ class Filter
/* returns a where clause and adds needed operators to the passed array */
public function whereClause(&$args, &$joins)
{
- $key = Filter::getNewKey($this->column);
+ $key = StatisticsFilter::getNewKey($this->column);
$addendum = '';
/* check if we have to do some parsing*/
- if (Page_Statistics::$columns[$this->column]['type'] === 'date') {
+ if (self::$columns[$this->column]['type'] === 'date') {
$args[$key] = strtotime($this->argument);
} else {
$args[$key] = $this->argument;
@@ -68,12 +92,12 @@ class Filter
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);
+ $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 Filter('clientip', '=', $q);
+ $filters[] = new StatisticsFilter('clientip', '=', $q);
continue;
}
/* find position of first operator */
@@ -95,20 +119,20 @@ class Filter
$rhs = trim(substr($q, $pos + strlen($operator)));
if ($lhs === 'gbram') {
- $filters[] = new RamGbFilter($operator, $rhs);
+ $filters[] = new RamGbStatisticsFilter($operator, $rhs);
} elseif ($lhs === 'runtime') {
- $filters[] = new RuntimeFilter($operator, $rhs);
+ $filters[] = new RuntimeStatisticsFilter($operator, $rhs);
} elseif ($lhs === 'state') {
- $filters[] = new StateFilter($operator, $rhs);
+ $filters[] = new StateStatisticsFilter($operator, $rhs);
} elseif ($lhs === 'hddgb') {
- $filters[] = new Id44Filter($operator, $rhs);
+ $filters[] = new Id44StatisticsFilter($operator, $rhs);
} elseif ($lhs === 'location') {
- $filters[] = new LocationFilter($operator, $rhs);
+ $filters[] = new LocationStatisticsFilter($operator, $rhs);
} elseif ($lhs === 'subnet') {
- $filters[] = new SubnetFilter($operator, $rhs);
+ $filters[] = new SubnetStatisticsFilter($operator, $rhs);
} else {
- if (array_key_exists($lhs, Page_Statistics::$columns) && Page_Statistics::$columns[$lhs]['column']) {
- $filters[] = new Filter($lhs, $operator, $rhs);
+ if (array_key_exists($lhs, self::$columns) && self::$columns[$lhs]['column']) {
+ $filters[] = new StatisticsFilter($lhs, $operator, $rhs);
} else {
Message::addError('invalid-filter-key', $lhs);
}
@@ -117,9 +141,194 @@ class Filter
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 RamGbFilter extends Filter
+class RamGbStatisticsFilter extends StatisticsFilter
{
public function __construct($operator, $argument)
{
@@ -128,9 +337,8 @@ class RamGbFilter extends Filter
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);
+ $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 == '<') {
@@ -151,7 +359,7 @@ class RamGbFilter extends Filter
}
}
-class RuntimeFilter extends Filter
+class RuntimeStatisticsFilter extends StatisticsFilter
{
public function __construct($operator, $argument)
{
@@ -160,7 +368,6 @@ class RuntimeFilter extends Filter
public function whereClause(&$args, &$joins)
{
- global $SIZE_RAM;
$upper = time() - (int)$this->argument * 3600;
$lower = $upper - 3600;
$common = "state IN ('OCCUPIED', 'IDLE', 'STANDBY') AND";
@@ -183,7 +390,7 @@ class RuntimeFilter extends Filter
}
}
-class Id44Filter extends Filter
+class Id44StatisticsFilter extends StatisticsFilter
{
public function __construct($operator, $argument)
{
@@ -192,10 +399,9 @@ class Id44Filter extends Filter
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);
+ $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);
}
@@ -220,7 +426,7 @@ class Id44Filter extends Filter
}
}
-class StateFilter extends Filter
+class StateStatisticsFilter extends StatisticsFilter
{
public function __construct($operator, $argument)
{
@@ -232,7 +438,7 @@ class StateFilter extends Filter
$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);
+ $key = StatisticsFilter::getNewKey($this->column);
$args[$key] = $map[$this->argument];
return " machine.state $neg IN ( :$key ) ";
} else {
@@ -242,7 +448,7 @@ class StateFilter extends Filter
}
}
-class LocationFilter extends Filter
+class LocationStatisticsFilter extends StatisticsFilter
{
public function __construct($operator, $argument)
{
@@ -264,7 +470,7 @@ class LocationFilter extends Filter
if ($this->argument === 0) {
return "machine.locationid IS $neg NULL";
} else {
- $key = Filter::getNewKey($this->column);
+ $key = StatisticsFilter::getNewKey($this->column);
if ($recursive) {
$args[$key] = array_keys(Location::getRecursiveFlat($this->argument));
} else {
@@ -275,7 +481,7 @@ class LocationFilter extends Filter
}
}
-class SubnetFilter extends Filter
+class SubnetStatisticsFilter extends StatisticsFilter
{
public function __construct($operator, $argument)
{
@@ -289,7 +495,7 @@ class SubnetFilter extends Filter
}
}
-class IsClientFilter extends Filter
+class IsClientStatisticsFilter extends StatisticsFilter
{
public function __construct($argument)
{
@@ -305,4 +511,7 @@ class IsClientFilter extends Filter
$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/filterset.inc.php b/modules-available/statistics/inc/statisticsfilterset.inc.php
index 774bfd18..c2642850 100644
--- a/modules-available/statistics/inc/filterset.inc.php
+++ b/modules-available/statistics/inc/statisticsfilterset.inc.php
@@ -1,9 +1,9 @@
<?php
-class FilterSet
+class StatisticsFilterSet
{
/**
- * @var \Filter[]
+ * @var \StatisticsFilter[]
*/
private $filters;
private $sortDirection;
@@ -20,7 +20,7 @@ class FilterSet
{
$direction = ($direction === 'DESC' ? 'DESC' : 'ASC');
- if (!is_string($col) || !array_key_exists($col, Page_Statistics::$columns)) {
+ if (!is_string($col) || !array_key_exists($col, StatisticsFilter::$columns)) {
/* default sorting column is clientip */
$col = 'clientip';
}
@@ -56,8 +56,8 @@ class FilterSet
$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) ;
+ $isMapped = array_key_exists('map_sort', StatisticsFilter::$columns[$col]);
+ $concreteCol = ($isMapped ? StatisticsFilter::$columns[$col]['map_sort'] : $col) ;
if ($concreteCol === 'clientip') {
$concreteCol = "INET_ATON(clientip)";
@@ -90,12 +90,12 @@ class FilterSet
return;
$this->cache = false;
// Runmode module exists, add filter
- $this->filters[] = new IsClientFilter(true);
+ $this->filters[] = new IsClientStatisticsFilter(true);
}
/**
* @param string $type filter type (class name)
- * @return false|Filter The filter, false if not found
+ * @return false|StatisticsFilter The filter, false if not found
*/
public function hasFilter($type)
{
@@ -124,7 +124,7 @@ class FilterSet
return true;
unset($this->filters['permissions']);
} else {
- $this->filters['permissions'] = new LocationFilter('=', $locs);
+ $this->filters['permissions'] = new LocationStatisticsFilter('=', $locs);
}
$this->cache = false;
return true;
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 @@
+<?php
+
+class StatisticsStyling
+{
+
+ public static function ramColorClass($mb)
+ {
+ if ($mb < 1500) {
+ return 'danger';
+ }
+ if ($mb < 2500) {
+ return 'warning';
+ }
+
+ return '';
+ }
+
+ public static function kvmColorClass($state)
+ {
+ if ($state === 'DISABLED') {
+ return 'danger';
+ }
+ if ($state === 'UNKNOWN' || $state === 'UNSUPPORTED') {
+ return 'warning';
+ }
+
+ return '';
+ }
+
+ public static function hddColorClass($gb)
+ {
+ if ($gb < 7) {
+ return 'danger';
+ }
+ if ($gb < 25) {
+ return 'warning';
+ }
+
+ return '';
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 4a282515..68649d91 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -1,23 +1,7 @@
<?php
-global $STATS_COLORS, $SIZE_ID44, $SIZE_RAM;
-
-$STATS_COLORS = array();
-for ($i = 0; $i < 10; ++$i) {
- $STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex((($i + 1) * ($i + 1)) / .3922), dechex(abs((5 - $i) * 51)));
-}
-//$STATS_COLORS = array('#57e', '#ee8', '#5ae', '#fb7', '#6d7', '#e77', '#3af', '#666', '#e0e', '#999');
-$SIZE_ID44 = array(0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 2000, 4000);
-$SIZE_RAM = array(1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024);
-
class Page_Statistics extends Page
{
- /* some constants, TODO: Find a better place */
- const OP_NOMINAL = ['!=', '='];
- const OP_ORDINAL = ['!=', '<=', '>=', '=', '<', '>'];
- 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'] .= '<div style="background:#444;left:0;width:' . round((min($row['dateline'], $client['lastboot']) - $cutoff) * $scale, 2) . '%">&nbsp;</div>';
- }
- $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'] .= '<div style="background:' . $color . ';left:' . round(($row['dateline'] - $cutoff) * $scale, 2) . '%;width:' . round(($row['data']) * $scale, 2) . '%">&nbsp;</div>';
- if ($client['isclient']) {
- $spans['rows'][] = $row;
- }
- $last = $row;
- }
- if ($first && $client['lastboot'] > $cutoff) {
- // Special case: offline before
- $spans['graph'] .= '<div style="background:#444;left:0;width:' . round(($client['lastboot'] - $cutoff) * $scale, 2) . '%">&nbsp;</div>';
- } elseif ($first) {
- // Not seen in last two weeks
- $spans['graph'] .= '<div style="background:#444;left:0;width:100%">&nbsp;</div>';
- }
- if ($client['state'] === 'OCCUPIED') {
- $spans['graph'] .= '<div style="background:#e99;left:' . round(($client['logintime'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['logintime'] + 900) * $scale, 2) . '%">&nbsp;</div>';
- $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'] .= '<div style="background:#444;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%">&nbsp;</div>';
- $spans['rows'][] = [
- 'from' => Util::prettyTime($client['lastseen']),
- 'duration' => '-',
- 'glyph' => 'off',
- ];
- } elseif ($client['state'] === 'STANDBY') {
- $spans['graph'] .= '<div style="background:#686;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%">&nbsp;</div>';
- $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'] .= '<div style="background:rgba(0,0,90,.2);left:' . round(($i - $cutoff) * $scale, 2) . '%;width:' . round((10 * 3600) * $scale, 2) . '%">&nbsp;</div>';
- }
- 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 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ User::assertPermission('view.list');
+ }
+
+ public static function doRender()
+ {
+ $sortColumn = Request::any('sortColumn');
+ $sortDirection = Request::any('sortDirection');
+
+ $filters = StatisticsFilter::parseQuery(StatisticsFilter::getQuery());
+ $filterSet = new StatisticsFilterSet($filters);
+ $filterSet->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 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+
+ }
+
+ public static function doRender()
+ {
+ self::showMachine(Request::get('uuid', false, 'string'));
+ }
+
+ private static 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 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'] .= '<div style="background:#444;left:0;width:' . round((min($row['dateline'], $client['lastboot']) - $cutoff) * $scale, 2) . '%">&nbsp;</div>';
+ }
+ $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'] .= '<div style="background:' . $color . ';left:' . round(($row['dateline'] - $cutoff) * $scale, 2) . '%;width:' . round(($row['data']) * $scale, 2) . '%">&nbsp;</div>';
+ if ($client['isclient']) {
+ $spans['rows'][] = $row;
+ }
+ $last = $row;
+ }
+ if ($first && $client['lastboot'] > $cutoff) {
+ // Special case: offline before
+ $spans['graph'] .= '<div style="background:#444;left:0;width:' . round(($client['lastboot'] - $cutoff) * $scale, 2) . '%">&nbsp;</div>';
+ } elseif ($first) {
+ // Not seen in last two weeks
+ $spans['graph'] .= '<div style="background:#444;left:0;width:100%">&nbsp;</div>';
+ }
+ if ($client['state'] === 'OCCUPIED') {
+ $spans['graph'] .= '<div style="background:#e99;left:' . round(($client['logintime'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['logintime'] + 900) * $scale, 2) . '%">&nbsp;</div>';
+ $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'] .= '<div style="background:#444;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%">&nbsp;</div>';
+ $spans['rows'][] = [
+ 'from' => Util::prettyTime($client['lastseen']),
+ 'duration' => '-',
+ 'glyph' => 'off',
+ ];
+ } elseif ($client['state'] === 'STANDBY') {
+ $spans['graph'] .= '<div style="background:#686;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%">&nbsp;</div>';
+ $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'] .= '<div style="background:rgba(0,0,90,.2);left:' . round(($i - $cutoff) * $scale, 2) . '%;width:' . round((10 * 3600) * $scale, 2) . '%">&nbsp;</div>';
+ }
+ 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 @@
+<?php
+
+class SubPage
+{
+
+ private static $STATS_COLORS;
+
+ public static function doPreprocess()
+ {
+ User::assertPermission('view.summary');
+ }
+
+ public static function doRender()
+ {
+ $sortColumn = Request::any('sortColumn');
+ $sortDirection = Request::any('sortDirection');
+
+ $filters = StatisticsFilter::parseQuery(StatisticsFilter::getQuery());
+ $filterSet = new StatisticsFilterSet($filters);
+ $filterSet->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