summaryrefslogtreecommitdiffstats
path: root/modules-available/statistics
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/statistics')
-rw-r--r--modules-available/statistics/api.inc.php166
-rw-r--r--modules-available/statistics/baseconfig/getconfig.inc.php39
-rw-r--r--modules-available/statistics/clientscript.js76
-rw-r--r--modules-available/statistics/config.json1
-rw-r--r--modules-available/statistics/hooks/config-tgz.inc.php4
-rw-r--r--modules-available/statistics/hooks/cron.inc.php58
-rw-r--r--modules-available/statistics/hooks/locations-column.inc.php150
-rw-r--r--modules-available/statistics/hooks/translation.inc.php4
-rw-r--r--modules-available/statistics/inc/devicetype.inc.php6
-rw-r--r--modules-available/statistics/inc/hardwareinfo.inc.php249
-rw-r--r--modules-available/statistics/inc/hardwareparser.inc.php789
-rw-r--r--modules-available/statistics/inc/hardwareparserlegacy.inc.php285
-rw-r--r--modules-available/statistics/inc/hardwarequery.inc.php169
-rw-r--r--modules-available/statistics/inc/hardwarequerycolumn.inc.php94
-rw-r--r--modules-available/statistics/inc/parser.inc.php410
-rw-r--r--modules-available/statistics/inc/pciid.inc.php82
-rw-r--r--modules-available/statistics/inc/statistics.inc.php21
-rw-r--r--modules-available/statistics/inc/statisticsfilter.inc.php382
-rw-r--r--modules-available/statistics/inc/statisticsfilterset.inc.php57
-rw-r--r--modules-available/statistics/inc/statisticshooks.inc.php24
-rw-r--r--modules-available/statistics/inc/statisticsstyling.inc.php30
-rw-r--r--modules-available/statistics/install.inc.php84
-rw-r--r--modules-available/statistics/lang/de/filters.json7
-rw-r--r--modules-available/statistics/lang/de/messages.json2
-rw-r--r--modules-available/statistics/lang/de/module.json5
-rw-r--r--modules-available/statistics/lang/de/permissions.json2
-rw-r--r--modules-available/statistics/lang/de/template-tags.json44
-rw-r--r--modules-available/statistics/lang/en/filters.json7
-rw-r--r--modules-available/statistics/lang/en/messages.json2
-rw-r--r--modules-available/statistics/lang/en/module.json6
-rw-r--r--modules-available/statistics/lang/en/permissions.json2
-rw-r--r--modules-available/statistics/lang/en/template-tags.json44
-rw-r--r--modules-available/statistics/page.inc.php131
-rw-r--r--modules-available/statistics/pages/hints.inc.php221
-rw-r--r--modules-available/statistics/pages/list.inc.php134
-rw-r--r--modules-available/statistics/pages/machine.inc.php420
-rw-r--r--modules-available/statistics/pages/projectors.inc.php6
-rw-r--r--modules-available/statistics/pages/replace.inc.php24
-rw-r--r--modules-available/statistics/pages/summary.inc.php268
-rw-r--r--modules-available/statistics/permissions/permissions.json6
-rw-r--r--modules-available/statistics/style.css22
-rw-r--r--modules-available/statistics/templates/clientlist.html190
-rw-r--r--modules-available/statistics/templates/cpumodels.html25
-rw-r--r--modules-available/statistics/templates/filterbox.html38
-rw-r--r--modules-available/statistics/templates/hints-cpu-legacy.html28
-rw-r--r--modules-available/statistics/templates/hints-hdd-grow.html67
-rw-r--r--modules-available/statistics/templates/hints-nic-speed.html32
-rw-r--r--modules-available/statistics/templates/hints-ram-underclocked.html49
-rw-r--r--modules-available/statistics/templates/hints-ram-upgrade.html32
-rw-r--r--modules-available/statistics/templates/id44.html27
-rw-r--r--modules-available/statistics/templates/js-pciquery.html24
-rw-r--r--modules-available/statistics/templates/kvmstate.html25
-rw-r--r--modules-available/statistics/templates/machine-hdds.html79
-rw-r--r--modules-available/statistics/templates/machine-main.html155
-rw-r--r--modules-available/statistics/templates/memory.html27
-rw-r--r--modules-available/statistics/templates/summary.html50
56 files changed, 4196 insertions, 1185 deletions
diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php
index 30d1fda9..18a58a77 100644
--- a/modules-available/statistics/api.inc.php
+++ b/modules-available/statistics/api.inc.php
@@ -11,17 +11,17 @@ if (substr($ip, 0, 7) === '::ffff:') $ip = substr($ip, 7);
* Power/hw/usage stats
*/
-if ($type{0} === '~') {
+if ($type[0] === '~') {
// UUID is mandatory
$uuid = Request::post('uuid', '', 'string');
$macaddr = Request::post('macaddr', false, 'string');
if ($macaddr !== false) {
$macaddr = strtolower(str_replace(':', '-', $macaddr));
- if (strlen($macaddr) !== 17 || $macaddr{2} !== '-') {
+ if (strlen($macaddr) !== 17 || $macaddr[2] !== '-') {
$macaddr = false;
}
}
- if ($macaddr !== false && $uuid{8} !== '-' && substr($uuid, 0, 16) === '000000000000001-') {
+ if ($macaddr !== false && $uuid[8] !== '-' && substr($uuid, 0, 16) === '000000000000001-') {
$uuid = 'baad1d00-9491-4716-b98b-' . str_replace('-', '', $macaddr);
}
if (strlen($uuid) !== 36 || !preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i', $uuid)) {
@@ -31,7 +31,8 @@ if ($type{0} === '~') {
// External mode of operation?
$mode = Request::post('mode', false, 'string');
$NOW = time();
- $old = Database::queryFirst('SELECT clientip, logintime, lastseen, lastboot, state, mbram, cpumodel, live_memfree, live_swapfree, live_tmpfree
+ $old = Database::queryFirst('SELECT clientip, locationid, logintime, lastseen, lastboot, state, mbram,
+ cpumodel, live_memfree, live_swapfree, live_tmpfree
FROM machine WHERE machineuuid = :uuid', array('uuid' => $uuid));
if ($old !== false) {
settype($old['logintime'], 'integer');
@@ -61,10 +62,26 @@ if ($type{0} === '~') {
if (!is_string($hostname) || $hostname === $ip) {
$hostname = '';
}
- $data = Util::cleanUtf8(Request::post('data', '', 'string'));
+ $json = false;
+ $data = Util::cleanUtf8(Request::post('json', '', 'string'));
+ if (!empty($data) && $data[0] === '{') {
+ $json = json_decode($data, true);
+ if (!is_array($json)) {
+ $json = false;
+ } else {
+ $json['cpu'] = [
+ 'sockets' => Request::post('sockets', 0, 'int'),
+ 'cores' => $realcores,
+ 'threads' => Request::post('vcores', 0, 'int'),
+ ];
+ }
+ }
+ if ($json === false) {
+ $data = Util::cleanUtf8(Request::post('data', '', 'string'));
+ }
// Prepare insert/update to machine table
$new = array(
- 'uuid' => $uuid,
+ 'machineuuid'=> $uuid,
'macaddr' => $macaddr,
'clientip' => $ip,
'lastseen' => $NOW,
@@ -86,7 +103,7 @@ if ($type{0} === '~') {
$res = Database::exec('INSERT INTO machine '
. '(machineuuid, macaddr, clientip, firstseen, lastseen, logintime, position, lastboot, realcores, mbram,'
. ' kvmstate, cpumodel, systemmodel, id44mb, badsectors, data, hostname, state) VALUES '
- . "(:uuid, :macaddr, :clientip, :firstseen, :lastseen, 0, '', :lastboot, :realcores, :mbram,"
+ . "(:machineuuid, :macaddr, :clientip, :firstseen, :lastseen, 0, '', :lastboot, :realcores, :mbram,"
. ' :kvmstate, :cpumodel, :systemmodel, :id44mb, :badsectors, :data, :hostname, :state)', $new, true);
if ($res === false) {
die("Concurrent insert, ignored. (RESULT=0)\n");
@@ -118,9 +135,9 @@ if ($type{0} === '~') {
. ' id44mb = :id44mb,'
. ' live_tmpsize = 0, live_swapsize = 0, live_memsize = 0, live_cpuload = 255, live_cputemp = 0,'
. ' badsectors = :badsectors,'
- . ' data = :data,'
+ . ' data = ' . ($json !== false ? ':data' : "If(Left(data, 1) = '{', data, :data)") . ','
. ' state = :state '
- . " WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen", $new);
+ . " WHERE machineuuid = :machineuuid AND state = :oldstate AND lastseen = :oldlastseen", $new);
if ($res === 0) {
die("Concurrent update, ignored. (RESULT=0)\n");
}
@@ -146,7 +163,17 @@ if ($type{0} === '~') {
if (($old === false || $old['clientip'] !== $ip) && Module::isAvailable('locations')) {
// New, or ip changed (dynamic pool?), update subnetlicationid
- Location::updateMapIpToLocation($uuid, $ip);
+ $loc = Location::updateMapIpToLocation($uuid, $ip);
+ $new['locationid'] = $loc; // For Filter Event
+ }
+
+ if ($json !== false) {
+ $ret = HardwareParser::parseMachine($uuid, $json);
+ if ($ret !== null) {
+ // This data is more accurate and ends up in the DB anyways, so use it for event filtering too
+ $new['id44mb'] = $ret['id44mb'];
+ $new['id45mb'] = $ret['id45mb'];
+ }
}
// Check for suspicious hardware changes
@@ -155,16 +182,31 @@ if ($type{0} === '~') {
// Log potential crash
if ($old['state'] === 'IDLE' || $old['state'] === 'OCCUPIED') {
- writeClientLog('machine-mismatch-poweron', 'Poweron event, but previous known state is ' . $old['state']
- . '. Free RAM: ' . Util::readableFileSize($old['live_memfree'], -1, 2)
- . ', free Swap: ' . Util::readableFileSize($old['live_swapfree'], -1, 2)
- . ', free ID44: ' . Util::readableFileSize($old['live_tmpfree'], -1, 2));
+ if (Module::isAvailable('syslog')) {
+ ClientLog::write($new, 'machine-mismatch-poweron',
+ 'Poweron event, but previous known state is ' . $old['state']
+ . '. Free RAM: ' . Util::readableFileSize($old['live_memfree'], -1, 2)
+ . ', free Swap: ' . Util::readableFileSize($old['live_swapfree'], -1, 2)
+ . ', free ID44: ' . Util::readableFileSize($old['live_tmpfree'], -1, 2));
+ }
}
+ // Add anything not present in $new from $old
+ $new += $old;
+ $new['oldlastboot'] = $old['lastboot'];
+ } else {
+ // First boot, mock some important fields for event log filtering
+ $new['oldlastboot'] = 0;
+ $new['oldlastseen'] = 0;
+ $new['oldstate'] = 'OFFLINE';
}
+ unset($new['data']);
+ EventLog::applyFilterRules($type, $new);
+
// Write statistics data
} else if ($type === '~runstate') {
+
// Usage (occupied/free)
$sessionLength = 0;
$strUpdateBoottime = '';
@@ -174,10 +216,14 @@ if ($type{0} === '~') {
}
$used = Request::post('used', 0, 'integer');
$params = array(
- 'uuid' => $uuid,
+ 'machineuuid' => $uuid,
'oldlastseen' => $old['lastseen'],
'oldstate' => $old['state'],
);
+ if ($NOW - $old['lastseen'] < 10 && ($old['state'] === 'OFFLINE' || $old['state'] === 'STANDBY')) {
+ // Avoid racing calls to ~runstate updates while/after we send a ~poweroff or ~suspend
+ die("OK.\n");
+ }
if ($old['state'] === 'OFFLINE') {
// This should never happen -- we expect a poweron event before runstate, which would set the state to IDLE
// So it might be that the poweron event got lost, or that a couple of runstate events got lost, which
@@ -199,14 +245,18 @@ if ($type{0} === '~') {
'memfree', 'tmpfree', 'swapfree', 'id45free',
'cpuload', 'cputemp'] as $item) {
$liveVal = Request::post($item, false, 'int');
- if ($liveVal !== false) {
- $strUpdateBoottime .= ' live_' . $item . ' = :_' . $item . ', ';
+ if ($liveVal !== false && $liveVal >= 0) {
+ $strUpdateBoottime .= ' live_' . $item . ' = :live_' . $item . ', ';
if ($item === 'cpuload' || $item === 'cputemp') {
$liveVal = round($liveVal);
} else {
$liveVal = ceil($liveVal / 1024);
}
- $params['_' . $item] = $liveVal;
+ $max = ($item === 'cpuload') ? 100 : (2 ** 31);
+ if ($liveVal > $max) {
+ $liveVal = $max;
+ }
+ $params['live_' . $item] = $liveVal;
}
}
if (($runmode = Request::post('runmode', false, 'string')) !== false) {
@@ -222,7 +272,7 @@ if ($type{0} === '~') {
$res = Database::exec('UPDATE machine SET lastseen = UNIX_TIMESTAMP(),'
. $strUpdateBoottime
. " logintime = 0, currentuser = NULL, state = 'IDLE' "
- . " WHERE machineuuid = :uuid AND lastseen = :oldlastseen AND state = :oldstate",
+ . " WHERE machineuuid = :machineuuid AND lastseen = :oldlastseen AND state = :oldstate",
$params);
} elseif ($used === 1 && $old['state'] !== 'OCCUPIED') {
// Machine is in use, was free before
@@ -235,7 +285,7 @@ if ($type{0} === '~') {
$res = Database::exec('UPDATE machine SET lastseen = UNIX_TIMESTAMP(),'
. $strUpdateBoottime
. " logintime = UNIX_TIMESTAMP(), currentuser = :user, currentsession = NULL, state = 'OCCUPIED' "
- . " WHERE machineuuid = :uuid AND lastseen = :oldlastseen AND state = :oldstate", $params);
+ . " WHERE machineuuid = :machineuuid AND lastseen = :oldlastseen AND state = :oldstate", $params);
} else {
$res = 0;
}
@@ -243,7 +293,7 @@ if ($type{0} === '~') {
// Nothing changed, simple lastseen update
$res = Database::exec('UPDATE machine SET '
. $strUpdateBoottime
- . ' lastseen = UNIX_TIMESTAMP() WHERE machineuuid = :uuid AND lastseen = :oldlastseen AND state = :oldstate', $params);
+ . ' lastseen = UNIX_TIMESTAMP() WHERE machineuuid = :machineuuid AND lastseen = :oldlastseen AND state = :oldstate', $params);
}
// Did we update, or was there a concurrent update?
if ($res === 0) {
@@ -253,7 +303,12 @@ if ($type{0} === '~') {
if ($mode === false && $sessionLength > 0 && $sessionLength < 86400*2 && $old['logintime'] !== 0) {
Statistics::logMachineState($uuid, $ip, Statistics::SESSION_LENGTH, $old['logintime'], $sessionLength);
}
+ // Client Events
+ $params['newstate'] = ($used === 0) ? 'IDLE' : 'OCCUPIED';
+ EventLog::applyFilterRules($type, $params + $old);
+
} elseif ($type === '~poweroff') {
+
if ($old === false) die("Unknown machine.\n");
if ($old['clientip'] !== $ip) {
updateIp('poweroff', $uuid, $old, $ip);
@@ -267,7 +322,11 @@ if ($type{0} === '~') {
Database::exec("UPDATE machine SET logintime = 0, lastseen = UNIX_TIMESTAMP(), state = 'OFFLINE'
WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen",
array('uuid' => $uuid, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state']));
+
+ EventLog::applyFilterRules($type, $old);
+
} elseif ($mode === false && $type === '~screens') {
+
if ($old === false) die("Unknown machine.\n");
$screens = Request::post('screen', false, 'array');
if (is_array($screens)) {
@@ -279,24 +338,24 @@ if ($type{0} === '~') {
if (!array_key_exists('name', $screen))
continue;
// Filter bogus data
- $screen['name'] = Util::cleanUtf8($screen['name']);
- $port = Util::cleanUtf8($port);
+ $screen['name'] = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $screen['name']);
+ $port = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $port);
if (empty($screen['name']))
continue;
if (array_key_exists($screen['name'], $hwids)) {
$hwid = $hwids[$screen['name']];
} else {
- $hwid = (int)Database::insertIgnore('statistic_hw', 'hwid',
- array('hwtype' => DeviceType::SCREEN, 'hwname' => $screen['name']));
+ $hwid = Database::insertIgnore('statistic_hw', 'hwid',
+ ['hwtype' => HardwareInfo::SCREEN, 'hwname' => $screen['name']]);
$hwids[$screen['name']] = $hwid;
}
// Now add new entries
$keepPair[] = array($hwid, $port);
- $machinehwid = Database::insertIgnore('machine_x_hw', 'machinehwid', array(
+ $machinehwid = Database::insertIgnore('machine_x_hw', 'machinehwid', [
'hwid' => $hwid,
'machineuuid' => $uuid,
'devpath' => $port,
- ), array('disconnecttime' => 0));
+ ], ['disconnecttime' => 0]);
$validProps = array();
if (count($screen) > 1) {
// Screen has additional properties (resolution, size, etc.)
@@ -333,20 +392,27 @@ if ($type{0} === '~') {
Database::exec("UPDATE machine_x_hw x, statistic_hw h
SET x.disconnecttime = UNIX_TIMESTAMP()
WHERE x.machineuuid = :uuid AND x.hwid = h.hwid AND h.hwtype = :type AND x.disconnecttime = 0",
- array('uuid' => $uuid, 'type' => DeviceType::SCREEN));
+ array('uuid' => $uuid, 'type' => HardwareInfo::SCREEN));
} else {
// Some screens connected, make sure old entries get removed
Database::exec("UPDATE machine_x_hw x, statistic_hw h
SET x.disconnecttime = UNIX_TIMESTAMP()
- WHERE (x.hwid, x.devpath) NOT IN (:pairs) AND x.disconnecttime = 0 AND h.hwtype = :type
+ WHERE (x.hwid, x.devpath) NOT IN (:pairs) AND x.hwid = h.hwid AND x.disconnecttime = 0 AND h.hwtype = :type
AND x.machineuuid = :uuid", array(
'pairs' => $keepPair,
'uuid' => $uuid,
- 'type' => DeviceType::SCREEN,
+ 'type' => HardwareInfo::SCREEN,
));
}
+
+ // Benchmarking
+ Database::exec("INSERT INTO statistic (dateline, typeid, clientip, machineuuid, username, `data`)
+ VALUES (UNIX_TIMESTAMP(), :type, :ip, :uuid, '', '')",
+ ['type' => 'graphical-startup', 'ip' => $ip, 'uuid' => $uuid]);
}
+
} else if ($type === '~suspend') {
+
// Client entering suspend
if ($old === false) die("Unknown machine.\n");
if ($old['clientip'] !== $ip) {
@@ -357,10 +423,13 @@ if ($type{0} === '~') {
standbysem = If(standbysem < 6, standbysem + 1, 6)
WHERE machineuuid = :uuid AND state = :oldstate AND lastseen = :oldlastseen",
array('uuid' => $uuid, 'oldlastseen' => $old['lastseen'], 'oldstate' => $old['state']));
+ EventLog::applyFilterRules($type, $old);
} else {
EventLog::info("[suspend] Client $uuid reported switch to standby when it wasn't powered on first. Was: " . $old['state']);
}
+
} else if ($type === '~resume') {
+
// Waking up from suspend
if ($old === false) die("Unknown machine.\n");
if ($old['clientip'] !== $ip) {
@@ -379,6 +448,7 @@ if ($type{0} === '~') {
Statistics::logMachineState($uuid, $ip, Statistics::SUSPEND_LENGTH, $lastSeen, $duration);
}
}
+ EventLog::applyFilterRules($type, $old);
} else {
EventLog::info("[resume] Client $uuid reported wakeup from standby when it wasn't logged as being in standby. Was: " . $old['state']);
}
@@ -413,21 +483,9 @@ function writeStatisticLog($type, $username, $data)
));
}
-function writeClientLog($type, $description)
-{
- global $ip, $uuid;
- Database::exec('INSERT INTO clientlog (dateline, logtypeid, clientip, machineuuid, description, extra) VALUES (UNIX_TIMESTAMP(), :type, :client, :uuid, :description, :longdesc)', array(
- 'type' => $type,
- 'client' => $ip,
- 'description' => $description,
- 'longdesc' => '',
- 'uuid' => $uuid,
- ));
-}
-
-
// For backwards compat, we require the . prefix
-if ($type{0} === '.') {
+if ($type[0] === '.') {
+ $data = false;
if ($type === '.vmchooser-session') {
$user = Util::cleanUtf8(Request::post('user', 'unknown', 'string'));
$loguser = Request::post('loguser', 0, 'int') !== 0;
@@ -437,21 +495,35 @@ if ($type{0} === '.') {
Database::exec("UPDATE machine SET currentuser = :user, currentsession = :session WHERE clientip = :ip",
compact('user', 'session', 'ip'));
writeStatisticLog('.vmchooser-session-name', ($loguser ? $user : 'anonymous'), $sessionName);
+ $data = [
+ 'clientip' => $ip,
+ 'sessionName' => $sessionName,
+ 'sessionUuid' => $sessionUuid,
+ 'session' => $session,
+ ];
} else {
if (!isset($_POST['description'])) die('Missing options..');
$description = $_POST['description'];
// and username embedded in message
if (preg_match('#^\[([^\]]+)\]\s*(.*)$#m', $description, $out)) {
writeStatisticLog($type, $out[1], $out[2]);
+ $data = [
+ 'clientip' => $ip,
+ 'user' => $out[1],
+ 'description' => $out[2],
+ ];
}
}
+ if ($data !== false) {
+ EventLog::applyFilterRules($type, $data);
+ }
}
/**
* @param array $old row from DB with client's old data
* @param array $new new data to be written
*/
-function checkHardwareChange($old, $new)
+function checkHardwareChange(array $old, array $new): void
{
if ($new['mbram'] !== 0) {
if ($new['mbram'] < 6200) {
@@ -463,10 +535,10 @@ function checkHardwareChange($old, $new)
}
if ($ram1 !== $ram2) {
$word = $ram1 > $ram2 ? 'decreased' : 'increased';
- EventLog::warning('[poweron] Client ' . $new['uuid'] . ' (' . $new['clientip'] . "): RAM $word from {$ram1}GB to {$ram2}GB");
+ EventLog::warning('[poweron] Client ' . $new['machineuuid'] . ' (' . $new['clientip'] . "): RAM $word from {$ram1}GB to {$ram2}GB");
}
if (!empty($old['cpumodel']) && !empty($new['cpumodel']) && $new['cpumodel'] !== $old['cpumodel']) {
- EventLog::warning('[poweron] Client ' . $new['uuid'] . ' (' . $new['clientip'] . "): CPU changed from '{$old['cpumodel']}' to '{$new['cpumodel']}'");
+ EventLog::warning('[poweron] Client ' . $new['machineuuid'] . ' (' . $new['clientip'] . "): CPU changed from '{$old['cpumodel']}' to '{$new['cpumodel']}'");
}
}
}
diff --git a/modules-available/statistics/baseconfig/getconfig.inc.php b/modules-available/statistics/baseconfig/getconfig.inc.php
index e8afeffb..f90cd49d 100644
--- a/modules-available/statistics/baseconfig/getconfig.inc.php
+++ b/modules-available/statistics/baseconfig/getconfig.inc.php
@@ -1,36 +1,41 @@
<?php
+/** @var ?string $uuid */
+/** @var ?string $ip */
+
// Location handling: figure out location
if (Request::any('force', 0, 'int') === 1 && Request::any('module', false, 'string') === 'statistics') {
// Force location for testing, but require logged in admin
if (User::load()) {
- $uuid = Request::any('value', '', 'string');
+ $uuid = Request::any('value', null, 'string');
}
}
-if (!$uuid) // Required at this point, bail out if not given
+if ($uuid === null) // Required at this point, bail out if not given
return;
// Query machine specific settings
$res = Database::simpleQuery("SELECT setting, value FROM setting_machine WHERE machineuuid = :uuid", ['uuid' => $uuid]);
-while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+foreach ($res as $row) {
ConfigHolder::add($row['setting'], $row['value'], 500);
}
+if ($ip !== null) {
// Statistics about booted system
-ConfigHolder::addPostHook(function() use ($ip, $uuid) {
- $type = Request::get('type', 'default', 'string');
- // System
- if ($type !== 'default') {
- Database::exec("INSERT INTO statistic (dateline, typeid, clientip, machineuuid, username, data)
+ ConfigHolder::addPostHook(function () use ($ip, $uuid) {
+ $type = Request::get('type', 'default', 'string');
+ // System
+ if ($type !== 'default') {
+ Database::exec("INSERT INTO statistic (dateline, typeid, clientip, machineuuid, username, data)
VALUES (UNIX_TIMESTAMP(), :type, :ip, :uuid, '', :data)",
- ['type' => 'boot-system', 'ip' => $ip, 'uuid' => $uuid, 'data' => $type]);
- }
- // Runmode
- $mode = ConfigHolder::get('SLX_RUNMODE_MODULE');
- if (!empty($mode)) {
- Database::exec("INSERT INTO statistic (dateline, typeid, clientip, machineuuid, username, data)
+ ['type' => 'boot-system', 'ip' => $ip, 'uuid' => $uuid, 'data' => $type]);
+ }
+ // Runmode
+ $mode = ConfigHolder::get('SLX_RUNMODE_MODULE');
+ if (!empty($mode)) {
+ Database::exec("INSERT INTO statistic (dateline, typeid, clientip, machineuuid, username, data)
VALUES (UNIX_TIMESTAMP(), :type, :ip, :uuid, '', :data)",
- ['type' => 'boot-runmode', 'ip' => $ip, 'uuid' => $uuid, 'data' => $mode]);
- }
-});
+ ['type' => 'boot-runmode', 'ip' => $ip, 'uuid' => $uuid, 'data' => $mode]);
+ }
+ });
+} \ No newline at end of file
diff --git a/modules-available/statistics/clientscript.js b/modules-available/statistics/clientscript.js
new file mode 100644
index 00000000..3c166f64
--- /dev/null
+++ b/modules-available/statistics/clientscript.js
@@ -0,0 +1,76 @@
+'use strict';
+
+// All the pie chars
+function makePieChart($parent) {
+ var data = $parent.data('chart');
+ var chartData = {
+ datasets: [{
+ data: data.map(function(x) { return x.value; }),
+ backgroundColor: data.map(function(x) { return x.color; })
+ }]
+ };
+ var $canv = $('<canvas style="width:100%;height:250px">');
+ $parent.append($canv);
+ (function() {
+ var $dest = $parent.data('chart-dest');
+ var cur = null;
+ new Chart($canv[0].getContext('2d'), {
+ type: 'pie', data: chartData, options: {
+ animation: false,
+ onHover: function (_, list) {
+ if (list.length === 0 || list[0].index !== cur) {
+ if (cur !== null) {
+ $($dest + cur).removeClass('slx-bold');
+ cur = null;
+ }
+ }
+ if (list.length !== 0 && list[0].index !== cur) {
+ cur = list[0].index;
+ $($dest + cur).addClass('slx-bold');
+ }
+ },
+ plugins: {
+ tooltip: {enabled: false},
+ legend: {display: false}
+ }
+ }
+ });
+ $canv.mouseout(function() {
+ if (cur !== null) {
+ $($dest + cur).removeClass('slx-bold');
+ cur = null;
+ }
+ });
+ })();
+}
+
+function popupFilter(field) {
+ var $row = addFilter(field, null, null);
+ if ($row !== null) {
+ $row.find('.arg').focus();
+ $row.removeClass('slx-focus')
+ setTimeout(function() { $row.addClass('slx-focus'); }, 10);
+ }
+}
+
+function addFilter(field, op, argument) {
+ if (field === null)
+ return null;
+ var $row = $('#filter-' + field);
+ if ($row.length === 0)
+ return null;
+ if (argument !== null) {
+ $row.find('.op').val(op);
+ $row.find('.arg').val(argument);
+ }
+ // Enable checkbox only if we got a predefined value, or if argument is in a select, as the user might want the preselected item and doesn't notice the checkbox is unchecked
+ if (argument !== null || $row.find('select.arg').length !== 0) {
+ $row.find('.filter-enable').prop('checked', true);
+ }
+ $row.show();
+ return $row;
+}
+
+function refresh() {
+ $('#query-form').submit();
+} \ No newline at end of file
diff --git a/modules-available/statistics/config.json b/modules-available/statistics/config.json
index 11d3fba3..a683ab6a 100644
--- a/modules-available/statistics/config.json
+++ b/modules-available/statistics/config.json
@@ -1,7 +1,6 @@
{
"category": "main.status",
"dependencies": [
- "js_chart",
"bootstrap_datepicker"
],
"permission": "0"
diff --git a/modules-available/statistics/hooks/config-tgz.inc.php b/modules-available/statistics/hooks/config-tgz.inc.php
index 8dffbff6..b732fd7a 100644
--- a/modules-available/statistics/hooks/config-tgz.inc.php
+++ b/modules-available/statistics/hooks/config-tgz.inc.php
@@ -4,12 +4,12 @@ $res = Database::simpleQuery('SELECT h.hwname FROM statistic_hw h'
. " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)"
. " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array(
'projector' => 'projector',
- 'screen' => DeviceType::SCREEN,
+ 'screen' => HardwareInfo::SCREEN,
));
if ($res !== false) { // CHeck this in case we're running on old DB during update
$content = '';
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$content .= $row['hwname'] . "=beamer\n";
}
diff --git a/modules-available/statistics/hooks/cron.inc.php b/modules-available/statistics/hooks/cron.inc.php
index 0de233a8..4ba5e2f6 100644
--- a/modules-available/statistics/hooks/cron.inc.php
+++ b/modules-available/statistics/hooks/cron.inc.php
@@ -9,12 +9,42 @@ function logstats()
$join = 'LEFT JOIN runmode r USING (machineuuid)';
$where = 'AND (r.isclient IS NULL OR r.isclient <> 0)';
}
- $known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE m.lastseen > $cutoff $where");
- $on = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE m.state IN ('IDLE', 'OCCUPIED') $where");
- $used = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE m.state = 'OCCUPIED' $where");
+ // Get total/online/in-use
+ $known = Database::queryKeyValueList("SELECT locationid, Count(*) AS val FROM machine m
+ $join WHERE m.lastseen > $cutoff $where
+ GROUP BY locationid");
+ $on = Database::queryKeyValueList("SELECT locationid, Count(*) AS val FROM machine m
+ $join WHERE m.state IN ('IDLE', 'OCCUPIED') $where
+ GROUP BY locationid");
+ $used = Database::queryKeyValueList("SELECT locationid, Count(*) AS val FROM machine m
+ $join WHERE m.state = 'OCCUPIED' $where
+ GROUP BY locationid");
+ // Get calendar data if available
+ if (Module::isAvailable('locationinfo')) {
+ // Refresh all calendars around 07:00
+ $calendars = LocationInfo::getAllCalendars(date('G') != 7 || date('i') >= 10);
+ }
+ // Mash together
+ $data = ['usage' => []];
+ foreach ($known as $lid => $val) {
+ $entry = ['t' => $val];
+ if (isset($on[$lid])) {
+ $entry['o'] = $on[$lid];
+ }
+ if (isset($used[$lid])) {
+ $entry['u'] = $used[$lid];
+ }
+ if (isset($calendars[$lid])) {
+ $title = LocationInfo::extractCurrentEvent($calendars[$lid]);
+ if (!empty($title)) {
+ $entry['event'] = $title;
+ }
+ }
+ $data['usage'][$lid] = $entry;
+ }
Database::exec("INSERT INTO statistic (dateline, typeid, clientip, username, data) VALUES (:now, '~stats', '', '', :vals)", array(
'now' => $NOW,
- 'vals' => $known['val'] . '#' . $on['val'] . '#' . $used['val'],
+ 'vals' => json_encode($data),
));
}
@@ -26,18 +56,12 @@ function state_cleanup()
// Query for logging
$res = Database::simpleQuery("SELECT machineuuid, clientip, state, logintime, lastseen, live_memfree, live_swapfree, live_tmpfree
FROM machine WHERE lastseen < If(state = 'STANDBY', $standby, $on) AND state <> 'OFFLINE'");
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- Database::exec('INSERT INTO clientlog (dateline, logtypeid, clientip, machineuuid, description, extra)
- VALUES (UNIX_TIMESTAMP(), :type, :client, :uuid, :description, :longdesc)', array(
- 'type' => 'machine-mismatch-cron',
- 'client' => $row['clientip'],
- 'description' => 'Client timed out, last known state is ' . $row['state']
- . '. Free RAM: ' . Util::readableFileSize($row['live_memfree'], -1, 2)
- . ', free Swap: ' . Util::readableFileSize($row['live_swapfree'], -1, 2)
- . ', free ID44: ' . Util::readableFileSize($row['live_tmpfree'], -1, 2),
- 'longdesc' => '',
- 'uuid' => $row['machineuuid'],
- ));
+ foreach ($res as $row) {
+ ClientLog::write($row, 'machine-mismatch-cron',
+ 'Client timed out, last known state is ' . $row['state']
+ . '. Free RAM: ' . Util::readableFileSize($row['live_memfree'], -1, 2)
+ . ', free Swap: ' . Util::readableFileSize($row['live_swapfree'], -1, 2)
+ . ', free ID44: ' . Util::readableFileSize($row['live_tmpfree'], -1, 2));
if ($row['state'] === 'OCCUPIED') {
$length = $row['lastseen'] - $row['logintime'];
if ($length > 0 && $length < 86400 * 7) {
@@ -59,7 +83,7 @@ state_cleanup();
logstats();
if (mt_rand(1, 10) === 1) {
- Database::exec("DELETE FROM statistic WHERE (UNIX_TIMESTAMP() - 86400 * 190) > dateline");
+ Database::exec("DELETE FROM statistic WHERE (UNIX_TIMESTAMP() - 86400 * 365 * 2) > dateline");
if (mt_rand(1, 100) === 1) {
Database::exec("OPTIMIZE TABLE statistic");
}
diff --git a/modules-available/statistics/hooks/locations-column.inc.php b/modules-available/statistics/hooks/locations-column.inc.php
new file mode 100644
index 00000000..51f280be
--- /dev/null
+++ b/modules-available/statistics/hooks/locations-column.inc.php
@@ -0,0 +1,150 @@
+<?php
+
+if (!User::hasPermission('.statistics.view.list')) {
+ return null;
+}
+
+class ClientCountLocationColumn extends AbstractLocationColumn
+{
+
+ private $lookup;
+
+ public function __construct()
+ {
+ $this->lookup = StatisticsColumnGetData([]);
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ if (!isset($this->lookup[$locationId]))
+ return '';
+ if ($this->lookup[$locationId]['hasChild'] ?? false) {
+ $child = <<<EOF
+ (<a href="?do=Statistics&amp;show=list&amp;filters=location~{$locationId}">&downarrow;{$this->lookup[$locationId]['clientCountSum']}</a>)
+EOF;
+ } else {
+ $child = '';
+ }
+
+ return <<<EOF
+ <div class="pull-right">
+ <a href="?do=Statistics&amp;show=list&amp;filters=location={$locationId}">&nbsp;{$this->lookup[$locationId]['clientCount']}&nbsp;</a>
+ <span class="text-right" style="display:inline-block;width:6ex">$child</span>
+ </div>
+EOF;
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ return '';
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('statistics', 'module', 'location-column-header-count');
+ }
+
+ public function priority(): int
+ {
+ return 800;
+ }
+
+}
+
+class ClientLoadLocationColumn extends AbstractLocationColumn
+{
+
+ private $lookup;
+
+ public function __construct()
+ {
+ $this->lookup = StatisticsColumnGetData([]);
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ if (!isset($this->lookup[$locationId]) || $this->lookup[$locationId]['clientCount'] === 0)
+ return '';
+ $c =& $this->lookup[$locationId];
+ return <<<EOF
+ <div class="load-col text-right" style="background:linear-gradient(to right, #f97, #f97 {$c['clientLoad']}%,
+ #6fa {$c['clientLoad']}%, #6fa {$c['clientIdle']}%, #eee {$c['clientIdle']}%)">
+ {$c['clientLoad']}&thinsp;%
+ </div>
+EOF;
+
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ return '';
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('statistics', 'module', 'location-column-header-load');
+ }
+
+ public function priority(): int
+ {
+ return 900;
+ }
+
+}
+
+function StatisticsColumnGetData(array $allowedLocationIds): array
+{
+ static $data = [];
+ if (!empty($data))
+ return $data;
+ $extra = '';
+ if (in_array(0, $allowedLocationIds)) {
+ $extra = ' OR locationid IS NULL';
+ }
+ $locs = Location::getLocationsAssoc();
+ $res = Database::simpleQuery("SELECT m.locationid, Count(*) AS cnt,
+ Sum(If(m.state = 'OCCUPIED', 1, 0)) AS used, Sum(If(m.state = 'IDLE', 1, 0)) AS idle
+ FROM machine m WHERE (locationid IN (:allowedLocationIds) $extra) GROUP BY locationid", compact('allowedLocationIds'));
+ foreach ($res as $row) {
+ $locId = (int)$row['locationid'];
+ $data[$locId] = [
+ 'clientCount' => $row['cnt'],
+ 'clientLoad' => round(100 * $row['used'] / $row['cnt']),
+ 'clientIdle' => round(100 * ($row['used'] + $row['idle']) / $row['cnt']),
+ ];
+ }
+ foreach ($allowedLocationIds as $locId) {
+ if (isset($data[$locId]))
+ continue;
+ $data[$locId] = [
+ 'clientCount' => 0,
+ 'clientLoad' => 0,
+ 'clientIdle' => 0,
+ ];
+ }
+ foreach ($data as $locId => &$loc) {
+ if (!in_array($locId, $allowedLocationIds))
+ continue;
+ if (!isset($loc['clientCountSum'])) {
+ $loc['clientCountSum'] = 0;
+ }
+ $loc['clientCountSum'] += $loc['clientCount'];
+ if ($locId !== 0) {
+ foreach ($locs[$locId]['parents'] as $pid) {
+ if (!in_array($pid, $allowedLocationIds))
+ continue;
+ $data[$pid]['hasChild'] = true;
+ if (!isset($data[$pid]['clientCountSum'])) {
+ $data[$pid]['clientCountSum'] = 0;
+ }
+ $data[$pid]['clientCountSum'] += $loc['clientCount'];
+ }
+ }
+ }
+ unset($loc);
+ return $data;
+}
+
+StatisticsColumnGetData($allowedLocationIds);
+
+return [new ClientCountLocationColumn(), new ClientLoadLocationColumn()]; \ No newline at end of file
diff --git a/modules-available/statistics/hooks/translation.inc.php b/modules-available/statistics/hooks/translation.inc.php
index 4d09553a..f7a50b0d 100644
--- a/modules-available/statistics/hooks/translation.inc.php
+++ b/modules-available/statistics/hooks/translation.inc.php
@@ -16,10 +16,8 @@ $HANDLER['subsections'] = array(
/**
* Configuration categories.
- * @param \Module $module
- * @return array
*/
-$HANDLER['grep_filters'] = function($module) {
+$HANDLER['grep_filters'] = function (Module $module): array {
if (!$module->activate(1, false))
return array();
$want = StatisticsFilter::$columns;
diff --git a/modules-available/statistics/inc/devicetype.inc.php b/modules-available/statistics/inc/devicetype.inc.php
deleted file mode 100644
index 41ee237d..00000000
--- a/modules-available/statistics/inc/devicetype.inc.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class DeviceType
-{
- const SCREEN = 'SCREEN';
-}
diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php
new file mode 100644
index 00000000..7e0bdba8
--- /dev/null
+++ b/modules-available/statistics/inc/hardwareinfo.inc.php
@@ -0,0 +1,249 @@
+<?php
+
+class HardwareInfo
+{
+
+ // Never change these!
+ const RAM_MODULE = 'RAM';
+ const MAINBOARD = 'MAINBOARD';
+ const DMI_SYSTEM = 'DMI_SYSTEM';
+ const POWER_SUPPLY = 'POWER_SUPPLY';
+ const SYSTEM_SLOT = 'SYSTEM_SLOT';
+ const PCI_DEVICE = 'PCI_DEVICE';
+ const HDD = 'HDD';
+ const CPU = 'CPU';
+ const SCREEN = 'SCREEN';
+
+ /**
+ * Get a KCL modification string for the given machine, enabling GVT, PCI passthrough etc.
+ * You can provide a UUID and/or MAC, or nothing. If nothing is provided,
+ * the "uuid" and "mac" GET parameters will be used. If both are provided,
+ * the resulting machine that has the greatest "lastseen" value will be used.
+ * @param ?string $uuid UUID of machine
+ * @param ?string $mac MAC of machine
+ */
+ public static function getKclModifications(?string $uuid = null, ?string $mac = null): string
+ {
+ if ($uuid === null && $mac === null) {
+ $uuid = Request::get('uuid', '', 'string');
+ $mac = Request::get('mac', '', 'string');
+ $mac = str_replace(':', '-', $mac);
+ }
+ $res = Database::simpleQuery("SELECT machineuuid, lastseen, cpumodel, locationid FROM machine
+ WHERE machineuuid = :uuid OR macaddr = :mac", ['uuid' => $uuid, 'mac' => $mac]);
+ $best = null;
+ foreach ($res as $row) {
+ if ($best === null || $best['lastseen'] < $row['lastseen']) {
+ $best = $row;
+ }
+ }
+ if ($best === null || ((int)$best['locationid']) === 0)
+ return '';
+ $locations = Location::getLocationRootChain($best['locationid']);
+ if (empty($locations))
+ return '';
+ $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true);
+ // TODO: Get list of enabled pass through groups for this client's location
+ $hw->addForeignJoin(true, '@PASSTHROUGH', 'passthrough_group_x_location', 'groupid',
+ 'locationid', $locations);
+ $hw->addGlobalColumn('vendor');
+ $hw->addGlobalColumn('device');
+ $hw->addLocalColumn('slot');
+ $res = $hw->query(['vendor', 'device']);
+ $passthrough = [];
+ $slots = [];
+ $gvt = false;
+ foreach ($res as $row) {
+ if ($row['@PASSTHROUGH'] === 'GVT') {
+ $gvt = true;
+ } else {
+ $passthrough[$row['vendor'] . ':' . $row['device']] = 1;
+ $slots[preg_replace('/\.[0-9]+$/', '', $row['slot'])] = 1;
+ }
+ }
+ $kcl = '';
+ if ($gvt || !empty($passthrough)) {
+ if (strpos($best['cpumodel'], 'Intel') !== false) {
+ $kcl = '-iommu -intel_iommu iommu=pt intel_iommu=on';
+ } elseif (strpos($best['cpumodel'], 'AMD') !== false) {
+ $kcl = '-iommu -amd_iommu iommu=pt amd_iommu=on';
+ } else {
+ error_log("Cannot determine CPU manufacturer from " . $best['cpumodel']);
+ $kcl = '-iommu -intel_iommu iommu=pt intel_iommu=on -amd_iommu amd_iommu=on';
+ }
+ }
+ if (!empty($passthrough)) {
+ foreach (array_keys($slots) as $slot) {
+ //error_log('Querying slot ' . $slot);
+ $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true);
+ $hw->addLocalColumn('slot')->addCondition('LIKE', $slot . '.%');
+ $hw->addGlobalColumn('vendor');
+ $hw->addGlobalColumn('device');
+ foreach ($hw->query() as $row) {
+ $passthrough[$row['vendor'] . ':' . $row['device']] = 1;
+ //error_log('Extra PT: ' . $row['vendor'] . ':' . $row['device']);
+ }
+ }
+ $kcl .= ' vfio-pci.ids=' . implode(',', array_keys($passthrough));
+ }
+ if ($gvt) {
+ $kcl .= ' i915.enable_gvt=1';
+ }
+ return $kcl;
+ }
+
+ // For lookup (from https://en.wikipedia.org/wiki/GUID_Partition_Table)
+ const GPT = [
+ '00000000-0000-0000-0000-000000000000' => 'Unused entry',
+ '024DEE41-33E7-11D3-9D69-0008C781F39F' => 'MBR partition scheme',
+ 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B' => 'EFI System partition',
+ '21686148-6449-6E6F-744E-656564454649' => 'BIOS boot partition',
+ 'D3BFE2DE-3DAF-11DF-BA40-E3A556D89593' => 'Intel Fast Flash (iFFS) partition (for Intel Rapid Start technology)',
+ 'F4019732-066E-4E12-8273-346C5641494F' => 'Sony boot partition',
+ 'BFBFAFE7-A34F-448A-9A5B-6213EB736C22' => 'Lenovo boot partition',
+ 'E3C9E316-0B5C-4DB8-817D-F92DF00215AE' => 'Microsoft Reserved Partition (MSR)',
+ 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7' => 'Microsoft Basic data partition',
+ '5808C8AA-7E8F-42E0-85D2-E1E90434CFB3' => 'Microsoft Logical Disk Manager (LDM) metadata partition',
+ 'AF9B60A0-1431-4F62-BC68-3311714A69AD' => 'Microsoft Logical Disk Manager data partition',
+ 'DE94BBA4-06D1-4D40-A16A-BFD50179D6AC' => 'Windows Recovery Environment',
+ '37AFFC90-EF7D-4E96-91C3-2D7AE055B174' => 'IBM General Parallel File System (GPFS) partition',
+ 'E75CAF8F-F680-4CEE-AFA3-B001E56EFC2D' => 'Storage Spaces partition',
+ '558D43C5-A1AC-43C0-AAC8-D1472B2923D1' => 'Storage Replica partition',
+ '75894C1E-3AEB-11D3-B7C1-7B03A0000000' => 'HPUX Data partition',
+ 'E2A1E728-32E3-11D6-A682-7B03A0000000' => 'HPUX Service partition',
+ '0FC63DAF-8483-4772-8E79-3D69D8477DE4' => 'Linux filesystem data',
+ 'A19D880F-05FC-4D3B-A006-743F0F84911E' => 'Linux RAID partition',
+ '44479540-F297-41B2-9AF7-D131D5F0458A' => 'Linux Root partition (x86)',
+ '4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709' => 'Linux Root partition (x86-64)',
+ '69DAD710-2CE4-4E3C-B16C-21A1D49ABED3' => 'Linux Root partition (32-bit ARM)',
+ 'B921B045-1DF0-41C3-AF44-4C6F280D3FAE' => 'Linux Root partition (64-bit ARM/AArch64)',
+ 'BC13C2FF-59E6-4262-A352-B275FD6F7172' => 'Linux /boot partition',
+ '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F' => 'Linux Swap partition',
+ 'E6D6D379-F507-44C2-A23C-238F2A3DF928' => 'Logical Volume Manager (LVM) partition',
+ '933AC7E1-2EB4-4F13-B844-0E14E2AEF915' => 'Linux /home partition',
+ '3B8F8425-20E0-4F3B-907F-1A25A76F98E8' => 'Linux /srv (server data) partition',
+ '7FFEC5C9-2D00-49B7-8941-3EA10A5586B7' => 'Linux Plain dm-crypt partition',
+ 'CA7D7CCB-63ED-4C53-861C-1742536059CC' => 'LUKS partition',
+ '8DA63339-0007-60C0-C436-083AC8230908' => 'Linux Reserved',
+ '83BD6B9D-7F41-11DC-BE0B-001560B84F0F' => 'FreeBSD Boot partition',
+ '516E7CB4-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD disklabel partition',
+ '516E7CB5-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD Swap partition',
+ '516E7CB6-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD Unix File System (UFS) partition',
+ '516E7CB8-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD Vinum volume manager partition',
+ '516E7CBA-6ECF-11D6-8FF8-00022D09712B' => 'FreeBSD ZFS partition',
+ '74BA7DD9-A689-11E1-BD04-00E081286ACF' => 'FreeBSD nandfs partition',
+ '48465300-0000-11AA-AA11-00306543ECAC' => 'Hierarchical File System Plus (HFS+) partition',
+ '7C3457EF-0000-11AA-AA11-00306543ECAC' => 'APFS FileVault volume container',
+ '55465300-0000-11AA-AA11-00306543ECAC' => 'Apple UFS container',
+ '52414944-0000-11AA-AA11-00306543ECAC' => 'Apple RAID partition',
+ '52414944-5F4F-11AA-AA11-00306543ECAC' => 'Apple RAID partition, offline',
+ '426F6F74-0000-11AA-AA11-00306543ECAC' => 'Apple Boot partition (Recovery HD)',
+ '4C616265-6C00-11AA-AA11-00306543ECAC' => 'Apple Label',
+ '5265636F-7665-11AA-AA11-00306543ECAC' => 'Apple TV Recovery partition',
+ '53746F72-6167-11AA-AA11-00306543ECAC' => 'HFS+ FileVault volume container',
+ '69646961-6700-11AA-AA11-00306543ECAC' => 'Apple APFS Preboot partition',
+ '52637672-7900-11AA-AA11-00306543ECAC' => 'Apple APFS Recovery partition',
+ '6A82CB45-1DD2-11B2-99A6-080020736631' => 'Solaris Boot partition',
+ '6A85CF4D-1DD2-11B2-99A6-080020736631' => 'Solaris Root partition',
+ '6A87C46F-1DD2-11B2-99A6-080020736631' => 'Solaris Swap partition',
+ '6A8B642B-1DD2-11B2-99A6-080020736631' => 'Solaris Backup partition',
+ '6A898CC3-1DD2-11B2-99A6-080020736631' => 'Solaris /usr partition',
+ '6A8EF2E9-1DD2-11B2-99A6-080020736631' => 'Solaris /var partition',
+ '6A90BA39-1DD2-11B2-99A6-080020736631' => 'Solaris /home partition',
+ '6A9283A5-1DD2-11B2-99A6-080020736631' => 'Solaris Alternate sector',
+ '6A945A3B-1DD2-11B2-99A6-080020736631' => 'Solaris Reserved partition',
+ '49F48D32-B10E-11DC-B99B-0019D1879648' => 'NetBSD Swap partition',
+ '49F48D5A-B10E-11DC-B99B-0019D1879648' => 'NetBSD FFS partition',
+ '49F48D82-B10E-11DC-B99B-0019D1879648' => 'NetBSD LFS partition',
+ '49F48DAA-B10E-11DC-B99B-0019D1879648' => 'NetBSD RAID partition',
+ '2DB519C4-B10F-11DC-B99B-0019D1879648' => 'NetBSD Concatenated partition',
+ '2DB519EC-B10F-11DC-B99B-0019D1879648' => 'NetBSD Encrypted partition',
+ 'FE3A2A5D-4F32-41A7-B725-ACCC3285A309' => 'Chrome OS kernel',
+ '3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC' => 'Chrome OS rootfs',
+ 'CAB6E88E-ABF3-4102-A07A-D4BB9BE3C1D3' => 'Chrome OS firmware',
+ '2E0A753D-9E48-43B0-8337-B15192CB1B5E' => 'Chrome OS future use',
+ '09845860-705F-4BB5-B16C-8A8A099CAF52' => 'Chrome OS miniOS',
+ '3F0F8318-F146-4E6B-8222-C28C8F02E0D5' => 'Chrome OS hibernate',
+ '5DFBF5F4-2848-4BAC-AA5E-0D9A20B745A6' => '/usr partition (coreos-usr)',
+ '3884DD41-8582-4404-B9A8-E9B84F2DF50E' => 'Resizable rootfs (coreos-resize)',
+ 'C95DC21A-DF0E-4340-8D7B-26CBFA9A03E0' => 'OEM customizations (coreos-reserved)',
+ 'BE9067B9-EA49-4F15-B4F6-F36F8C9E1818' => 'Root filesystem on RAID (coreos-root-raid)',
+ '42465331-3BA3-10F1-802A-4861696B7521' => 'Haiku BFS',
+ '85D5E45E-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Boot partition',
+ '85D5E45A-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Data partition',
+ '85D5E45B-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Swap partition',
+ '0394EF8B-237E-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Unix File System (UFS) partition',
+ '85D5E45C-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD Vinum volume manager partition',
+ '85D5E45D-237C-11E1-B4B3-E89A8F7FC3A7' => 'MidnightBSD ZFS partition',
+ '45B0969E-9B03-4F30-B4C6-B4B80CEFF106' => 'Cepth Journal',
+ '45B0969E-9B03-4F30-B4C6-5EC00CEFF106' => 'Cepth dm-crypt journal',
+ '4FBD7E29-9D25-41B8-AFD0-062C0CEFF05D' => 'Cepth OSD',
+ '4FBD7E29-9D25-41B8-AFD0-5EC00CEFF05D' => 'Cepth dm-crypt OSD',
+ '89C57F98-2FE5-4DC0-89C1-F3AD0CEFF2BE' => 'Cepth Disk in creation',
+ '89C57F98-2FE5-4DC0-89C1-5EC00CEFF2BE' => 'Cepth dm-crypt disk in creation',
+ 'CAFECAFE-9B03-4F30-B4C6-B4B80CEFF106' => 'Cepth Block',
+ '30CD0809-C2B2-499C-8879-2D6B78529876' => 'Cepth Block DB',
+ '5CE17FCE-4087-4169-B7FF-056CC58473F9' => 'Cepth Block write-ahead log',
+ 'FB3AABF9-D25F-47CC-BF5E-721D1816496B' => 'Cepth Lockbox for dm-crypt keys',
+ '4FBD7E29-8AE0-4982-BF9D-5A8D867AF560' => 'Cepth Multipath OSD',
+ '45B0969E-8AE0-4982-BF9D-5A8D867AF560' => 'Cepth Multipath journal',
+ 'CAFECAFE-8AE0-4982-BF9D-5A8D867AF560' => 'Cepth Multipath block',
+ '7F4A666A-16F3-47A2-8445-152EF4D03F6C' => 'Cepth Multipath block',
+ 'EC6D6385-E346-45DC-BE91-DA2A7C8B3261' => 'Cepth Multipath block DB',
+ '01B41E1B-002A-453C-9F17-88793989FF8F' => 'Cepth Multipath block write-ahead log',
+ 'CAFECAFE-9B03-4F30-B4C6-5EC00CEFF106' => 'Cepth dm-crypt block',
+ '93B0052D-02D9-4D8A-A43B-33A3EE4DFBC3' => 'Cepth dm-crypt block DB',
+ '306E8683-4FE2-4330-B7C0-00A917C16966' => 'Cepth dm-crypt block write-ahead log',
+ '45B0969E-9B03-4F30-B4C6-35865CEFF106' => 'Cepth dm-crypt LUKS journal',
+ 'CAFECAFE-9B03-4F30-B4C6-35865CEFF106' => 'Cepth dm-crypt LUKS block',
+ '166418DA-C469-4022-ADF4-B30AFD37F176' => 'Cepth dm-crypt LUKS block DB',
+ '86A32090-3647-40B9-BBBD-38D8C573AA86' => 'Cepth dm-crypt LUKS block write-ahead log',
+ '4FBD7E29-9D25-41B8-AFD0-35865CEFF05D' => 'Cepth dm-crypt LUKS OSD',
+ '824CC7A0-36A8-11E3-890A-952519AD3F61' => 'OpenBSD Data partition',
+ 'CEF5A9AD-73BC-4601-89F3-CDEEEEE321A1' => 'Power-safe (QNX6) file system',
+ 'C91818F9-8025-47AF-89D2-F030D7000C2C' => 'Plan 9 partition',
+ '9D275380-40AD-11DB-BF97-000C2911D1B8' => 'vmkcore (coredump partition)',
+ 'AA31E02A-400F-11DB-9590-000C2911D1B8' => 'VMFS filesystem partition',
+ '9198EFFC-31C0-11DB-8F78-000C2911D1B8' => 'VMware Reserved',
+ '2568845D-2332-4675-BC39-8FA5A4748D15' => 'Android-x86 Bootloader',
+ '114EAFFE-1552-4022-B26E-9B053604CF84' => 'Android-x86 Bootloader2',
+ '49A4D17F-93A3-45C1-A0DE-F50B2EBE2599' => 'Android-x86 Boot',
+ '4177C722-9E92-4AAB-8644-43502BFD5506' => 'Android-x86 Recovery',
+ 'EF32A33B-A409-486C-9141-9FFB711F6266' => 'Android-x86 Misc',
+ '20AC26BE-20B7-11E3-84C5-6CFDB94711E9' => 'Android-x86 Metadata',
+ '38F428E6-D326-425D-9140-6E0EA133647C' => 'Android-x86 System',
+ 'A893EF21-E428-470A-9E55-0668FD91A2D9' => 'Android-x86 Cache',
+ 'DC76DDA9-5AC1-491C-AF42-A82591580C0D' => 'Android-x86 Data',
+ 'EBC597D0-2053-4B15-8B64-E0AAC75F4DB1' => 'Android-x86 Persistent',
+ 'C5A0AEEC-13EA-11E5-A1B1-001E67CA0C3C' => 'Android-x86 Vendor',
+ 'BD59408B-4514-490D-BF12-9878D963F378' => 'Android-x86 Config',
+ '8F68CC74-C5E5-48DA-BE91-A0C8C15E9C80' => 'Android-x86 Factory',
+ '9FDAA6EF-4B3F-40D2-BA8D-BFF16BFB887B' => 'Android-x86 Factory (alt)',
+ '767941D0-2085-11E3-AD3B-6CFDB94711E9' => 'Android-x86 Fastboot / Tertiary',
+ 'AC6D7924-EB71-4DF8-B48D-E267B27148FF' => 'Android-x86 OEM',
+ '19A710A2-B3CA-11E4-B026-10604B889DCF' => 'Android Meta',
+ '193D1EA4-B3CA-11E4-B075-10604B889DCF' => 'Android EXT',
+ '7412F7D5-A156-4B13-81DC-867174929325' => 'ONIE Boot',
+ 'D4E6E2CD-4469-46F3-B5CB-1BFF57AFC149' => 'ONIE Config',
+ '9E1A2D38-C612-4316-AA26-8B49521E5A8B' => 'PReP boot',
+ '734E5AFE-F61A-11E6-BC64-92361F002671' => 'Atari TOS Basic data partition (GEM, BGM, F32)',
+ '8C8F8EFF-AC95-4770-814A-21994F2DBC8F' => 'VeraCrypt Encrypted data',
+ '90B6FF38-B98F-4358-A21F-48F35B4A8AD3' => 'ArcaOS Type 1',
+ '7C5222BD-8F5D-4087-9C00-BF9843C7B58C' => 'SPDK block device',
+ '4778ED65-BF42-45FA-9C5B-287A1DC4AAB1' => 'barebox-state',
+ '3DE21764-95BD-54BD-A5C3-4ABE786F38A8' => 'U-Boot environment',
+ 'B6FA30DA-92D2-4A9A-96F1-871EC6486200' => 'SoftRAID_Status',
+ '2E313465-19B9-463F-8126-8A7993773801' => 'SoftRAID_Scratch',
+ 'FA709C7E-65B1-4593-BFD5-E71D61DE9B02' => 'SoftRAID_Volume',
+ 'BBBA6DF5-F46F-4A89-8F59-8765B2727503' => 'SoftRAID_Cache',
+ 'FE8A2634-5E2E-46BA-99E3-3A192091A350' => 'Fuchsia Bootloader (slot A/B/R)',
+ 'D9FD4535-106C-4CEC-8D37-DFC020CA87CB' => 'Fuchsia Durable mutable encrypted system data',
+ 'A409E16B-78AA-4ACC-995C-302352621A41' => 'Fuchsia Durable mutable bootloader data (including A/B/R metadata)',
+ 'F95D940E-CABA-4578-9B93-BB6C90F29D3E' => 'Fuchsia Factory-provisioned read-only system data',
+ '10B8DBAA-D2BF-42A9-98C6-A7C5DB3701E7' => 'Fuchsia Factory-provisioned read-only bootloader data',
+ '49FD7CB8-DF15-4E73-B9D9-992070127F0F' => 'Fuchsia Volume Manager',
+ '421A8BFC-85D9-4D85-ACDA-B64EEC0133E9' => 'Fuchsia Verified boot metadata (slot A/B/R)',
+ '9B37FFF6-2E58-466A-983A-F7926D0B04E0' => 'Fuchsia Zircon boot image (slot A/B/R)',
+ ];
+
+}
diff --git a/modules-available/statistics/inc/hardwareparser.inc.php b/modules-available/statistics/inc/hardwareparser.inc.php
new file mode 100644
index 00000000..428f7d55
--- /dev/null
+++ b/modules-available/statistics/inc/hardwareparser.inc.php
@@ -0,0 +1,789 @@
+<?php
+
+class HardwareParser
+{
+
+ const SIZE_LOOKUP = ['T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024, '' => 1];
+ const SI_LOOKUP = ['T' => 1000000000000, 'G' => 1000000000, 'M' => 1000000, 'K' => 1000, '' => 1];
+
+ /**
+ * Convert/format size unit. Input string can be a size like
+ * 8 GB or 1024 MB and will be converted according to passed parameters.
+ * @param string $string Input string
+ * @param string $scale 'a' for auto, T/G/M/K/'' for according units
+ * @param bool $appendUnit append unit string, e.g. 'GiB'
+ * @return false|string|int Formatted result
+ */
+ public static function convertSize(string $string, string $scale = 'a', bool $appendUnit = true)
+ {
+ if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out)) {
+ //error_log("Not size: $string");
+ return false;
+ }
+ $val = (int)$out[1] * self::SIZE_LOOKUP[strtoupper($out[2])];
+ if (!array_key_exists($scale, self::SIZE_LOOKUP)) {
+ foreach (self::SIZE_LOOKUP as $k => $v) {
+ if ($k === '' || $val / 8 >= $v || abs($val - $v) < 50) {
+ $scale = $k;
+ break;
+ }
+ }
+ }
+ $val = (int)round($val / self::SIZE_LOOKUP[$scale]);
+ if ($appendUnit) {
+ $val .= ' ' . ($scale === '' ? 'Byte' : $scale . 'iB'); // NBSP!!
+ }
+ return $val;
+ }
+
+ /**
+ * Decode JEDEC ID to according manufacturer
+ */
+ public static function decodeJedec(string $string): string
+ {
+ // JEDEC ID:7F 7F 9E 00 00 00 00 00
+ // or the ID as 8 hex digits with no spacing and prefix
+ $id = null;
+ if (preg_match('/JEDEC(?:\s*ID)?\s*:?\s*([0-9a-f\s]{8,23})\s*$/i', $string, $out)
+ || preg_match('/^([0-9a-f]{14}00)$/i', $string, $out)) {
+ preg_match_all('/[0-9a-f]{2}/i', $out[1], $out);
+ $bank = 0;
+ foreach ($out[0] as $id) {
+ $bank++;
+ $id = hexdec($id) & 0x7f; // Let's just ignore the parity bit, and any potential error
+ if ($id !== 0x7f)
+ break;
+ }
+ if ($id !== null) {
+ $id = self::lookupJedec($bank, $id);
+ }
+ } elseif (preg_match('/Unknown.{0,16}[\[(](?:0x)?([0-9a-fA-F]{2,4})[\])]/', $string, $out)) {
+ // First byte (big endian) is id-in-bank, low byte is bank
+ $id = self::decodeBankAndId($out, false);
+ } elseif (preg_match('/JEDEC(?:\s*ID)?\s*:?\s*([0-9a-f]{2}\s?[0-9a-f]{2})/i', $string, $out)
+ || (preg_match('/^([0-9A-F]{4})([0-9A-F]{4})([0-9A-F]{4})$/', $string, $out) && $out[2] === '0000')) {
+ // First byte is bank, second byte is id-in-bank
+ $id = self::decodeBankAndId($out, true);
+ } elseif (preg_match('/^([0-9a-f]{4})$/i', $string, $out)) {
+ // This one was seen with both endianesses
+ $id = self::decodeBankAndId($out, true);
+ if ($id === null) {
+ $id = self::decodeBankAndId($out, false);
+ }
+ }
+
+ if ($id !== null)
+ return $id;
+ return $string;
+ }
+
+ private static function decodeBankAndId(array $out, bool $bankFirst): ?string
+ {
+ // 16bit encoding from DDR3+: lower byte is number of 0x7f bytes, upper byte is id within bank
+ $id = hexdec(str_replace(' ', '', $out[1]));
+ // Our bank counting starts at one. Also ignore parity bit.
+ $bank = ($id & 0x7f);
+ // Shift down id, get rid of parity bit
+ $id = ($id >> 8) & 0x7f;
+ if ($bankFirst) {
+ // Observed second case, on OptiPlex 5050, is 80AD000080AD, but here endianness is reversed
+ $tmp = $id;
+ $id = $bank;
+ $bank = $tmp;
+ }
+ $bank++;
+ return self::lookupJedec($bank, $id);
+ }
+
+ private static function lookupJedec(int $bank, int $id): ?string
+ {
+ static $data = false;
+ if ($data === false) {
+ $data = json_decode(file_get_contents(dirname(__FILE__) . '/jedec.json'), true);
+ }
+ if (array_key_exists('bank' . $bank, $data) && array_key_exists('id' . $id, $data['bank' . $bank]))
+ return $data['bank' . $bank]['id' . $id];
+ return null;
+ }
+
+ /**
+ * Turn several numeric measurements like Size, Speed, Voltage into a unitless
+ * base representation, meant for comparison. For example, Voltages are converted
+ * to Millivolts, Anything measured in [KMGT]Bytes (per second) to bytes, GHz to
+ * Hz, and so on.
+ * @return ?int value, or null if not numeric
+ */
+ private static function toNumeric(string $key, string $val): ?int
+ {
+ $key = strtolower($key);
+ // Normalize voltage to mV
+ if ((strpos($key, 'volt') !== false || strpos($key, 'current') !== false)
+ && preg_match('/^([0-9]+(?:\.[0-9]+)?)\s+(m?)V/', $val, $out)) {
+ return (int)($out[1] * ($out[2] === 'm' ? 1 : 1000));
+ }
+ if (preg_match('/speed|width|size|capacity/', $key)
+ && preg_match('#^([0-9]+(?:\.[0-9]+)?)\s*([TGMK]?)i?([BT](?:it|yte|))s?(?:/s)?#i',
+ $val, $out)) {
+ // Matched (T/G/M) Bits, Bytes, etc...
+ // For bits, use SI
+ if ($out[3] !== 'B' && strtolower($out[3]) !== 'byte')
+ return (int)($out[1] * self::SI_LOOKUP[strtoupper($out[2])]);
+ // For bytes, use 1024
+ return (int)($out[1] * self::SIZE_LOOKUP[strtoupper($out[2])]);
+ }
+ // Speed in Hz
+ if (preg_match('#^([0-9]+(?:\.[0-9]+)?)\s*([TGMK]?)Hz#i',
+ $val, $out)) {
+ return (int)($out[1] * self::SI_LOOKUP[strtoupper($out[2])]);
+ }
+ // Count, size (unitless)
+ if (is_numeric($val) && preg_match('/^-?[0-9]+$/', $val)
+ && preg_match('/used|occupied|count|number|speed|width|size|capacity|temperature|_start|_value|_thresh|_worst|_time|_rate/', $key)) {
+ return (int)$val;
+ }
+ // Date
+ if (preg_match('#^(?:[0-9]{2}/[0-9]{2}/[0-9]{4}|[0-9]{4}-[0-9]{2}-[0-9]{2})$#', $val)) {
+ return (int)strtotime($val);
+ }
+ return null;
+ }
+
+ /**
+ * Takes hwinfo json, then looks up and returns all sections from the
+ * dmidecode subtree that represent the given dmi table entry type,
+ * e.g. 17 for memory. It will then return an array of 'props' subtrees.
+ *
+ * @param array $data hwinfo tree
+ * @param int $type dmi type
+ * @return array [ <props>, <props>, ... ]
+ */
+ public static function getDmiHandles(array $data, int $type): array
+ {
+ if (empty($data['dmidecode']))
+ return [];
+ $ret = [];
+ foreach ($data['dmidecode'] as $section) {
+ if ($section['handle']['type'] == $type) {
+ $ret[] = $section['props'];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Takes key-value-array, returns a concatenated string of all the values with the keys given in $fields.
+ * The items are separated by spaces, and returned in the order they were given in $fields. Missing keys
+ * are silently omitted.
+ */
+ private static function idFromArray(array $array, string ...$fields): string
+ {
+ $out = '';
+ foreach ($fields as $field) {
+ if (!isset($array[$field]))
+ continue;
+ if (empty($out)) {
+ $out = $array[$field];
+ } else {
+ $out .= ' ' . $array[$field];
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Establish a mapping between a client and some hardware device.
+ * Optionally writes hardware properties specific to a hardware instance of a client
+ *
+ * @param string $uuid client
+ * @param int $hwid hw global hw id
+ * @param string $pathId unique identifier for the local instance of this hw, e.q. PCI slot, /dev path, something that handles the case that there are multiple instances of the same hardware in one machine
+ * @param array $props KV-pairs of properties to write for this instance; can be empty
+ * @return int ID of mapping in DB
+ */
+ private static function writeLocalHardwareData(string $uuid, int $hwid, string $pathId, array $props): int
+ {
+ // Add mapping between hw entity and machine
+ $pathId = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $pathId);
+ $mappingId = Database::insertIgnore('machine_x_hw', 'machinehwid',
+ ['hwid' => $hwid, 'machineuuid' => $uuid, 'devpath' => $pathId],
+ ['disconnecttime' => 0]);
+ // And all the properties specific to this entity instance (e.g. serial number)
+ if (!empty($props)) {
+ $vals = [];
+ foreach ($props as $k => $v) {
+ $vals[] = [$mappingId, $k, $v, self::toNumeric($k, $v)];
+ }
+ Database::exec("INSERT INTO machine_x_hw_prop (machinehwid, prop, `value`, `numeric`)
+ VALUES :vals
+ ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `numeric` = VALUES(`numeric`)", ['vals' => $vals]);
+ }
+ return $mappingId;
+ }
+
+ /**
+ * Takes an array of type [ key1 => [ 'values' => [ <val1.1>, <val1.2>, ... ] ], key2 => ... ]
+ * and turns it into [ key1 => <val1.1>, key2 => <val2.1>, ... ]
+ *
+ * Along the way:
+ * 1) any fields with bogus values, or values analogous to empty will get removed
+ */
+ public static function prepareDmiProperties(array $data): array
+ {
+ $ret = [];
+ foreach ($data as $key => $vals) {
+ $val = trim($vals['values'][0] ?? 'NULL');
+ if ($val === '[Empty]' || $val === 'NULL')
+ continue;
+ $val = preg_replace('/[^a-z0-9]/', '', strtolower($val));
+ if ($val === '' || $val === 'notspecified' || $val === 'tobefilledbyoem' || $val === 'unknown'
+ || $val === 'chassismanufacture' || $val === 'chassismanufacturer' || $val === 'chassisversion'
+ || $val === 'chassisserialnumber' || $val === 'defaultstring' || $val === 'productname'
+ || $val === 'manufacturer' || $val === 'systemmodel' || $val === 'fillbyoem' || $val === 'none') {
+ continue;
+ }
+ $val = trim($vals['values'][0] ?? '');
+ if ($key === 'Manufacturer') {
+ $val = self::fixManufacturer($val);
+ }
+ $ret[$key] = $val;
+ }
+ return $ret;
+ }
+
+ /**
+ * Mark all devices of a given type disconnected from the given machine, with an optional
+ * exclude list of machine-client-mapping IDs
+ *
+ * @param string $uuid client
+ * @param string $dbType type, eg HDD
+ * @param array $excludedHwIds mappingIDs to exclude, ie. devices that are still connected
+ */
+ private static function markDisconnected(string $uuid, string $dbType, array $excludedHwIds)
+ {
+ //error_log("Marking disconnected for $dbType except " . implode(', ', $excludedHwIds));
+ if (empty($excludedHwIds)) {
+ Database::exec("UPDATE machine_x_hw mxh, statistic_hw h
+ SET mxh.disconnecttime = UNIX_TIMESTAMP()
+ WHERE h.hwtype = :type AND h.hwid = mxh.hwid AND mxh.machineuuid = :uuid
+ AND mxh.disconnecttime = 0",
+ ['type' => $dbType, 'uuid' => $uuid]);
+ } else {
+ Database::exec("UPDATE machine_x_hw mxh, statistic_hw h
+ SET mxh.disconnecttime = UNIX_TIMESTAMP()
+ WHERE h.hwtype = :type AND h.hwid = mxh.hwid AND mxh.machineuuid = :uuid
+ AND mxh.disconnecttime = 0 AND mxh.machinehwid NOT IN (:hwids)",
+ ['type' => $dbType, 'uuid' => $uuid, 'hwids' => $excludedHwIds]);
+ }
+ }
+
+ /**
+ * Insert some hardware into database. $global is supposed to contain key-value-pairs of properties
+ * this hardware has that is the same for every instance of this hardware, like model number, speed
+ * or size. Individual properties, like a serial number, are considered local properties, and go
+ * into a different table, that would contain a row for each client that has this hardware.
+ * @param string $dbType Hardware typ (HDD, RAM, ...)
+ * @param array $global associative array of properties this hardware has
+ * @return int id of this hardware; primary key of row in statistic_hw_prop
+ */
+ private static function writeGlobalHardwareData(string $dbType, array $global, array $globalExtra = []): int
+ {
+ static $cache = [];
+ // Since the global properties are supposed to be unique for a specific piece of hardware, use them all
+ // to generate a combined ID for this hardware entity, as opposed to $localProps, which should differ
+ // between instances of the same hardware entity, e.g. one specific HDD model has different serial numbers.
+ $id = md5(implode(' ', $global));
+ // But don't include our "fake" fields in this as we might add more there later, which would
+ // change the ID then.
+ $global += $globalExtra;
+ if (!isset($cache[$id])) {
+ // Cache lookup, make sure we insert this only once for every run, as this is supposed to be general
+ // information about the hardware, e.g. model number, max. resolution, capacity, ...
+ $hwid = Database::insertIgnore('statistic_hw', 'hwid', ['hwtype' => $dbType, 'hwname' => $id]);
+ $vals = [];
+ foreach ($global as $k => $v) {
+ $vals[] = [$hwid, $k, $v, self::toNumeric($k, $v)];
+ }
+ if (!empty($vals)) {
+ Database::exec("INSERT INTO statistic_hw_prop (hwid, prop, `value`, `numeric`)
+ VALUES :vals
+ ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `numeric` = VALUES(`numeric`)",
+ ['vals' => $vals]);
+ }
+ $cache[$id] = $hwid;
+ }
+ return $cache[$id];
+ }
+
+ /**
+ * Process hardware info for given client.
+ * @param string $uuid System-UUID of client
+ * @param array $data Hardware info, deserialized assoc array.
+ * @return ?array id44mb and id45mb as calculated from given HDD data
+ */
+ public static function parseMachine(string $uuid, array $data): ?array
+ {
+ $version = $data['version'] ?? 0;
+ if ($version != 2) {
+ error_log("Received unsupported hw json v$version");
+ return null;
+ }
+ // determine misc stuff first
+ $globalCpuExtra = [];
+ $globalMainboardExtra = [];
+ $localMainboardExtra = [];
+ // physical memory array
+ $memArrays = self::getDmiHandles($data, 16);
+ // We mostly have a seprate hardware type for all the dmi types, but not for memory arrays.
+ // These get added to the mainboard hw-type as it's practically a property of the mainboard.
+ // While we can have multiple physical memory arrays, we only ever have one mainboard per
+ // client. Add up the data from all arrays.
+ $globalMainboardExtra['Memory Slot Count'] = 0;
+ $globalMainboardExtra['Memory Maximum Capacity'] = 0;
+ foreach ($memArrays as $mem) {
+ $mem = self::prepareDmiProperties($mem);
+ // Not all memory arrays are for RAM....
+ if (($mem['Use'] ?? 0) !== 'System Memory')
+ continue;
+ if (isset($mem['Number Of Devices'])) {
+ $globalMainboardExtra['Memory Slot Count'] += $mem['Number Of Devices'];
+ }
+ if (isset($mem['Maximum Capacity'])) {
+ // Temporary unit is MB
+ $globalMainboardExtra['Memory Maximum Capacity']
+ += self::convertSize($mem['Maximum Capacity'], 'M', false);
+ }
+ }
+ // Now finally convert to GB
+ $globalMainboardExtra['Memory Maximum Capacity']
+ = self::convertSize($globalMainboardExtra['Memory Maximum Capacity'] . ' MB', 'G');
+ // BIOS section - need to combine this with mainboard or system model, as it doesn't have a meaningful
+ // identifier on its own. So again like above, we add this to the mainboard data.
+ $bios = self::prepareDmiProperties(self::getDmiHandles($data, 0)[0] ?? []);
+ foreach (['Version', 'Release Date', 'Firmware Revision'] as $k) {
+ if (isset($bios[$k])) {
+ // Prefix with "BIOS" to clarify, since it's added to the mainboard meta-data
+ $localMainboardExtra['BIOS ' . $k] = $bios[$k];
+ }
+ }
+ if (isset($bios['BIOS Revision'])) { // This one already has the BIOS prefix
+ $localMainboardExtra['BIOS Revision'] = $bios['BIOS Revision'];
+ }
+ // Vendor and ROM size of BIOS *should* always be the same for a specific mainboard
+ foreach (['Vendor', 'ROM Size'] as $k) {
+ if (isset($bios[$k])) {
+ $globalMainboardExtra['BIOS ' . $k] = $bios[$k];
+ }
+ }
+ // "Normal" dmi entries - these map directly to one of our hardware types
+ // RAM modules
+ $capa = 0;
+ $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, HardwareInfo::RAM_MODULE,
+ // Filter callback - we can modify the entry, or return false to ignore it
+ function (array $flat) use (&$capa): bool {
+ $size = self::convertSize(($flat['Size'] ?? 0), '', false);
+ // Let's assume we're never running on old HW with <=128MB modules, so this
+ // might be a hint that this is some other kind of memory. The proper way would be
+ // to check if the related physical memory array (16) has "Use" = "System Memory"
+ if ($size > 129 * 1024 * 1024) {
+ $capa += $size;
+ return true;
+ }
+ return false;
+ },
+ ['Locator'],
+ ['Data Width',
+ 'Size',
+ 'Form Factor',
+ 'Type',
+ 'Type Detail',
+ 'Speed',
+ 'Manufacturer',
+ 'Part Number',
+ 'Minimum Voltage',
+ 'Maximum Voltage'],
+ ['Locator', 'Bank Locator', 'Serial Number', 'Asset Tag', 'Configured Memory Speed', 'Configured Voltage']
+ );
+ // Put RAM slots used/total etc. into mainboard data
+ $localMainboardExtra['Memory Slot Occupied'] = $ramModCount;
+ $localMainboardExtra['Memory Installed Capacity'] = self::convertSize($capa, 'G', true);
+ // Also add generic socket, core and thread count to mainboard data. This doesn't seem to make too much sense
+ // at first since it's not a property of the mainboard. But we can get away with it since we make it a local
+ // property, i.e. specific to a client. This is just aggregated, so it's not super well suited for the CPU
+ // hardware type, referenced below. In fact, on some systems the dmi/smbios tables don't contain all that much
+ // information about the CPU at all, so we have at least this.
+ foreach (['sockets', 'cores', 'threads'] as $key) {
+ if (!isset($data['cpu'][$key]))
+ continue;
+ $localMainboardExtra['cpu-' . $key] = $data['cpu'][$key];
+ }
+ if ($data['cpu']['vmx-legacy'] ?? false) {
+ $globalCpuExtra['vmx-legacy'] = 1;
+ }
+ // Do the same hack with the primary NIC's speed and duplex. Even if it's not an onboard NIC, we only have one
+ // primary boot interface
+ $bootNic = $data['net']['boot0'] ?? $data['net']['eth0'] ?? null;
+ if ($bootNic !== null) {
+ $localMainboardExtra['nic-speed'] = $bootNic['speed'] ?? 0;
+ $localMainboardExtra['nic-duplex'] = $bootNic['duplex'] ?? 'unknown';
+ }
+ // Finally handle mainbard data, with all of our gathered extra fields
+ self::updateHwTypeFromDmi($uuid, $data, 2, HardwareInfo::MAINBOARD, [],
+ [],
+ ['Manufacturer', 'Product Name', 'Type', 'Version'], // Global props, don't change
+ ['Serial Number', 'Asset Tag', 'Location In Chassis'],
+ $globalMainboardExtra, $localMainboardExtra
+ );
+ // System information, mostly set by OEMs, might be empty/bogus on custom systems
+ self::updateHwTypeFromDmi($uuid, $data, 1, HardwareInfo::DMI_SYSTEM, ['Manufacturer', 'Product Name'],
+ [],
+ ['Manufacturer', 'Product Name', 'Version', 'Wake-up Type'], // Global props, don't change
+ ['Serial Number', 'UUID', 'SKU Number']
+ );
+ // Might contain more or less accurate information. Mostly works on servers and OEM systems
+ self::updateHwTypeFromDmi($uuid, $data, 39, HardwareInfo::POWER_SUPPLY, ['Manufacturer'],
+ ['Location',
+ 'Power Unit Group',
+ 'Name'], // Location might be empty/"Unknown", but Name can be something like "PSU 2"
+ ['Manufacturer', 'Product Name', 'Model Part Number', 'Revision', 'Max Power Capacity'], // Global props, don't change
+ ['Serial Number', 'Asset Tag', 'Status', 'Plugged', 'Hot Replaceable']
+ );
+ // On some more recent systems this contains quite some useful information
+ self::updateHwTypeFromDmi($uuid, $data, 4, HardwareInfo::CPU, ['Version'],
+ ['Socket Designation'],
+ ['Type', 'Family', 'Manufacturer', 'Signature', 'Version', 'Core Count', 'Thread Count'], // Global props, don't change
+ ['Voltage', 'Current Speed', 'Upgrade', 'Core Enabled'],
+ $globalCpuExtra);
+ // Information about system slots
+ self::updateHwTypeFromDmi($uuid, $data, 9, HardwareInfo::SYSTEM_SLOT,
+ function (array &$entry): bool {
+ // Use a callback filter to extract PCIe slot metadata into unique fields
+ if (!isset($entry['Type']))
+ return false;
+ // Split up PCIe info – gen, electrical width and physical width are mashed into one field
+ if (preg_match('/^x(?<b>\d+) PCI Express( (?<g>\d+)( x(?<s>\d+))?)?$/', $entry['Type'], $out)) {
+ $entry['Type'] = 'PCI Express';
+ $entry['PCIe Bus Width'] = $out['b'];
+ if (!empty($out['g'])) {
+ $entry['PCIe Gen'] = $out['g'];
+ }
+ if (!empty($out['s'])) {
+ $entry['PCIe Slot Width'] = $out['s'];
+ }
+ }
+ return true;
+ },
+ ['Designation', 'ID', 'Bus Address'],
+ ['Type', 'PCIe Bus Width', 'PCIe Gen', 'PCIe Slot Width'], // Global props, don't change
+ ['Current Usage', 'Designation']
+ );
+ // dmidecode end
+ // ---- lspci ------------------------------------
+ $pciHwIds = [];
+ foreach (($data['lspci'] ?? []) as $dev) {
+ // $props is global props, don't change or the ID will change
+ $props = self::propsFromArray($dev, 'vendor', 'device', 'rev', 'class');
+ if (!isset($props['vendor']) || !isset($props['device']))
+ continue;
+ $hwid = self::writeGlobalHardwareData(HardwareInfo::PCI_DEVICE, $props);
+ $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['slot'] ?? 'unknown',
+ self::propsFromArray($dev, 'slot', 'subsystem', 'subsystem_vendor', 'iommu_group'));
+ $pciHwIds[] = $mappingId;
+ }
+ self::markDisconnected($uuid, HardwareInfo::PCI_DEVICE, $pciHwIds);
+ // ---- Disks ------------------------------------
+ $excludedHddHwIds = [];
+ // Sum of all ID44/45 partitions in bytes
+ $id44 = $id45 = 0;
+ foreach (($data['drives'] ?? []) as $dev) {
+ if (($dev['type'] ?? 'drive') !== 'drive')
+ continue; // TODO: Handle CD/DVD drives? Still relevant?
+ if (empty($dev['readlink'])) // This is the canonical entry name directly under /dev/, e.g. /dev/sda
+ continue;
+ // Use smartctl as the source of truth, lsblk as fallback if data is missing
+ if (!isset($dev['smartctl']) || !is_array($dev['smartctl'])) {
+ $smart = [];
+ } else {
+ $smart =& $dev['smartctl'];
+ }
+ if (!isset($dev['lsblk']['blockdevices'][0]) || !is_array($dev['lsblk']['blockdevices'][0])) {
+ $lsblk = [];
+ } else {
+ $lsblk =& $dev['lsblk']['blockdevices'][0];
+ }
+ if (!isset($smart['rotation_rate']) && isset($lsblk['rota']) && !$lsblk['rota']) {
+ // smartctl didn't report on it, lsblk says it's non-rotational
+ $smart['rotation_rate'] = 0;
+ }
+ $size = $lsblk['size'] ?? $smart['user_capacity']['bytes'] ?? -1;
+ // Don't change the global props, it would change the HW ID
+ $hwid = self::writeGlobalHardwareData(HardwareInfo::HDD, [
+ // Try to use the model name as the unique identifier
+ 'model' => $smart['model_name'] ?? $lsblk['model'] ?? 'unknown',
+ // Append device size as some kind of fallback, in case model is unknown
+ 'size' => $size,
+ 'physical_block_size' => $smart['physical_block_size'] ?? $lsblk['phy-sec'] ?? 0,
+ 'logical_block_size' => $smart['logical_block_size'] ?? $lsblk['log-sec'] ?? 0,
+ ] + self::propsFromArray($smart, 'rotation_rate', 'sata_version//string',
+ 'interface_speed//max//string', 'model_family'));
+ // Mangle smart attribute table
+ // TODO: Handle used endurance indicator for (SATA) SSDs
+ $table = [];
+ foreach (($smart['ata_smart_attributes']['table'] ?? []) as $attr) {
+ if (!isset($attr['id']))
+ continue;
+ $id = 'attr_' . $attr['id'] . '_';
+ foreach (['value', 'worst', 'thresh', 'when_failed'] as $item) {
+ if (isset($attr[$item])) {
+ $table[$id . $item] = $attr[$item];
+ }
+ }
+ if (isset($attr['raw']['value'])) {
+ if ($attr['id'] === 194) {
+ if (!isset($smart['temperature'])) {
+ $smart['temperature'] = [];
+ }
+ if (!isset($smart['temperature']['current'])) {
+ $smart['temperature']['current'] = $attr['raw']['value'] & 0xffff;
+ }
+ $smart['temperature']['min'] = ($attr['raw']['value'] >> 16) & 0xffff;
+ $smart['temperature']['max'] = ($attr['raw']['value'] >> 32) & 0xffff;
+ }
+ $table[$id . 'raw'] = $attr['raw']['value'];
+ }
+ }
+ if (isset($smart['nvme_smart_health_information_log'])
+ && is_array($smart['nvme_smart_health_information_log'])) {
+ $table += array_filter($smart['nvme_smart_health_information_log'], function ($v, $k) {
+ return !is_array($v) && $k !== 'temperature' && $k !== 'temperature_sensors';
+ }, ARRAY_FILTER_USE_BOTH);
+ }
+ // Partitions
+ $used = 0;
+ if (isset($dev['sfdisk']['partitiontable'])) {
+ $table['partition_table'] = $dev['sfdisk']['partitiontable']['label'] ?? 'none';
+ switch ($dev['sfdisk']['partitiontable']['unit'] ?? 'sectors') {
+ case 'sectors':
+ $fac = 512;
+ break;
+ case 'bytes':
+ $fac = 1;
+ break;
+ default:
+ $fac = 0;
+ }
+ $i = 0;
+ foreach (($dev['sfdisk']['partitiontable']['partitions'] ?? []) as $part) {
+ if (!isset($part['size']))
+ continue;
+ if ($table['partition_table'] === 'dos') {
+ $type = hexdec($part['type'] ?? '0');
+ if ($type === 0x0 || $type === 0x5 || $type === 0xf || $type === 0x15 || $type === 0x1f
+ || $type === 0x85 || $type === 0xc5 || $type == 0xcf) {
+ // Extended partition, ignore
+ continue;
+ }
+ }
+ $used += $part['size'] * $fac;
+ if (isset($part['node']) && preg_match('/-part(\d+)$/', $part['node'], $out)) {
+ $id = 'part_' . $out[1] . '_';
+ } else {
+ $id = 'part_' . ($i + 1) . '_';
+ }
+ foreach (['start', 'size', 'type', 'uuid', 'name'] as $item) {
+ if (!isset($part[$item]))
+ continue;
+ if ($item === 'size' || $item === 'start') {
+ // Turn size and start into byte offsets
+ $table[$id . $item] = $part[$item] * $fac;
+ } else {
+ $table[$id . $item] = $part[$item];
+ }
+ }
+ $type = $table[$id . 'type'] ?? 0;
+ $name = $table[$id . 'name'] ?? '';
+ if ($type == '44' || strtolower($type) === '87f86132-ff94-4987-b250-444444444444'
+ || $name === 'OpenSLX-ID44') {
+ $table[$id . 'slxtype'] = '44';
+ $id44 += $part['size'] * $fac;
+ } elseif ($type == '45' || strtolower($type) === '87f86132-ff94-4987-b250-454545454545'
+ || $name === 'OpenSLX-ID45') {
+ $table[$id . 'slxtype'] = '45';
+ $id45 += $part['size'] * $fac;
+ }
+ //
+ ++$i;
+ }
+ }
+ $table['unused'] = $size - $used;
+ $table['dev'] = $dev['readlink'];
+ $table += self::propsFromArray($smart + $lsblk,
+ 'serial_number', 'firmware_version',
+ 'interface_speed//current//string',
+ 'smart_status//passed', 'temperature//current', 'temperature//min', 'temperature//max',
+ 'power_on_time//hours');
+ $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['readlink'],
+ $table);
+ // Delete old partition and smart attribute entries
+ Database::exec("DELETE FROM machine_x_hw_prop WHERE machinehwid = :id AND prop NOT IN (:keep)
+ AND prop NOT LIKE '@%'", [
+ 'id' => $mappingId,
+ 'keep' => array_keys($table),
+ ]);
+ $excludedHddHwIds[] = $mappingId;
+ unset($smart, $lsblk);
+ } // End loop over disks
+ self::markDisconnected($uuid, HardwareInfo::HDD, $excludedHddHwIds);
+ //
+ // Mark parse date
+ $params = [
+ 'uuid' => $uuid,
+ 'id44mb' => round($id44 / (1024 * 1024)),
+ 'id45mb' => round($id45 / (1024 * 1024)),
+ ];
+ Database::exec("UPDATE machine SET dataparsetime = UNIX_TIMESTAMP(), id44mb = :id44mb, id45mb = :id45mb
+ WHERE machineuuid = :uuid", $params);
+ return $params;
+ }
+
+ /**
+ * Unify different variants of manufacturer names
+ */
+ private static function fixManufacturer(string $in): string
+ {
+ $in = self::decodeJedec($in);
+ switch (strtolower($in)) {
+ case 'advanced micro devices, inc.':
+ case 'advanced micro devices':
+ case 'authenticamd':
+ return 'AMD';
+ case 'apple inc.':
+ return 'Apple';
+ case 'asustek computer inc.':
+ return 'ASUS';
+ case 'crucial';
+ case 'crucial technology':
+ return 'Crucial';
+ case 'dell inc.':
+ return 'Dell';
+ case 'fujitsu':
+ case 'fujitsu client computing limited':
+ return 'Fujitsu';
+ case 'hewlett packard':
+ case 'hewlett-packard':
+ return 'HP';
+ case 'genuineintel':
+ case 'intel corporation':
+ case 'intel(r) corp.':
+ case 'intel(r) corporation':
+ return 'Intel';
+ case 'micron technology':
+ return 'Micron';
+ case 'ramaxel technology':
+ return 'Ramaxel';
+ case 'samsung sdi':
+ return 'Samsung';
+ case 'hynix semiconduc':
+ case 'hynix/hyundai':
+ case 'hyundai electronics hynix semiconductor inc':
+ case 'hynix semiconductor inc sk hynix':
+ return 'SK Hynix';
+ }
+ return $in;
+ }
+
+ /**
+ * Takes key-value-array, returns a new array with only the keys listed in $fields.
+ * Checks if the given key is not an array. If it's an array, it will be ignored.
+ * Supports nested arrays. Nested keys are separated by '//', so to query
+ * $array['x']['y'], add 'x//y' to $fields. The value will be added to the return
+ * value as key 'x//y'.
+ */
+ private static function propsFromArray(array $array, string ...$fields): array
+ {
+ $ret = [];
+ foreach ($fields as $field) {
+ if (strpos($field, '//') === false) {
+ if (isset($array[$field]) && !is_array($array[$field])) {
+ $ret[$field] = $array[$field];
+ }
+ } else {
+ $parts = explode('//', $field);
+ $elem = $array;
+ foreach ($parts as $part) {
+ if (isset($elem[$part])) {
+ $elem = $elem[$part];
+ } else {
+ $elem = false;
+ break;
+ }
+ }
+ if ($elem !== false && !is_array($elem)) {
+ $ret[preg_replace('~//(value|string)$~', '', $field)] = $elem;
+ }
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Extract data from dmi/smbios and write to DB.
+ * This is a pretty involved function that does several things, among them is splitting
+ * up the data by global and local properties, create new hardware entry if new, and
+ * making sure other hardware of same type gets marked as disconnected from given client.
+ * @param string $uuid client uuid
+ * @param array $data dmidecode part of hardware info
+ * @param int $type dmi type to extract from $data
+ * @param string $dbType hardware type to write this to DB as
+ * @param array|callable $requiredPropsOrCallback either a list of properties that are
+ * mandatory for this hwtype, or a callback function that returns true/false for
+ * valid/invalid dmi entries
+ * @param array $pathFields fields from entry that define the path or location of the
+ * hardware in the client
+ * @param array $globalProps properties of entry that are considered the same for all
+ * instances of that hardware, e.g. model name
+ * @param array $localProps properties of entry that are considered different for each
+ * instance of this hardware per client, e.g. serial number, power-on hours
+ * @param array $globalExtra additional key-value-pairs to write to DB as being global
+ * @param array $localExtra additional key-value-pairs to write to DB as being local
+ * @return int number of table entries written to DB, i.e. passed $requiredPropsOrCallback
+ */
+ private static function updateHwTypeFromDmi(
+ string $uuid, array $data, int $type, string $dbType,
+ $requiredPropsOrCallback, array $pathFields, array $globalProps, array $localProps,
+ array $globalExtra = [], array $localExtra = []
+ ): int
+ {
+ $sections = self::getDmiHandles($data, $type);
+ if (empty($sections) && !empty($globalExtra) || !empty($localExtra)) {
+ // Section not found, but as we want to store additional artificial columms,
+ // just create one empty fake section so the loop will be executed
+ $sections = [[]];
+ }
+ $thisMachineHwIds = [];
+ foreach ($sections as $section) {
+ $flat = self::prepareDmiProperties($section);
+ if (is_array($requiredPropsOrCallback)) {
+ foreach ($requiredPropsOrCallback as $prop) {
+ if (!isset($flat[$prop]))
+ continue 2;
+ }
+ }
+ if (is_callable($requiredPropsOrCallback)) {
+ if (!$requiredPropsOrCallback($flat))
+ continue;
+ }
+ // Global
+ $props = self::propsFromArray($flat, ...$globalProps);
+ $hwid = self::writeGlobalHardwareData($dbType, $props, $globalExtra);
+ // Local
+ $pathId = md5(self::idFromArray($flat, ...$pathFields));
+ $props = self::propsFromArray($flat, ...$localProps);
+ $mappingId = self::writeLocalHardwareData($uuid, $hwid, $pathId, $props + $localExtra);
+ $thisMachineHwIds[] = $mappingId;
+ }
+ // Any hw <-> client mappings not in that list get marked as disconnected
+ self::markDisconnected($uuid, $dbType, $thisMachineHwIds);
+ return count($thisMachineHwIds);
+ }
+
+}
diff --git a/modules-available/statistics/inc/hardwareparserlegacy.inc.php b/modules-available/statistics/inc/hardwareparserlegacy.inc.php
new file mode 100644
index 00000000..a6ac6d5e
--- /dev/null
+++ b/modules-available/statistics/inc/hardwareparserlegacy.inc.php
@@ -0,0 +1,285 @@
+<?php
+
+class HardwareParserLegacy
+{
+
+ public static function parseHdd(&$row, $data)
+ {
+ $hdds = [];
+ // Could have more than one disk - linear scan
+ $lines = preg_split("/[\r\n]+/", $data);
+ $i = 0;
+ $mbrToByteFactor = $sectorToByteFactor = 0;
+ foreach ($lines as $line) {
+ if (preg_match('/^Disk (\S+):.* (\d+) bytes/i', $line, $out)) {
+ // --- Beginning of MBR disk ---
+ unset($hdd);
+ if ($out[2] < 10000) // sometimes vmware reports lots of 512byte disks
+ continue;
+ if (preg_match('#^/dev/(dm-|x?loop|d?nbd)#', $out[1])) // Ignore device mapper etc.
+ continue;
+ // disk total size and name
+ $mbrToByteFactor = 0; // This is != 0 for mbr
+ $sectorToByteFactor = 0; // This is != for gpt
+ $hdd = [
+ 'devid' => 'devid-' . ++$i,
+ 'dev' => $out[1],
+ 'sectors' => 0,
+ 'size' => $out[2],
+ 'used' => 0,
+ 'partitions' => [],
+ ];
+ $hdds[] = &$hdd;
+ } elseif (preg_match('/^Disk (\S+):\s+(\d+)\s+sectors,/i', $line, $out)) {
+ // --- Beginning of GPT disk ---
+ unset($hdd);
+ if ($out[2] < 1000) // sometimes vmware reports lots of 512byte disks
+ continue;
+ if (preg_match('#^/dev/(dm-|x?loop|d?nbd)#', $out[1])) // Ignore device mapper etc.
+ continue;
+ // disk total size and name
+ $mbrToByteFactor = 0; // This is != 0 for mbr
+ $sectorToByteFactor = 0; // This is != for gpt
+ $hdd = [
+ 'devid' => 'devid-' . ++$i,
+ 'dev' => $out[1],
+ 'sectors' => $out[2],
+ 'size' => 0,
+ 'used' => 0,
+ 'partitions' => [],
+ ];
+ $hdds[] = &$hdd;
+ } elseif (preg_match('/^Units =.*= (\d+) bytes/i', $line, $out)) {
+ // --- MBR: Line that tells us how to interpret units for the partition lines ---
+ // Unit for start and end
+ $mbrToByteFactor = $out[1]; // Convert so that multiplying by unit yields MiB
+ } elseif (preg_match('/^Logical sector size:\s*(\d+)/i', $line, $out)) {
+ // --- GPT: Line that tells us the logical sector size used everywhere ---
+ $sectorToByteFactor = $out[1];
+ } elseif (isset($hdd) && preg_match('/^First usable sector.* is (\d+)$/i', $line, $out)) {
+ // --- Some fdisk versions are messed up and report 2^32 as the sector count in the first line,
+ // but the correct value in the "last usable sector is xxxx" line below ---
+ if ($out[1] > $hdd['sectors']) {
+ $hdd['sectors'] = $out[1];
+ }
+ } elseif (isset($hdd) && $mbrToByteFactor !== 0 && preg_match(',
+ ^/dev/(\S+) # device
+ \s+.*\s(\d+)[+\-]? # start
+ \s+(\d+)[+\-]? # end
+ \s+\d+[+\-]? # size
+ \s+([0-9a-f]+) # typeid
+ \s+(.*)$ # type name
+ ,ix', $line, $out)) {
+ // --- MBR: Partition entry ---
+ // Some partition
+ $type = strtolower($out[4]);
+ if ($type === '5' || $type === 'f' || $type === '85') {
+ continue;
+ } elseif ($type === '44') {
+ $out[5] = 'OpenSLX-ID44';
+ } elseif ($type === '45') {
+ $out[5] = 'OpenSLX-ID45';
+ }
+
+ $start = $out[2] * $mbrToByteFactor;
+ $partsize = ($out[3] - $out[2]) * $mbrToByteFactor;
+ $hdd['partitions'][] = [
+ 'id' => $out[1],
+ 'index' => $out[1],
+ 'start' => $start,
+ 'size' => $partsize,
+ 'name' => $out[5],
+ 'slxtype' => $type,
+ ];
+ $hdd['used'] += $partsize;
+ } elseif (isset($hdd) && $sectorToByteFactor !== 0 && preg_match(',
+ ^\s*(\d+) # index
+ \s+(\d+)[+\-]? # start
+ \s+(\d+)[+\-]? # end
+ \s+\S+ # human readable size
+ \s+([0-9a-f]{2})[0-9a-f]* # pseudo-type-id
+ \s+(.*)$ # PartLabel
+ ,ix', $line, $out)) {
+ // --- GPT: Partition entry ---
+ // Some partition
+ $slxtype = $out[4];
+ if ($out[5] === 'OpenSLX-ID44') {
+ $slxtype = '44';
+ } elseif ($out[5] === 'OpenSLX-ID45') {
+ $slxtype = '45';
+ } elseif ($out[5] === 'Linux swap') {
+ $slxtype = '82';
+ }
+ $id = $hdd['devid'] . '-' . $out[1];
+ $start = $out[2] * $sectorToByteFactor;
+ $partsize = ($out[3] - $out[2]) * $sectorToByteFactor;
+ $hdd['partitions'][] = [
+ 'id' => $id,
+ 'index' => $out[1],
+ 'start' => $start,
+ 'size' => $partsize,
+ 'name' => $out[5],
+ 'slxtype' => $slxtype,
+ ];
+ $hdd['used'] += $partsize;
+ }
+ }
+ unset($hdd);
+ foreach ($hdds as &$hdd) {
+ if ($hdd['size'] === 0 && $hdd['sectors'] !== 0) {
+ $hdd['size'] = round($hdd['sectors'] * $sectorToByteFactor);
+ }
+ }
+ unset($hdd);
+ $row['hdds'] = &$hdds;
+ }
+
+ public static function parsePci(string $data): array
+ {
+ preg_match_all('/[a-f0-9:.]{7}\s+"(Class\s*)?(?<class>[a-f0-9]{4})"\s+"(?<vendor>[a-f0-9]{4})"\s+"(?<device>[a-f0-9]{4})".*(:?-r(?<rev>[0-9a-f]+))?/is', $data, $out, PREG_SET_ORDER);
+ return $out;
+ }
+
+ public static function parseSmartctl(&$hdds, $data)
+ {
+ $lines = preg_split("/[\r\n]+/", $data);
+ foreach ($lines as $line) {
+ if (preg_match('/^NEXTHDD=(.+)$/', $line, $out)) {
+ unset($dev);
+ foreach ($hdds as &$hdd) {
+ if ($hdd['dev'] === $out[1]) {
+ $dev = &$hdd;
+ }
+ }
+ continue;
+ }
+ if (!isset($dev)) {
+ continue;
+ }
+ if (preg_match('/^([A-Z][^:]+):\s*(.*)$/', $line, $out)) {
+ $key = preg_replace('/\s|-|_/', '', $out[1]);
+ if ($key === 'ModelNumber' || $key === 'DeviceModel') {
+ $dev['model'] = $out[2];
+ } elseif ($key === 'ModelFamily') {
+ $dev['model_family'] = $out[2];
+ } elseif ($key === 'SerialNumber') {
+ $dev['serial_number'] = $out[2];
+ }
+ } elseif (preg_match('/
+ ^\s*(?<id>\d+)\s+\S+ # flags
+ \s+\S+\s+(?<v>\d+)
+ \s+(?<w>\d+)
+ \s+(?<t>\S+)\s+\S+ # fail
+ \s+(?<raw>\d+)(\s|$)/x', $line, $out)) {
+ $dev['attr_' . $out['id']] = [
+ 'value' => $out['v'],
+ 'worst' => $out['w'],
+ 'thresh' => $out['t'],
+ 'raw' => $out['raw'],
+ ];
+ if ($out['id'] == 194) {
+ $dev['temperature'] = $out['raw'];
+ }
+ }
+ }
+ }
+
+ public static function parseCpu(&$row, $data)
+ {
+ if (0 >= preg_match_all('/^(.+):\s+(\d+)$/im', $data, $out, PREG_SET_ORDER)) {
+ return;
+ }
+ $tmp = [];
+ foreach ($out as $entry) {
+ $tmp[str_replace(' ', '', $entry[1])] = $entry[2];
+ }
+ $row['cpu-sockets'] = $tmp['Sockets'];
+ $row['cpu-cores'] = $tmp['Realcores'];
+ $row['cpu-threads'] = $tmp['Virtualcores'];
+ }
+
+ public static function parseDmiDecode(&$row, $data)
+ {
+ $lines = preg_split("/[\r\n]+/", $data);
+ $section = false;
+ $ramOk = false;
+ $ramForm = $ramType = false;
+ $ramslot = [];
+ $row['ram'] = $row['system'] = $row['mainboard'] = $row['bios'] = [];
+ $row['Memory Slot Count'] = $row['Memory Maximum Capacity'] = 0;
+ foreach ($lines as $line) {
+ if (empty($line)) {
+ continue;
+ }
+ if ($line[0] !== "\t" && $line[0] !== ' ') {
+ if (isset($ramslot['Size'])) {
+ $row['ram'][] = $ramslot;
+ }
+ $ramslot = [];
+ $section = $line;
+ $ramOk = false;
+ if ($ramForm || $ramType) {
+ if (isset($row['ramtype'])) {
+ continue;
+ }
+ $row['ramtype'] = $ramType . '-' . $ramForm;
+ $ramForm = false;
+ $ramType = false;
+ }
+ continue;
+ }
+ if ($section === 'Base Board Information') {
+ if (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out)
+ && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified') {
+ $row['mainboard'][$out[1]] = $out[2];
+ }
+ } elseif ($section === 'System Information') {
+ if (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out)
+ && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified') {
+ $row['system'][$out[1]] = $out[2];
+ }
+ } elseif ($section === 'Physical Memory Array') {
+ if (!$ramOk && preg_match('/Use: System Memory/i', $line)) {
+ $ramOk = true;
+ }
+ if ($ramOk && preg_match('/^\s*Number Of Devices:\s+(\d+)\s*$/i', $line, $out)) {
+ $row['Memory Slot Count'] += (int)$out[1];
+ }
+ if ($ramOk && preg_match('/^\s*Maximum Capacity:\s+(\d.+)/i', $line, $out)) {
+ /** @var array{"Memory Slot Count": int} $row */
+ $row['Memory Maximum Capacity'] += (int)HardwareParser::convertSize($out[1], 'G', false);
+ }
+ } elseif ($section === 'Memory Device') {
+ if (preg_match('/^\s*Size:\s*(.*?)\s*$/i', $line, $out)) {
+ if (preg_match('/(\d+)\s*(\w)i?B/i', $out[1])) {
+ if (HardwareParser::convertSize($out[1], 'M', false) < 35)
+ continue; // TODO: Parsing this line by line is painful. Check for other indicators, like Locator
+ $ramslot['Size'] = HardwareParser::convertSize($out[1], 'G');
+ }
+ } elseif (preg_match('/^\s*Manufacturer:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
+ $ramslot['Manufacturer'] = HardwareParser::decodeJedec($out[1]);
+ } elseif (preg_match('/^\s*Form Factor:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
+ $ramForm = $out[1];
+ } elseif (preg_match('/^\s*Type:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
+ $ramType = $out[1];
+ } elseif (preg_match('/^\s*Configured Memory Speed:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
+ $ramslot['Configured Clock Speed'] = $out[1];
+ } elseif (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out)
+ && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified' && $out[2] !== 'None') {
+ $ramslot[$out[1]] = $out[2];
+ }
+ } elseif ($section === 'BIOS Information') {
+ if (preg_match('/^\s*([^:]+):\s*(.*?)\s*$/i', $line, $out)
+ && $out[2] !== 'Unknown' && $out[2] !== '' && $out[2] !== 'Not Specified') {
+ $row['bios'][$out[1]] = $out[2];
+ }
+ }
+ }
+ if (empty($row['Memory Slot Count']) || (isset($row['ramslot']) && $row['Memory Slot Count'] < count($row['ramslot']))) {
+ $row['Memory Slot Count'] = isset($row['ramslot']) ? count($row['ramslot']) : 0;
+ }
+ if ($row['Memory Maximum Capacity'] > 0) {
+ $row['Memory Maximum Capacity'] .= ' GiB';
+ }
+ }
+} \ No newline at end of file
diff --git a/modules-available/statistics/inc/hardwarequery.inc.php b/modules-available/statistics/inc/hardwarequery.inc.php
new file mode 100644
index 00000000..6b1b5043
--- /dev/null
+++ b/modules-available/statistics/inc/hardwarequery.inc.php
@@ -0,0 +1,169 @@
+<?php
+
+class HardwareQuery
+{
+
+ private $id = 0;
+ private $joins = [];
+ private $where = [];
+ private $args = [];
+ private $columns = [];
+
+ /**
+ * @param string $type hardware type form HardwareInfo
+ * @param ?string $uuid If set, only return data for specific client
+ */
+ public function __construct(string $type, string $uuid = null, $connectedOnly = true)
+ {
+ if ($connectedOnly) {
+ $this->joins['mxhw_join'] = "INNER JOIN machine_x_hw mxhw ON (mxhw.hwid = shw.hwid AND mxhw.disconnecttime = 0)";
+ } else {
+ $this->joins['mxhw_join'] = "INNER JOIN machine_x_hw mxhw ON (mxhw.hwid = shw.hwid)";
+ }
+ if ($uuid !== null) {
+ $this->where[] = 'mxhw.machineuuid = :uuid';
+ $this->args['uuid'] = $uuid;
+ }
+ $this->where[] = 'shw.hwtype = :hwtype';
+ $this->args['hwtype'] = $type;
+ }
+
+ private function id(): string
+ {
+ return 'b' . (++$this->id);
+ }
+
+ /**
+ * Add join of a virtual column (hw property) to an arbitrary table and column.
+ * @param bool $global Is the virtual column global or local to machine?
+ * @param string $prop Name of property/virtual column
+ * @param string $jTable Table to join on
+ * @param string $jColumn Column to join on
+ * @param string $condColumn optionally, another column from the joined table to match against $condVal
+ * @param string|array $condVal optionally, a literal, or array of literals, to match foreign column against
+ * @return void
+ */
+ public function addForeignJoin(bool $global, string $prop, string $jTable, string $jColumn, string $condColumn = '', $condVal = null)
+ {
+ if (isset($this->columns["$jTable.$prop"]))
+ return;
+ if ($global) {
+ $srcTable = 'shw';
+ $table = 'statistic_hw_prop';
+ $column = 'hwid';
+ } else {
+ $srcTable = 'mxhw';
+ $table = 'machine_x_hw_prop';
+ $column = 'machinehwid';
+ }
+ $tid = $this->id();
+ $pid = $this->id();
+ $this->joins[$prop] = "INNER JOIN $table $tid ON ($srcTable.$column = $tid.$column
+ AND $tid.prop = :$pid)";
+ $this->args[$pid] = $prop;
+ $this->columns[$prop] = "$tid.`value` AS `$prop`";
+ $jtid = $this->id();
+ $cond = '';
+ if (!empty($condColumn)) {
+ $vid = $this->id();
+ if (is_array($condVal)) {
+ $cond = " AND $jtid.`$condColumn` IN (:$vid)";
+ } else {
+ $cond = " AND $jtid.`$condColumn` = :$vid";
+ }
+ $this->args[$vid] = $condVal;
+ }
+ $this->joins[$jTable] = "INNER JOIN $jTable $jtid ON ($jtid.$jColumn = $tid.`value` $cond)";
+ }
+
+ public function addMachineWhere(string $column, string $op, $value)
+ {
+ if (isset($this->columns[$column]))
+ return;
+ $vid = $this->id();
+ $this->joins['machine'] = 'INNER JOIN machine m USING (machineuuid)';
+ $this->where[] = "m.$column $op (:$vid)";
+ $this->args[$vid] = $value;
+ $this->columns[$column] = "m.$column";
+ }
+
+ public function addGlobalColumn(string $prop): HardwareQueryColumn
+ {
+ return $this->addColumn(true, $prop);
+ }
+
+ public function addLocalColumn(string $prop): HardwareQueryColumn
+ {
+ return $this->addColumn(false, $prop);
+ }
+
+ public function addColumn(bool $global, string $prop, string $alias = null): HardwareQueryColumn
+ {
+ return $this->columns[] = new HardwareQueryColumn($global, $prop, $alias);
+ }
+
+ /**
+ * Join the machine table and add the given column from it to the SELECT
+ */
+ public function addMachineColumn(string $column): void
+ {
+ if (isset($this->columns[$column]))
+ return;
+ $this->joins['machine'] = 'INNER JOIN machine m USING (machineuuid)';
+ $this->columns[$column] = "m.$column";
+ }
+
+ /**
+ * @return false|PDOStatement
+ */
+ public function query($groupBy = '')
+ {
+ return Database::simpleQuery($this->buildQuery($groupBy), $this->args);
+ }
+
+ /**
+ * Build query string
+ * @param string[]|string $groupBy Column to group by
+ */
+ public function buildQuery($groupBy = ''): string
+ {
+ if (empty($groupBy)) {
+ $groupBy = [];
+ } elseif (!is_array($groupBy)) {
+ $groupBy = [$groupBy];
+ }
+ foreach ($groupBy as &$gb) {
+ if ($gb[0] !== '`') {
+ $gb = "`$gb`";
+ }
+ }
+ $columns = [];
+ foreach ($this->columns as $column) {
+ if ($column instanceof HardwareQueryColumn) {
+ $column->generate($this->joins, $columns, $this->args, $groupBy);
+ } else {
+ $columns[] = $column;
+ }
+ }
+ $columns[] = 'mxhw.machineuuid';
+ $columns[] = 'shw.hwid';
+ // TODO: Untangle this implicit magic
+ if (empty($groupBy) || $groupBy[0] === 'mxhw.machinehwid') {
+ $columns[] = 'mxhw.disconnecttime';
+ } else {
+ $columns[] = 'Sum(If(mxhw.disconnecttime = 0, 1, 0)) AS connected_count';
+ }
+ if (!empty($groupBy)) {
+ $columns[] = 'Count(*) AS group_count';
+ $groupBy = " GROUP BY " . implode(', ', $groupBy);
+ } else {
+ $groupBy = '';
+ }
+ return 'SELECT ' . implode(', ', $columns)
+ . ' FROM statistic_hw shw '
+ . implode(' ', $this->joins)
+ . ' WHERE ' . implode(' AND ', $this->where)
+ . $groupBy;
+ }
+
+}
diff --git a/modules-available/statistics/inc/hardwarequerycolumn.inc.php b/modules-available/statistics/inc/hardwarequerycolumn.inc.php
new file mode 100644
index 00000000..01e32978
--- /dev/null
+++ b/modules-available/statistics/inc/hardwarequerycolumn.inc.php
@@ -0,0 +1,94 @@
+<?php
+
+class HardwareQueryColumn
+{
+ /** @var int For unique table names in join */
+ private static $id = 0;
+
+ private $global;
+ private $tableAlias;
+ private $virtualColumnName;
+ private $alias;
+ private $conditions = [];
+ private $params = [];
+ private $classId;
+
+ private static function getId(): string
+ {
+ return 't' . ++self::$id;
+ }
+
+ public function __construct(bool $global, string $column, string $alias = null)
+ {
+ $this->classId = ++self::$id;
+ $this->global = $global;
+ $this->tableAlias = self::getId();
+ $this->virtualColumnName = $column;
+ $this->alias = '`' . ($alias ?? $column) . '`';
+ }
+
+ /**
+ * Add necessary conditions, joins, columns to final SQL arrays. To be called
+ * from HardwareQuery::buildQuery().
+ * @param string[] $groupConcat if column name is NOT in this array, add as distinct GROUP_CONCAT to column.
+ */
+ public function generate(array &$joins, array &$columns, array &$params, array $groupConcat = [], string $globalSrcTableAlias = null)
+ {
+ if ($this->global) {
+ $srcTable = $globalSrcTableAlias ?? 'shw';
+ $table = 'statistic_hw_prop';
+ $column = 'hwid';
+ } else {
+ $srcTable = 'mxhw';
+ $table = 'machine_x_hw_prop';
+ $column = 'machinehwid';
+ }
+ $tid = $this->tableAlias;
+ $pid = self::getId();
+ $this->conditions[] = "$srcTable.$column = $tid.$column AND $tid.prop = :$pid";
+ $params[$pid] = $this->virtualColumnName; // value of property column is our virtual column
+ // If we have just one condition, it's the join condition itself. Since we pretend we're just adding
+ // a column to the query, do a left join, so the "column" is NULL if the join doesn't match.
+ // If however any conditions were added to this class via the addCondition() method, do a regular
+ // INNER JOIN, so the result will be empty if the condition doesn't match.
+ $type = count($this->conditions) === 1 ? 'LEFT' : 'INNER';
+ $joins[] = "$type JOIN $table $tid ON (" . implode(' AND ', $this->conditions) . ")";
+ if (!empty($groupConcat) && !in_array($this->alias, $groupConcat)) {
+ $columns[] = "Group_Concat(DISTINCT $tid.`value` SEPARATOR ', ') AS {$this->alias}";
+ } else {
+ $columns[] = "$tid.`value` AS {$this->alias}";
+ }
+ $params += $this->params;
+ }
+
+ /**
+ * @param string $op Operator (<>=, IN, LIKE)
+ * @param string|string[]|HardwareQueryColumn $other value to compare with.
+ * Can be a literal, an array (if opererator is IN), or another Column
+ * @return void
+ */
+ public function addCondition(string $op, $other)
+ {
+ $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value';
+ if ($other instanceof HardwareQueryColumn) {
+ $cond = "{$this->tableAlias}.`$valueCol` $op {$other->tableAlias}.`$valueCol`";
+ // Don't reference a column of a table that hasn't been joined yet
+ if ($this->classId > $other->classId) {
+ $this->conditions[] = $cond;
+ } else {
+ $other->conditions[] = $cond;
+ }
+ } elseif ($op === '~' || $op === '!~') {
+ $op = $op === '~' ? 'LIKE' : 'NOT LIKE';
+ $other = str_replace(array('=', '_', '%', '*', '?'), array('==', '=_', '=%', '%', '_'), $other);
+ $pid = self::getId();
+ $this->conditions[] = "{$this->tableAlias}.`$valueCol` $op (:$pid) ESCAPE '='";
+ $this->params[$pid] = $other;
+ } else {
+ $pid = self::getId();
+ $this->conditions[] = "{$this->tableAlias}.`$valueCol` $op (:$pid)";
+ $this->params[$pid] = $other;
+ }
+ }
+
+}
diff --git a/modules-available/statistics/inc/parser.inc.php b/modules-available/statistics/inc/parser.inc.php
deleted file mode 100644
index bdf021a6..00000000
--- a/modules-available/statistics/inc/parser.inc.php
+++ /dev/null
@@ -1,410 +0,0 @@
-<?php
-
-class Parser {
- public static function parseCpu(&$row, $data)
- {
- if (0 >= preg_match_all('/^(.+):\s+(\d+)$/im', $data, $out, PREG_SET_ORDER)) {
- return;
- }
- foreach ($out as $entry) {
- $row[str_replace(' ', '', $entry[1])] = $entry[2];
- }
- }
-
- public static function parseDmiDecode(&$row, $data)
- {
- $lines = preg_split("/[\r\n]+/", $data);
- $section = false;
- $ramOk = false;
- $ramForm = $ramType = $ramSpeed = $ramClockSpeed = false;
- $ramslot = [];
- $row['ramslotcount'] = $row['maxram'] = 0;
- foreach ($lines as $line) {
- if (empty($line)) {
- continue;
- }
- if ($line{0} !== "\t" && $line{0} !== ' ') {
- if (isset($ramslot['size'])) {
- $row['ramslot'][] = $ramslot;
- $ramslot = [];
- }
- $section = $line;
- $ramOk = false;
- if (($ramForm || $ramType) && ($ramSpeed || $ramClockSpeed)) {
- if (isset($row['ramtype']) && !$ramClockSpeed) {
- continue;
- }
- $row['ramtype'] = $ramType . ' ' . $ramForm;
- if ($ramClockSpeed) {
- $row['ramtype'] .= ', ' . $ramClockSpeed;
- } elseif ($ramSpeed) {
- $row['ramtype'] .= ', ' . $ramSpeed;
- }
- $ramForm = false;
- $ramType = false;
- $ramClockSpeed = false;
- }
- continue;
- }
- if ($section === 'Base Board Information') {
- if (preg_match('/^\s*Product Name: +(\S.+?) *$/i', $line, $out)) {
- $row['mobomodel'] = $out[1];
- }
- if (preg_match('/^\s*Manufacturer: +(\S.+?) *$/i', $line, $out)) {
- $row['mobomanufacturer'] = $out[1];
- }
- } elseif ($section === 'System Information') {
- if (preg_match('/^\s*Product Name: +(\S.+?) *$/i', $line, $out)) {
- $row['pcmodel'] = $out[1];
- }
- if (preg_match('/^\s*Manufacturer: +(\S.+?) *$/i', $line, $out)) {
- $row['pcmanufacturer'] = $out[1];
- }
- } elseif ($section === 'Physical Memory Array') {
- if (!$ramOk && preg_match('/Use: System Memory/i', $line)) {
- $ramOk = true;
- }
- if ($ramOk && preg_match('/^\s*Number Of Devices:\s+(\d+)\s*$/i', $line, $out)) {
- $row['ramslotcount'] += $out[1];
- }
- if ($ramOk && preg_match('/^\s*Maximum Capacity:\s+(\d.+)/i', $line, $out)) {
- $row['maxram'] += self::convertSize($out[1], 'G', false);
- }
- } elseif ($section === 'Memory Device') {
- 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'] = '_____';
- }
- }
- if (preg_match('/^\s*Manufacturer:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
- $ramslot['manuf'] = self::decodeJedec($out[1]);
- }
- if (preg_match('/^\s*Form Factor:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
- $ramForm = $out[1];
- }
- if (preg_match('/^\s*Type:\s*(.*?)\s*$/i', $line, $out) && $out[1] !== 'Unknown') {
- $ramType = $out[1];
- }
- if (preg_match('/^\s*Speed:\s*(\d.*?)\s*$/i', $line, $out)) {
- $ramSpeed = $out[1];
- }
- if (preg_match('/^\s*Configured (?:Clock|Memory) Speed:\s*(\d.*?)\s*$/i', $line, $out)) {
- $ramClockSpeed = $out[1];
- }
- } elseif ($section === 'BIOS Information') {
- if (preg_match(',^\s*Release Date:\s*(\d{2}/\d{2}/\d{4})\s*$,i', $line, $out)) {
- $row['biosdate'] = date('d.m.Y', strtotime($out[1]));
- } elseif (preg_match('/^\s*BIOS Revision:\s*(.*?)\s*$/i', $line, $out)) {
- $row['biosrevision'] = $out[1];
- } elseif (preg_match('/^\s*Version:\s*(.*?)\s*$/i', $line, $out)) {
- $row['biosversion'] = $out[1];
- }
- }
- }
- if (empty($row['ramslotcount']) || (isset($row['ramslot']) && $row['ramslotcount'] < count($row['ramslot']))) {
- $row['ramslotcount'] = isset($row['ramslot']) ? count($row['ramslot']) : 0;
- }
- if ($row['maxram'] > 0) {
- $row['maxram'] .= ' GiB';
- }
- }
-
- const LOOKUP = ['T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024, '' => 1];
-
- /**
- * Convert/format size unit. Input string can be a size like
- * 8 GB or 1024 MB and will be converted according to passed parameters.
- * @param string $string Input string
- * @param string $scale 'a' for auto, T/G/M/K for according units
- * @param bool $appendUnit append unit string, e.g. 'GiB'
- * @return string|int Formatted result
- */
- private static function convertSize($string, $scale = 'a', $appendUnit = true)
- {
- if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out))
- return false;
- $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) {
- $scale = $k;
- break;
- }
- }
- }
- $val = round($val / self::LOOKUP[$scale]);
- if ($appendUnit) {
- $val .= ' ' . ($scale === '' ? 'Byte' : $scale . 'iB'); // NBSP!!
- }
- return $val;
- }
-
- public static function parseHdd(&$row, $data)
- {
- $hdds = array();
- // Could have more than one disk - linear scan
- $lines = preg_split("/[\r\n]+/", $data);
- $i = 0;
- $mbrToMbFactor = $sectorToMbFactor = 0;
- foreach ($lines as $line) {
- if (preg_match('/^Disk (\S+):.* (\d+) bytes/i', $line, $out)) {
- // --- Beginning of MBR disk ---
- unset($hdd);
- if ($out[2] < 10000) // sometimes vmware reports lots of 512byte disks
- continue;
- if (substr($out[1], 0, 8) === '/dev/dm-') // Ignore device mapper
- continue;
- // disk total size and name
- $mbrToMbFactor = 0; // This is != 0 for mbr
- $sectorToMbFactor = 0; // This is != for gpt
- $hdd = array(
- 'devid' => 'devid-' . ++$i,
- 'dev' => $out[1],
- 'sectors' => 0,
- 'size' => round($out[2] / (1024 * 1024 * 1024)),
- 'used' => 0,
- 'partitions' => array(),
- 'json' => array(),
- );
- $hdds[] = &$hdd;
- } elseif (preg_match('/^Disk (\S+):\s+(\d+)\s+sectors,/i', $line, $out)) {
- // --- Beginning of GPT disk ---
- unset($hdd);
- if ($out[2] < 1000) // sometimes vmware reports lots of 512byte disks
- continue;
- if (substr($out[1], 0, 8) === '/dev/dm-') // Ignore device mapper
- continue;
- // disk total size and name
- $mbrToMbFactor = 0; // This is != 0 for mbr
- $sectorToMbFactor = 0; // This is != for gpt
- $hdd = array(
- 'devid' => 'devid-' . ++$i,
- 'dev' => $out[1],
- 'sectors' => $out[2],
- 'size' => 0,
- 'used' => 0,
- 'partitions' => array(),
- 'json' => array(),
- );
- $hdds[] = &$hdd;
- } elseif (preg_match('/^Units =.*= (\d+) bytes/i', $line, $out)) {
- // --- MBR: Line that tells us how to interpret units for the partition lines ---
- // Unit for start and end
- $mbrToMbFactor = $out[1] / (1024 * 1024); // Convert so that multiplying by unit yields MiB
- } elseif (preg_match('/^Logical sector size:\s*(\d+)/i', $line, $out)) {
- // --- GPT: Line that tells us the logical sector size used everywhere ---
- $sectorToMbFactor = $out[1] / (1024 * 1024);
- } elseif (isset($hdd) && preg_match('/^First usable sector.* is (\d+)$/i', $line, $out)) {
- // --- Some fdisk versions are messed up and report 2^32 as the sector count in the first line,
- // but the correct value in the "last usable sector is xxxx" line below ---
- if ($out[1] > $hdd['sectors']) {
- $hdd['sectors'] = $out[1];
- }
- } elseif (isset($hdd) && $mbrToMbFactor !== 0 && preg_match(',^/dev/(\S+)\s+.*\s(\d+)[\+\-]?\s+(\d+)[\+\-]?\s+\d+[\+\-]?\s+([0-9a-f]+)\s+(.*)$,i', $line, $out)) {
- // --- MBR: Partition entry ---
- // Some partition
- $type = strtolower($out[4]);
- if ($type === '5' || $type === 'f' || $type === '85') {
- continue;
- } elseif ($type === '44') {
- $out[5] = 'OpenSLX-ID44';
- $color = '#5c1';
- } elseif ($type === '45') {
- $out[5] = 'OpenSLX-ID45';
- $color = '#0d7';
- } elseif ($type === '82') {
- $color = '#48f';
- } else {
- $color = '#e55';
- }
-
- $partsize = round(($out[3] - $out[2]) * $mbrToMbFactor);
- $hdd['partitions'][] = array(
- 'id' => $out[1],
- 'name' => $out[1],
- 'size' => round($partsize / 1024, $partsize < 1024 ? 1 : 0),
- 'type' => $out[5],
- );
- $hdd['json'][] = array(
- 'label' => $out[1],
- 'value' => $partsize,
- 'color' => $color,
- );
- $hdd['used'] += $partsize;
- } elseif (isset($hdd) && $sectorToMbFactor !== 0 && preg_match(',^\s*(\d+)\s+(\d+)[\+\-]?\s+(\d+)[\+\-]?\s+\S+\s+([0-9a-f]+)\s+(.*)$,i', $line, $out)) {
- // --- GPT: Partition entry ---
- // Some partition
- $type = $out[5];
- if ($type === 'OpenSLX-ID44') {
- $color = '#5c1';
- } elseif ($type === 'OpenSLX-ID45') {
- $color = '#0d7';
- } elseif ($type === 'Linux swap') {
- $color = '#48f';
- } else {
- $color = '#e55';
- }
- $id = $hdd['devid'] . '-' . $out[1];
- $partsize = round(($out[3] - $out[2]) * $sectorToMbFactor);
- $hdd['partitions'][] = array(
- 'id' => $id,
- 'name' => $out[1],
- 'size' => round($partsize / 1024, $partsize < 1024 ? 1 : 0),
- 'type' => $type,
- );
- $hdd['json'][] = array(
- 'label' => $id,
- 'value' => $partsize,
- 'color' => $color,
- );
- $hdd['used'] += $partsize;
- }
- }
- unset($hdd);
- $i = 0;
- foreach ($hdds as &$hdd) {
- $hdd['used'] = round($hdd['used'] / 1024);
- if ($hdd['size'] === 0 && $hdd['sectors'] !== 0) {
- $hdd['size'] = round(($hdd['sectors'] * $sectorToMbFactor) / 1024);
- }
- $free = $hdd['size'] - $hdd['used'];
- if ($hdd['size'] > 0 && ($free > 5 || ($free / $hdd['size']) > 0.1)) {
- $hdd['partitions'][] = array(
- 'id' => 'free-id-' . $i,
- 'name' => Dictionary::translate('unused'),
- 'size' => $free,
- 'type' => '-',
- );
- $hdd['json'][] = array(
- 'label' => 'free-id-' . $i,
- 'value' => $free * 1024,
- 'color' => '#aaa',
- );
- ++$i;
- }
- $hdd['json'] = json_encode($hdd['json']);
- }
- unset($hdd);
- $row['hdds'] = &$hdds;
- }
-
- public static function parsePci(&$pci1, &$pci2, $data)
- {
- preg_match_all('/[a-f0-9\:\.]{7}\s+"(Class\s*)?(?<class>[a-f0-9]{4})"\s+"(?<ven>[a-f0-9]{4})"\s+"(?<dev>[a-f0-9]{4})"/is', $data, $out, PREG_SET_ORDER);
- $NOW = time();
- $pci = array();
- foreach ($out as $entry) {
- if (!isset($pci[$entry['class']])) {
- $class = 'c.' . $entry['class'];
- $res = Page_Statistics::getPciId('CLASS', $class);
- if ($res === false || $res['dateline'] < $NOW) {
- $pci[$entry['class']]['lookupClass'] = 'do-lookup';
- $pci[$entry['class']]['class'] = $class;
- } else {
- $pci[$entry['class']]['class'] = $res['value'];
- }
- }
- $new = array(
- 'ven' => $entry['ven'],
- 'dev' => $entry['ven'] . ':' . $entry['dev'],
- );
- $res = Page_Statistics::getPciId('VENDOR', $new['ven']);
- if ($res === false || $res['dateline'] < $NOW) {
- $new['lookupVen'] = 'do-lookup';
- } else {
- $new['ven'] = $res['value'];
- }
- $res = Page_Statistics::getPciId('DEVICE', $new['dev']);
- if ($res === false || $res['dateline'] < $NOW) {
- $new['lookupDev'] = 'do-lookup';
- } else {
- $new['dev'] = $res['value'] . ' (' . $new['dev'] . ')';
- }
- $pci[$entry['class']]['entries'][] = $new;
- }
- ksort($pci);
- foreach ($pci as $class => $entry) {
- if ($class === '0300' || $class === '0200' || $class === '0403') {
- $pci1[] = $entry;
- } else {
- $pci2[] = $entry;
- }
- }
- }
-
- public static function parseSmartctl(&$hdds, $data)
- {
- $lines = preg_split("/[\r\n]+/", $data);
- foreach ($lines as $line) {
- if (preg_match('/^NEXTHDD=(.+)$/', $line, $out)) {
- unset($dev);
- foreach ($hdds as &$hdd) {
- if ($hdd['dev'] === $out[1]) {
- $dev = &$hdd;
- }
- }
- continue;
- }
- if (!isset($dev)) {
- continue;
- }
- if (preg_match('/^([A-Z][^:]+):\s*(.*)$/', $line, $out)) {
- $key = preg_replace('/\s|-|_/', '', $out[1]);
- if ($key === 'ModelNumber') {
- $key = 'DeviceModel';
- }
- $dev['s_' . $key] = $out[2];
- } elseif (preg_match('/^\s*\d+\s+(\S+)\s+\S+\s+\d+\s+\d+\s+\S+\s+\S+\s+(\d+)(\s|$)/', $line, $out)) {
- $dev['s_' . preg_replace('/\s|-|_/', '', $out[1])] = $out[2];
- }
- }
- // Format strings
- foreach ($hdds as &$hdd) {
- if (isset($hdd['s_PowerOnHours'])) {
- $hdd['PowerOnTime'] = '';
- $val = (int)str_replace('.', '', $hdd['s_PowerOnHours']);
- if ($val > 8760) {
- $hdd['PowerOnTime'] .= floor($val / 8760) . 'Y, ';
- $val %= 8760;
- }
- if ($val > 720) {
- $hdd['PowerOnTime'] .= floor($val / 720) . 'M, ';
- $val %= 720;
- }
- if ($val > 24) {
- $hdd['PowerOnTime'] .= floor($val / 24) . 'd, ';
- $val %= 24;
- }
- $hdd['PowerOnTime'] .= $val . 'h';
- }
- }
- }
-
- public static function decodeJedec($string)
- {
- // JEDEC ID:7F 7F 9E 00 00 00 00 00
- if (preg_match('/JEDEC(?:\s*ID)?\s*:\s*([0-9a-f\s]+)/i', $string, $out)) {
- preg_match_all('/[0-9a-f]{2}/i', $out[1], $out);
- $bank = 0;
- foreach ($out[0] as $id) {
- $bank++;
- $id = hexdec($id) & 0x7f; // Let's just ignore the parity bit, and any potential error
- if ($id !== 0x7f)
- break;
- }
- if ($id !== 0) {
- static $data = false;
- if ($data === false) $data = json_decode(file_get_contents(dirname(__FILE__) . '/jedec.json'), true);
- if (array_key_exists('bank' . $bank, $data) && array_key_exists('id' . $id, $data['bank' . $bank]))
- return $data['bank' . $bank]['id' . $id];
- }
- }
- return $string;
- }
-
-}
diff --git a/modules-available/statistics/inc/pciid.inc.php b/modules-available/statistics/inc/pciid.inc.php
new file mode 100644
index 00000000..38a2c56d
--- /dev/null
+++ b/modules-available/statistics/inc/pciid.inc.php
@@ -0,0 +1,82 @@
+<?php
+
+class PciId
+{
+
+ const DEVICE = 'DEVICE';
+ const VENDOR = 'VENDOR';
+ const DEVCLASS = 'CLASS';
+ const AUTO = 'AUTO';
+
+
+ /**
+ * @param string $cat type of query - self::DEVICE, self::VENDOR, self::DEVCLASS or self::AUTO for auto detection
+ * @param string $id the id to query - depends on $cat
+ * @return string|false Name of Class/Vendor/Device, false if not found
+ */
+ public static function getPciId(string $cat, string $id, bool $dnsQuery = false)
+ {
+ static $cache = [];
+ if ($cat === self::DEVCLASS && $id[1] === '.') {
+ $id = substr($id, 2);
+ }
+ if ($cat === self::AUTO) {
+ if (preg_match('/^([a-f0-9]{4})[:._-]?([a-f0-9]{4})$/', $id, $out)) {
+ $cat = 'DEVICE';
+ $host = $out[2] . '.' . $out[1];
+ $id = $out[1] . ':' . $out[2];
+ } elseif (preg_match('/^[a-f0-9]{4}$/', $id)) {
+ $cat = 'VENDOR';
+ $host = $id;
+ } elseif (preg_match('/^c[.-]([a-f0-9]{2})([a-f0-9]{2})$/', $id)) {
+ $cat = 'CLASS';
+ $host = $out[2] . '.' . $out[1] . '.c';
+ $id = substr($id, 2);
+ } else {
+ error_log('Invalid PCIID lookup format: ' . $id);
+ return false;
+ }
+ } elseif ($cat === self::DEVICE && preg_match('/^([a-f0-9]{4})[:._-]?([a-f0-9]{4})$/', $id, $out)) {
+ $host = $out[2] . '.' . $out[1];
+ $id = $out[1] . ':' . $out[2];
+ } elseif ($cat === self::VENDOR && preg_match('/^([a-f0-9]{4})$/', $id)) {
+ $host = $id;
+ } elseif ($cat === self::DEVCLASS && preg_match('/^(?:c[.-])?([a-f0-9]{2})([a-f0-9]{2})$/', $id, $out)) {
+ $host = $out[2] . '.' . $out[1] . '.c';
+ $id = 'c.' . $out[1] . $out[2];
+ } else {
+ error_log("getPciId called with unknown format: ($cat) ($id)");
+ return false;
+ }
+ $key = $cat . '-' . $id;
+ if (isset($cache[$key]))
+ return $cache[$key];
+ $row = Database::queryFirst('SELECT value, dateline FROM pciid WHERE category = :cat AND id = :id LIMIT 1',
+ array('cat' => $cat, 'id' => $id));
+ if ($row !== false && $row['dateline'] >= time()) {
+ return $cache[$key] = $row['value'];
+ }
+ if (!$dnsQuery)
+ return false;
+ // Unknown, query
+ $res = dns_get_record($host . '.pci.id.ucw.cz', DNS_TXT);
+ if (!is_array($res))
+ return false;
+ foreach ($res as $entry) {
+ if (isset($entry['txt']) && substr($entry['txt'], 0, 2) === 'i=') {
+ $string = substr($entry['txt'], 2);
+ Database::exec('INSERT INTO pciid (category, id, value, dateline) VALUES (:cat, :id, :value, :timeout)'
+ . ' ON DUPLICATE KEY UPDATE value = VALUES(value), dateline = VALUES(dateline)',
+ array(
+ 'cat' => $cat,
+ 'id' => $id,
+ 'value' => $string,
+ 'timeout' => time() + mt_rand(10, 30) * 86400,
+ ), true);
+ return $cache[$key] = $string;
+ }
+ }
+ return $cache[$key] = ($row['value'] ?? false);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/statistics/inc/statistics.inc.php b/modules-available/statistics/inc/statistics.inc.php
index 1f8a081a..c12f5be4 100644
--- a/modules-available/statistics/inc/statistics.inc.php
+++ b/modules-available/statistics/inc/statistics.inc.php
@@ -7,7 +7,7 @@ class Statistics
private static $machineFields = false;
- private static function initFields($returnData)
+ private static function initFields(int $returnData): string
{
if (self::$machineFields === false) {
$r = new ReflectionClass('Machine');
@@ -19,23 +19,21 @@ class Statistics
} elseif ($returnData === Machine::RAW_DATA) {
self::$machineFields['data'] = true;
} else {
- Util::traceError('Invalid $returnData option passed');
+ ErrorHandler::traceError('Invalid $returnData option passed');
}
return implode(',', array_keys(self::$machineFields));
}
/**
- * @param string $machineuuid
* @param int $returnData What kind of data to return Machine::NO_DATA, Machine::RAW_DATA, ...
- * @return \Machine|false
*/
- public static function getMachine($machineuuid, $returnData)
+ public static function getMachine(string $machineuuid, int $returnData): ?Machine
{
$fields = self::initFields($returnData);
$row = Database::queryFirst("SELECT $fields FROM machine WHERE machineuuid = :machineuuid", compact('machineuuid'));
if ($row === false)
- return false;
+ return null;
$m = new Machine();
foreach ($row as $key => $val) {
$m->{$key} = $val;
@@ -44,23 +42,22 @@ class Statistics
}
/**
- * @param string $ip
* @param int $returnData What kind of data to return Machine::NO_DATA, Machine::RAW_DATA, ...
* @param string $sort something like 'lastseen ASC' - not sanitized, don't pass user input!
- * @return \Machine[] list of matches
+ * @return Machine[] list of matches
*/
- public static function getMachinesByIp($ip, $returnData, $sort = false)
+ public static function getMachinesByIp(string $ip, int $returnData, string $sort = null): array
{
$fields = self::initFields($returnData);
- if ($sort === false) {
+ if ($sort === null) {
$sort = '';
} else {
$sort = "ORDER BY $sort";
}
$res = Database::simpleQuery("SELECT $fields FROM machine WHERE clientip = :ip $sort", compact('ip'));
$list = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$m = new Machine();
foreach ($row as $key => $val) {
$m->{$key} = $val;
@@ -74,7 +71,7 @@ class Statistics
const OFFLINE_LENGTH = '~offline-length';
const SUSPEND_LENGTH = '~suspend-length';
- public static function logMachineState($uuid, $ip, $type, $start, $length, $username = '')
+ public static function logMachineState(string $uuid, string $ip, string $type, int $start, int $length, string $username = ''): int
{
return Database::exec('INSERT INTO statistic (dateline, typeid, machineuuid, clientip, username, data)'
. " VALUES (:start, :type, :uuid, :clientip, :username, :length)", array(
diff --git a/modules-available/statistics/inc/statisticsfilter.inc.php b/modules-available/statistics/inc/statisticsfilter.inc.php
index 4a4899e2..5e6448c7 100644
--- a/modules-available/statistics/inc/statisticsfilter.inc.php
+++ b/modules-available/statistics/inc/statisticsfilter.inc.php
@@ -10,8 +10,10 @@ abstract class StatisticsFilter
*/
const LEGACY_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);
+ const SIZE_PARTITION = [0, 8, 16, 24, 30, 40, 50, 60, 80, 100, 120, 150, 180, 250, 300, 400, 500, 1000, 1500, 2000, 3000,
+ 4000, 6000, 8000, 10000];
+ const SIZE_RAM = [1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 320, 480, 512, 768, 1024, 1536,
+ 2048];
private static $keyCounter = 0;
@@ -56,22 +58,57 @@ abstract class StatisticsFilter
$this->placeholder = $placeholder;
}
- public function type()
+ public function type(): string
{
return ($this->ops === self::OP_ORDINAL || $this->ops === self::OP_FUZZY_ORDINAL) ? 'int' : 'string';
}
- /* returns a where clause and adds needed operators to the passed arrays */
- public abstract function whereClause(string $operator, $argument, array &$args, array &$joins);
+ /**
+ * Needed for joins with the hardware tables, to use the HardwareQueryColumn afterwards.
+ * The HardwareQuery class should probably be extended/rewritten to be more versatile in
+ * this regard.
+ */
+ public static function addHardwareJoin(array &$args, array &$joins, string $hwtype = null): string
+ {
+ $joins['mxhw'] = ' INNER JOIN machine_x_hw mxhw ON (mxhw.disconnecttime = 0 AND mxhw.machineuuid = m.machineuuid)';
+ $key = self::getNewKey('foo');
+ $shw = self::getNewKey('shw');
+ if ($hwtype === null) {
+ $joins[] = " INNER JOIN statistic_hw $shw ON (mxhw.hwid = {$shw}.hwid)";
+ } else {
+ $joins[] = " INNER JOIN statistic_hw $shw ON (mxhw.hwid = {$shw}.hwid AND {$shw}.hwtype = :$key)";
+ $args[$key] = $hwtype;
+ }
+ return $shw;
+ }
- public function bind(string $op, $argument) { return new DatabaseFilter($this, $op, $argument); }
+ /**
+ * To be called by DatabaseFilter::whereClause() when building actual query.
+ * @param string $operator operator to use
+ * @param string[]|string $argument argument to compare against
+ * @param string[] $args assoc array to add parametrized version of $argument to
+ * @param string[] $joins any optional joins can be added to this array
+ * @return string where clause
+ */
+ public abstract function whereClause(string $operator, $argument, array &$args, array &$joins): string;
+ /**
+ * Called to get an instance of DatabaseFilter that binds the given $op and $argument to this filter.
+ * @param string[]|string $argument
+ */
+ public function bind(string $op, $argument): DatabaseFilter { return new DatabaseFilter($this, $op, $argument); }
+
+ /**
+ * Check if given $operator is valid for this filter. Throws error and halts if not.
+ * @return void
+ */
public final function validateOperator(string $operator)
{
if (empty($this->ops))
return;
if (!in_array($operator, $this->ops)) {
- Util::traceError("Invalid op '$operator' for " . get_class($this) . '::' . $this->column);
+ // Yes keep $this in this call, get_class() !== get_class($this)
+ ErrorHandler::traceError("Invalid op '$operator' for " . get_class($this) . '::' . $this->column);
}
}
@@ -100,7 +137,7 @@ abstract class StatisticsFilter
return ($array[$best] + $array[$best - 1]) / 2;
}
- public static function getNewKey($colname)
+ public static function getNewKey($colname): string
{
return $colname . '_' . (self::$keyCounter++);
}
@@ -108,7 +145,7 @@ abstract class StatisticsFilter
/**
* @return DatabaseFilter[]
*/
- public static function parseQuery()
+ public static function parseQuery(): array
{
// Get current settings from GET
$ops = Request::get('op', [], 'array');
@@ -141,10 +178,7 @@ abstract class StatisticsFilter
return $filters;
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- public static function renderFilterBox($show, $filterSet)
+ public static function renderFilterBox(string $show, StatisticsFilterSet $filterSet): void
{
// Build location list, with permissions
if (Module::isAvailable('locations')) {
@@ -156,7 +190,7 @@ abstract class StatisticsFilter
foreach (self::$columns as $key => $filter) {
$col = [
'key' => $key,
- 'name' => Dictionary::translateFile('filters', $key, true),
+ 'name' => Dictionary::translateFile('filters', $key),
'placeholder' => $filter->placeholder,
];
$bind = $filterSet->hasFilterKey($key);
@@ -169,8 +203,9 @@ abstract class StatisticsFilter
$col['inputclass'] = 'is-date';
} elseif ($filter->type() === 'enum') {
$col['enum'] = true;
+ /** @var EnumStatisticsFilter $filter */
$col['values'] = $filter->values;
- if ($bind !== false) {
+ if ($bind !== null) {
// Current value from GET
foreach ($col['values'] as &$value) {
if ($value['key'] == $bind->argument) {
@@ -180,7 +215,7 @@ abstract class StatisticsFilter
}
}
// current value from GET
- if ($bind !== false) {
+ if ($bind !== null) {
$col['currentvalue'] = $bind->argument;
$col['checked'] = 'checked';
$showCount++;
@@ -190,7 +225,7 @@ abstract class StatisticsFilter
$col['op'] = $filter->ops;
foreach ($col['op'] as &$value) {
$value = ['op' => $value];
- if ($bind !== false && $bind->op === $value['op']) {
+ if ($bind !== null && $bind->op === $value['op']) {
$value['selected'] = 'selected';
}
}
@@ -218,16 +253,16 @@ abstract class StatisticsFilter
'clientip' => new IpStatisticsFilter(),
'hostname' => new SimpleStatisticsFilter('hostname', self::OP_STRCMP, 'pc.fqdn.example.com'),
'machineuuid' => new SimpleStatisticsFilter('machineuuid', self::OP_STRCMP, '88888888-4444-4444-121212121212'),
- 'macaddr' => new SimpleStatisticsFilter('macaddr', self::OP_STRCMP, '11-22-33-44-55-66'),
+ 'macaddr' => new MacAddressStatisticsFilter(),
'firstseen' => new DateStatisticsFilter('firstseen', '2020-10-15 14:00'),
'lastseen' => new DateStatisticsFilter('lastseen', '2020-10-15 14:00'),
- 'logintime' => new DateStatisticsFilter('logintime', '2020-10-15 14:00'),
'lastboot' => new DateStatisticsFilter('lastboot', '2020-10-15 14:00'),
'runtime' => new RuntimeStatisticsFilter(),
'realcores' => new SimpleStatisticsFilter('realcores', self::OP_ORDINAL, ''),
- 'systemmodel' => new SimpleStatisticsFilter('systemmodel', self::OP_STRCMP, 'PC-365 (IBM)'),
+ 'systemmodel' => new SystemModelStatisticsFilter(),
'cpumodel' => new SimpleStatisticsFilter('cpumodel', self::OP_STRCMP, 'Pentium Pro 200 MHz'),
- 'hddgb' => new Id44GbStatisticsFilter(),
+ 'hddgb' => new PartitionGbStatisticsFilter('id44mb'),
+ 'persistentgb' => new PartitionGbStatisticsFilter('id45mb'),
'gbram' => new RamGbStatisticsFilter(),
'kvmstate' => new EnumStatisticsFilter('kvmstate', ['ENABLED', 'DISABLED', 'UNSUPPORTED']),
'badsectors' => new SimpleStatisticsFilter('badsectors', self::OP_ORDINAL, ''),
@@ -236,6 +271,12 @@ abstract class StatisticsFilter
'live_swapfree' => new SimpleStatisticsFilter('live_swapfree', self::OP_ORDINAL, 'MiB'),
'live_memfree' => new SimpleStatisticsFilter('live_memfree', self::OP_ORDINAL, 'MiB'),
'live_tmpfree' => new SimpleStatisticsFilter('live_tmpfree', self::OP_ORDINAL, 'MiB'),
+ 'live_id45free' => new SimpleNotZeroStatisticsFilter('live_id45free', self::OP_ORDINAL, 'MiB'),
+ 'standbycrash' => new StandbyCrashStatisticsFilter(),
+ 'pcidev' => new PciDeviceStatisticsFilter(),
+ 'nicspeed' => new NicSpeedStatisticsFilter(),
+ 'hddrpm' => new HddRpmStatisticsFilter(),
+ //'anydev' => new AnyHardwarePropStatisticsFilter(),
];
if (Module::isAvailable('locations')) {
self::$columns['location'] = new LocationStatisticsFilter();
@@ -247,14 +288,14 @@ abstract class StatisticsFilter
class SimpleStatisticsFilter extends StatisticsFilter
{
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
$addendum = '';
$key = self::getNewKey($this->column);
$args[$key] = $argument;
if (is_array($argument)) {
- if ($operator{0} === '!') {
+ if ($operator[0] === '!') {
$op = 'NOT IN';
} else {
$op = 'IN';
@@ -277,6 +318,20 @@ class SimpleStatisticsFilter extends StatisticsFilter
}
+class SimpleNotZeroStatisticsFilter extends SimpleStatisticsFilter
+{
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $str = parent::whereClause($operator, $argument, $args, $joins);
+ if ((int)$argument !== 0 || $operator !== '=') {
+ $str = "($str AND {$this->column} != 0)";
+ }
+ return $str;
+ }
+
+}
+
class EnumStatisticsFilter extends SimpleStatisticsFilter
{
@@ -301,28 +356,55 @@ class EnumStatisticsFilter extends SimpleStatisticsFilter
$this->values = $values;
}
- public function type() { return 'enum'; }
+ public function type(): string { return 'enum'; }
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
- $keys = ArrayUtil::flattenByKey($this->values, 'key');
- if (is_array($argument)) {
- $ok = true;
- foreach ($argument as $e) {
- if (!in_array($e, $keys)) {
- $ok = false;
+ if ($this->validateArgument()) {
+ $keys = ArrayUtil::flattenByKey($this->values, 'key');
+ if (is_array($argument)) {
+ $ok = true;
+ foreach ($argument as $e) {
+ if (!in_array($e, $keys)) {
+ $ok = false;
+ }
}
+ } else {
+ $ok = in_array($argument, $keys);
+ }
+ if (!$ok) {
+ Message::addError('invalid-enum-item', $this->column, $argument);
+ return '0';
}
- } else {
- $ok = in_array($argument, $keys);
}
- if (!$ok) {
- Message::addError('invalid-enum-item', $this->column, $argument);
- return '0';
+ return parent::whereClause($operator, $argument, $args, $joins);
+ }
+
+ protected function validateArgument(): bool { return true; }
+
+}
+
+class StandbyCrashStatisticsFilter extends EnumStatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct('standbysem', ['NONE', 'MANY']);
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ if ($argument === 'NONE') {
+ $argument = 0;
+ } else { // MANY
+ $argument = 3;
+ $operator = $operator === '=' ? '>' : '<=';
}
return parent::whereClause($operator, $argument, $args, $joins);
}
+ protected function validateArgument(): bool { return false; }
+
}
class DateStatisticsFilter extends StatisticsFilter
@@ -333,12 +415,11 @@ class DateStatisticsFilter extends StatisticsFilter
parent::__construct($column, self::OP_ORDINAL, $placeholder);
}
- public function type() { return 'date'; }
+ public function type(): string { return 'date'; }
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
$key = self::getNewKey($this->column);
- $addendum = '';
if (!preg_match('/^(?<date>\d{4}-\d{2}-\d{2})(\s+(?<h>\d{1,2})(:(?<m>\d{2})(:\d+)?)?)?$/', $argument, $out)) {
Message::addError('invalid-date-format', $argument);
@@ -364,7 +445,7 @@ class DateStatisticsFilter extends StatisticsFilter
$args[$key] = strtotime('+1 ' . $span . ' -1 second', $args[$key]);
}
- return 'm.' . $this->column . ' ' . $operator . ' :' . $key . $addendum;
+ return 'm.' . $this->column . ' ' . $operator . ' :' . $key;
}
}
@@ -377,7 +458,7 @@ class RuntimeStatisticsFilter extends StatisticsFilter
parent::__construct('lastboot', self::OP_ORDINAL);
}
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
$upper = time() - (int)$argument * 3600;
$lower = $upper - 3600;
@@ -401,7 +482,7 @@ class RuntimeStatisticsFilter extends StatisticsFilter
abstract class GbToMbRangeStatisticsFilter extends StatisticsFilter
{
- protected function rangeClause(string $operator, $argument, array $fuzzyVals)
+ protected function rangeClause(string $operator, $argument, array $fuzzyVals): string
{
if ($operator === '~' || $operator === '!~') {
$lower = (int)floor(StatisticsFilter::findBestValue($fuzzyVals, (int)$argument, false) * 1024 - 500);
@@ -434,24 +515,24 @@ class RamGbStatisticsFilter extends GbToMbRangeStatisticsFilter
parent::__construct('mbram', self::OP_FUZZY_ORDINAL, 'GiB');
}
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
return parent::rangeClause($operator, $argument, self::SIZE_RAM);
}
}
-class Id44GbStatisticsFilter extends GbToMbRangeStatisticsFilter
+class PartitionGbStatisticsFilter extends GbToMbRangeStatisticsFilter
{
- public function __construct()
+ public function __construct(string $column)
{
- parent::__construct('id44mb', self::OP_FUZZY_ORDINAL,'GiB');
+ parent::__construct($column, self::OP_FUZZY_ORDINAL, 'GiB');
}
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
- return parent::rangeClause($operator, $argument, self::SIZE_ID44);
+ return parent::rangeClause($operator, $argument, self::SIZE_PARTITION);
}
}
@@ -463,18 +544,17 @@ class StateStatisticsFilter extends EnumStatisticsFilter
parent::__construct('state', ['on', 'off', 'idle', 'occupied', 'standby']);
}
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
$map = [ 'on' => ['IDLE', 'OCCUPIED'], 'off' => ['OFFLINE'], 'idle' => ['IDLE'], 'occupied' => ['OCCUPIED'], 'standby' => ['STANDBY'] ];
- $neg = $operator == '!=' ? 'NOT ' : '';
+ $neg = $operator === '!=' ? 'NOT ' : '';
if (array_key_exists($argument, $map)) {
$key = StatisticsFilter::getNewKey($this->column);
$args[$key] = $map[$argument];
return " m.state $neg IN ( :$key ) ";
- } else {
- Message::addError('invalid-filter-argument', 'state', $argument);
- return ' 1';
}
+ Message::addError('invalid-filter-argument', 'state', $argument);
+ return ' 1';
}
}
@@ -493,15 +573,15 @@ class LocationStatisticsFilter extends EnumStatisticsFilter
parent::__construct('locationid', $locs, self::OP_LOCATIONS);
}
- public function type() { return 'enum'; }
+ public function type(): string { return 'enum'; }
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
$recursive = (substr($operator, -1) === '~');
$operator = str_replace('~', '=', $operator);
if ($recursive && is_array($argument)) {
- Util::traceError('Cannot use ~ operator for location with array');
+ ErrorHandler::traceError('Cannot use ~ operator for location with array');
}
if ($recursive) {
$argument = array_keys(Location::getRecursiveFlat($argument));
@@ -539,21 +619,13 @@ class IpStatisticsFilter extends StatisticsFilter
} elseif (strpos($argument, '/') !== false) {
// TODO: IPv6 CIDR
$range = IpUtil::parseCidr($argument);
- if ($range === false) {
+ if ($range === null) {
Message::addError('invalid-cidr-notion', $argument);
return '0';
}
return 'INET_ATON(clientip) BETWEEN ' . $range['start'] . ' AND ' . $range['end'];
} elseif (($num = substr_count($argument, ':')) !== 0 && $num <= 7) {
- // IPv6, not yet in DB but let's prepare
- if ($num > 7 || strpos($argument, '::') !== false) { // Too many :, or invalid compressed format
- Message::addError('invalid-ip-address', $argument);
- return '0';
- } elseif ($num <= 7 && substr($argument, -1) === ':') {
- $argument .= '*';
- } elseif ($num < 7) {
- $argument .= ':*';
- }
+ // TODO: Probably valid IPv6, not yet in DB
} elseif (($num = substr_count($argument, '.')) !== 0 && $num <= 3) {
if (substr($argument, -1) === '.') {
$argument .= '*';
@@ -564,7 +636,8 @@ class IpStatisticsFilter extends StatisticsFilter
Message::addError('invalid-ip-address', $argument);
return '0';
}
- return "clientip LIKE '" . str_replace('*', '%', $argument) . "'";
+ $operator = $operator[0] === '!' ? 'NOT LIKE' : 'LIKE';
+ return "clientip $operator '" . str_replace('*', '%', $argument) . "'";
}
}
@@ -576,18 +649,170 @@ class IsClientStatisticsFilter extends StatisticsFilter
parent::__construct(null, []);
}
- public function whereClause(string $operator, $argument, array &$args, array &$joins)
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
{
if ($argument) {
- $joins[] = ' LEFT JOIN runmode USING (machineuuid)';
+ $joins[] = ' LEFT JOIN runmode ON (m.machineuuid = runmode.machineuuid)';
return "(runmode.isclient <> 0 OR runmode.isclient IS NULL)";
}
- $joins[] = ' INNER JOIN runmode USING (machineuuid)';
+ $joins[] = ' INNER JOIN runmode ON (m.machineuuid = runmode.machineuuid)';
return "runmode.isclient = 0";
}
}
+class PciDeviceStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, ['='], 'vvvv[:dddd][,cccc]');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ // vendor[:device][,class]
+ if (!preg_match('/^(?<v>[0-9a-f]{4})(?::(?<d>[0-9a-f]{4}))?(?:,(?<c>[0-9a-f]{4}))?$/i', $argument, $out)) {
+ Message::addError('invalid-pciid', $argument);
+ return '0';
+ }
+ $vendor = $out['v'];
+ $device = $out['d'] ?? '';
+ $class = $out['c'] ?? '';
+ // basic join for hw_x_machine
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::PCI_DEVICE);
+ $_ = [];
+ $c = new HardwareQueryColumn(true, 'vendor');
+ $c->addCondition($operator, $vendor);
+ $c->generate($joins, $_, $args, [], $shw);
+ if (!empty($device)) {
+ $c = new HardwareQueryColumn(true, 'device');
+ $c->addCondition($operator, $device);
+ $c->generate($joins, $_, $args, [], $shw);
+ }
+ if (!empty($class)) {
+ $c = new HardwareQueryColumn(true, 'class');
+ $c->addCondition($operator, $class);
+ $c->generate($joins, $_, $args, [], $shw);
+ }
+ return '1';
+ }
+
+}
+
+class NicSpeedStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, StatisticsFilter::OP_ORDINAL, 'MBit/s');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::MAINBOARD);
+ $_ = [];
+ $c = new HardwareQueryColumn(false, 'nic-speed');
+ $c->addCondition($operator, $argument);
+ $c->generate($joins, $_, $args, [], $shw);
+ return '1';
+ }
+
+}
+
+class HddRpmStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, StatisticsFilter::OP_ORDINAL, '7200');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::HDD);
+ $_ = [];
+ $c = new HardwareQueryColumn(true, 'rotation_rate');
+ $c->addCondition($operator, $argument);
+ $c->generate($joins, $_, $args, [], $shw);
+ return '1';
+ }
+
+}
+
+class SystemModelStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, StatisticsFilter::OP_STRCMP, 'PC-365 (IBM)');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins, HardwareInfo::DMI_SYSTEM);
+ $_ = [];
+ $manufacturer = null;
+ $model = $argument;
+ if (preg_match('/^(.*)\((.*)\)\s*$/', $model, $out)) {
+ $manufacturer = trim($out[2]);
+ $model = trim($out[1]);
+ }
+ $c = new HardwareQueryColumn(true, 'Product Name');
+ $c->addCondition($operator, $model);
+ $c->generate($joins, $_, $args, [], $shw);
+ if ($manufacturer !== null) {
+ $c = new HardwareQueryColumn(true, 'Manufacturer');
+ $c->addCondition($operator, $manufacturer);
+ $c->generate($joins, $_, $args, [], $shw);
+ }
+ return '1';
+ }
+
+}
+
+class MacAddressStatisticsFilter extends SimpleStatisticsFilter
+{
+ public function __construct()
+ {
+ parent::__construct('macaddr', self::OP_STRCMP, '11-22-33-44-55-66');
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ // Allow just 12 hex digits, and convert ':' to '-', which we unfortunately settled on for the DB format
+ if (preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i',
+ $argument, $out)) {
+ $argument = $out[1] . '-' . $out[2] . '-' . $out[3] . '-' . $out[4] . '-' . $out[5] . '-' . $out[6];
+ } elseif (strpos($argument, ':') !== false) {
+ $argument = str_replace(':', '-', $argument);
+ }
+ return parent::whereClause($operator, $argument, $args, $joins);
+ }
+}
+
+class AnyHardwarePropStatisticsFilter extends StatisticsFilter
+{
+
+ public function __construct()
+ {
+ parent::__construct(null, ['~']);
+ }
+
+ public function whereClause(string $operator, $argument, array &$args, array &$joins): string
+ {
+ $shw = StatisticsFilter::addHardwareJoin($args, $joins);
+ $val = self::getNewKey('val');
+ $key1 = self::getNewKey('hw');
+ $joins[] = "LEFT JOIN statistic_hw_prop $key1 ON (`$key1`.`value` LIKE :$val AND `$key1`.hwid = `$shw`.hwid)";
+ $key2 = self::getNewKey('hw');
+ $joins[] = "LEFT JOIN machine_x_hw_prop $key2 ON (`$key2`.`value` LIKE :$val AND `$key2`.machinehwid = mxhw.machinehwid)";
+ $args[$val] = '%' . str_replace(['%', '*'], ['_', '%'], $argument) . '%';
+ return "((`$key1`.`value` IS NOT NULL) OR (`$key2`.`value` IS NOT NULL))";
+ }
+
+}
+
class DatabaseFilter
{
/** @var StatisticsFilter
@@ -595,6 +820,10 @@ class DatabaseFilter
private $inst;
public $op;
public $argument;
+
+ /**
+ * Called by StatisticsFilter::bind().
+ */
public function __construct(StatisticsFilter $inst, string $op, $argument)
{
$inst->validateOperator($op);
@@ -602,16 +831,25 @@ class DatabaseFilter
$this->op = $op;
$this->argument = $argument;
}
- public function whereClause(array &$args, array &$joins)
+
+ /**
+ * Called from StatisticsFilterSet::makeFragments() to build the final query.
+ */
+ public function whereClause(array &$args, array &$joins): string
{
return $this->inst->whereClause($this->op, $this->argument, $args, $joins);
}
- public function isClass($what)
+ public function isClass(string $what): bool
{
return get_class($this->inst) === $what;
}
+ public function getClass(): string
+ {
+ return get_class($this->inst);
+ }
+
}
StatisticsFilter::initConstants();
diff --git a/modules-available/statistics/inc/statisticsfilterset.inc.php b/modules-available/statistics/inc/statisticsfilterset.inc.php
index a38f9d3f..26595e93 100644
--- a/modules-available/statistics/inc/statisticsfilterset.inc.php
+++ b/modules-available/statistics/inc/statisticsfilterset.inc.php
@@ -9,7 +9,10 @@ class StatisticsFilterSet
private $cache = false;
- public function __construct($filters)
+ /**
+ * @param DatabaseFilter[] $filters
+ */
+ public function __construct(array $filters)
{
$this->filters = $filters;
}
@@ -37,16 +40,10 @@ class StatisticsFilterSet
$join = implode(' ', array_unique($joins));
$this->cache = compact('where', 'join', 'args');
}
-
- public function isNoId44Filter()
- {
- $filter = $this->hasFilter('Id44GbStatisticsFilter');
- return $filter !== false && $filter->argument == 0;
- }
public function filterNonClients()
{
- if (Module::get('runmode') === false || $this->hasFilter('IsClientStatisticsFilter') !== false)
+ if (Module::get('runmode') === false || $this->hasFilter('IsClientStatisticsFilter') !== null)
return;
$this->cache = false;
// Runmode module exists, add filter
@@ -55,27 +52,27 @@ class StatisticsFilterSet
/**
* @param string $type filter type (class name)
- * @return false|DatabaseFilter The filter, false if not found
+ * @return ?DatabaseFilter The filter, null if not found
*/
- public function hasFilter($type)
+ public function hasFilter(string $type): ?DatabaseFilter
{
foreach ($this->filters as $filter) {
if ($filter->isClass($type)) {
return $filter;
}
}
- return false;
+ return null;
}
/**
* @param string $type filter type key/id
- * @return false|DatabaseFilter The filter, false if not found
+ * @return ?DatabaseFilter The filter, null if not found
*/
- public function hasFilterKey($type)
+ public function hasFilterKey(string $type): ?DatabaseFilter
{
if (isset($this->filters[$type]))
return $this->filters[$type];
- return false;
+ return null;
}
/**
@@ -85,7 +82,7 @@ class StatisticsFilterSet
* @param string $permission permission to use
* @return bool false if no permission for any location, true otherwise
*/
- public function setAllowedLocationsFromPermission($permission)
+ public function setAllowedLocationsFromPermission(string $permission): bool
{
if (!Module::isAvailable('locations'))
return true;
@@ -108,9 +105,35 @@ class StatisticsFilterSet
*/
public function getAllowedLocations()
{
- if (isset($this->filters['permissions']->argument) && is_array($this->filters['permissions']->argument))
- return $this->filters['permissions']->argument;
+ if (isset($this->filters['permissions']) && is_array($this->filters['permissions']->argument))
+ return (array)$this->filters['permissions']->argument;
return false;
}
+ public function suitableForUsageGraph(): bool
+ {
+ foreach ($this->filters as $filter) {
+ switch ($filter->getClass()) {
+ case 'LocationStatisticsFilter':
+ case 'IsClientStatisticsFilter':
+ break;
+ case 'DateStatisticsFilter':
+ if ($filter->op !== '>' && $filter->op !== '>=')
+ return false;
+ if (strtotime($filter->argument) + 3*86400 > time())
+ return false;
+ break;
+ case 'RuntimeStatisticsFilter':
+ if ($filter->op !== '>' && $filter->op !== '>=')
+ return false;
+ if ($filter->argument < 3 * 24)
+ return false;
+ break;
+ default:
+ return false;
+ }
+ }
+ return true;
+ }
+
}
diff --git a/modules-available/statistics/inc/statisticshooks.inc.php b/modules-available/statistics/inc/statisticshooks.inc.php
index 746bdabf..6b9dfa21 100644
--- a/modules-available/statistics/inc/statisticshooks.inc.php
+++ b/modules-available/statistics/inc/statisticshooks.inc.php
@@ -5,7 +5,7 @@ class StatisticsHooks
private static $row = false;
- private static function getRow($machineuuid)
+ private static function getRow(string $machineuuid)
{
if (self::$row !== false)
return;
@@ -13,15 +13,22 @@ class StatisticsHooks
['machineuuid' => $machineuuid]);
}
- public static function getBaseconfigName($machineuuid)
+ /**
+ * Hook for baseconfig.
+ * @return false|string Client name, or false if invalid
+ */
+ public static function getBaseconfigName(string $machineuuid)
{
self::getRow($machineuuid);
if (self::$row === false)
return false;
- return self::$row['hostname'] ? self::$row['hostname'] : self::$row['clientip'];
+ return self::$row['hostname'] ?: self::$row['clientip'];
}
- public static function baseconfigLocationResolver($machineuuid)
+ /**
+ * Hook for baseconfig.
+ */
+ public static function baseconfigLocationResolver(string $machineuuid): int
{
self::getRow($machineuuid);
if (self::$row === false)
@@ -30,16 +37,17 @@ class StatisticsHooks
}
/**
- * Hook to get inheritance tree for all config vars
- * @param int $machineuuid MachineUUID currently being edited
+ * Hook to get inheritance tree for all config vars.
+ *
+ * @param string $machineuuid MachineUUID currently being edited
*/
- public static function baseconfigInheritance($machineuuid)
+ public static function baseconfigInheritance(string $machineuuid): array
{
self::getRow($machineuuid);
if (self::$row === false)
return [];
BaseConfig::prepareWithOverrides([
- 'locationid' => self::$row['locationid']
+ 'locationid' => self::$row['locationid'] ?? 0
]);
return ConfigHolder::getRecursiveConfig(true);
}
diff --git a/modules-available/statistics/inc/statisticsstyling.inc.php b/modules-available/statistics/inc/statisticsstyling.inc.php
index 1fd1d326..0e158026 100644
--- a/modules-available/statistics/inc/statisticsstyling.inc.php
+++ b/modules-available/statistics/inc/statisticsstyling.inc.php
@@ -3,19 +3,19 @@
class StatisticsStyling
{
- public static function ramColorClass($mb)
+ public static function ramColorClass(int $mb): string
{
- if ($mb < 1500) {
+ if ($mb < 2500) {
return 'danger';
}
- if ($mb < 2500) {
+ if ($mb < 5100) {
return 'warning';
}
return '';
}
- public static function kvmColorClass($state)
+ public static function kvmColorClass(string $state): string
{
if ($state === 'DISABLED') {
return 'danger';
@@ -27,7 +27,7 @@ class StatisticsStyling
return '';
}
- public static function hddColorClass($gb)
+ public static function hddColorClass(int $gb): string
{
if ($gb < 7) {
return 'danger';
@@ -39,4 +39,24 @@ class StatisticsStyling
return '';
}
+ /**
+ * Take a machine state enum value, return a matching glyphicon class.
+ * @param string $state State value (OFFLINE, IDLE, ...)
+ */
+ public static function machineStateToIcon(string $state): string
+ {
+ switch ($state) {
+ case 'OFFLINE':
+ return 'glyphicon-off';
+ case 'IDLE':
+ return 'glyphicon-ok green';
+ case 'OCCUPIED':
+ return 'glyphicon-user red';
+ case 'STANDBY':
+ return 'glyphicon-off green';
+ default:
+ return 'glyphicon-question-sign';
+ }
+ }
+
} \ No newline at end of file
diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php
index 3becce8f..bc8a5c91 100644
--- a/modules-available/statistics/install.inc.php
+++ b/modules-available/statistics/install.inc.php
@@ -11,7 +11,7 @@ $res[] = tableCreate('statistic', "
`clientip` varchar(40) NOT NULL,
`machineuuid` char(36) CHARACTER SET ascii DEFAULT NULL,
`username` varchar(30) NOT NULL,
- `data` varchar(255) NOT NULL,
+ `data` BLOB NOT NULL,
PRIMARY KEY (`logid`),
KEY `dateline` (`dateline`),
KEY `logtypeid` (`typeid`,`dateline`),
@@ -40,8 +40,10 @@ $res[] = tableCreate('machine', "
`cpumodel` varchar(120) NOT NULL,
`systemmodel` varchar(120) NOT NULL DEFAULT '',
`id44mb` int(10) unsigned NOT NULL,
+ `id45mb` int(10) unsigned NOT NULL DEFAULT 0,
`badsectors` int(10) unsigned NOT NULL,
- `data` mediumtext NOT NULL,
+ `data` mediumblob NOT NULL,
+ `dataparsetime` int(10) unsigned NOT NULL DEFAULT 0,
`hostname` varchar(200) NOT NULL DEFAULT '',
`currentsession` varchar(120) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
`currentuser` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
@@ -64,7 +66,7 @@ $res[] = $machineHwCreate = tableCreate('machine_x_hw', "
`machinehwid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`hwid` int(10) unsigned NOT NULL,
`machineuuid` char(36) CHARACTER SET ascii NOT NULL,
- `devpath` char(50) CHARACTER SET ascii NOT NULL,
+ `devpath` char(32) CHARACTER SET ascii NOT NULL,
`disconnecttime` int(10) unsigned NOT NULL COMMENT 'time the device was not connected to the pc anymore for the first time, 0 if it is connected',
PRIMARY KEY (`machinehwid`),
UNIQUE KEY `hwid` (`hwid`,`machineuuid`,`devpath`),
@@ -74,23 +76,25 @@ $res[] = $machineHwCreate = tableCreate('machine_x_hw', "
$res[] = tableCreate('machine_x_hw_prop', "
`machinehwid` int(10) unsigned NOT NULL,
- `prop` char(16) CHARACTER SET ascii NOT NULL,
+ `prop` varchar(64) CHARACTER SET ascii NOT NULL,
`value` varchar(500) NOT NULL,
+ `numeric` bigint(20) DEFAULT NULL,
PRIMARY KEY (`machinehwid`,`prop`)
");
$res[] = tableCreate('statistic_hw', "
`hwid` int(10) unsigned NOT NULL AUTO_INCREMENT,
- `hwtype` char(11) CHARACTER SET ascii NOT NULL,
- `hwname` varchar(200) NOT NULL,
+ `hwtype` char(16) CHARACTER SET ascii NOT NULL,
+ `hwname` char(32) CHARACTER SET ascii NOT NULL,
PRIMARY KEY (`hwid`),
UNIQUE KEY `hwtype` (`hwtype`,`hwname`)
");
$res[] = tableCreate('statistic_hw_prop', "
`hwid` int(10) unsigned NOT NULL,
- `prop` char(16) CHARACTER SET ascii NOT NULL,
+ `prop` varchar(64) CHARACTER SET ascii NOT NULL,
`value` varchar(500) NOT NULL,
+ `numeric` bigint(20) DEFAULT NULL,
PRIMARY KEY (`hwid`,`prop`)
");
@@ -248,7 +252,7 @@ if (!tableHasColumn('machine', 'live_tmpsize')) {
// 2019-02-20: Convert bogus UUIDs
$res2 = Database::simpleQuery("SELECT machineuuid, macaddr FROM machine WHERE machineuuid LIKE '00000000000000_-%'");
-while ($row = $res2->fetch(PDO::FETCH_ASSOC)) {
+foreach ($res2 as $row) {
$new = strtoupper('baad1d00-9491-4716-b98b-' . preg_replace('/[^0-9a-f]/i', '', $row['macaddr']));
error_log('Replacing ' . $row['machineuuid'] . ' with ' . $new);
if (strlen($new) === 36) {
@@ -299,5 +303,69 @@ if (!tableHasColumn('machine', 'live_id45size')) {
$res[] = UPDATE_DONE;
}
+// 2021-08-19 Enhanced machine property indexing
+if (stripos(tableColumnType('statistic_hw_prop', 'prop'), 'varchar(64)') === false) {
+ Database::exec("DELETE FROM statistic_hw_prop WHERE prop NOT REGEXP BINARY '^[a-zA-Z0-9_ =@*!.:/\\\\-]+$'");
+ $ret = Database::exec("ALTER TABLE statistic_hw_prop
+ MODIFY `prop` varchar(64) CHARACTER SET ascii NOT NULL,
+ ADD `numeric` bigint(20) DEFAULT NULL");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Changing prop of statistic_hw_prop failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+if (stripos(tableColumnType('machine_x_hw_prop', 'prop'), 'varchar(64)') === false) {
+ Database::exec("DELETE FROM machine_x_hw_prop WHERE prop NOT REGEXP BINARY '^[a-zA-Z0-9_ =@*!.:/\\\\-]+$'");
+ $ret = Database::exec("ALTER TABLE machine_x_hw_prop
+ MODIFY `prop` varchar(64) CHARACTER SET ascii NOT NULL,
+ ADD `numeric` bigint(20) DEFAULT NULL");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Changing prop of machine_x_hw_prop failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+if (stripos(tableColumnType('statistic_hw', 'hwname'), 'char(32)') === false) {
+ Database::exec("DELETE FROM statistic_hw WHERE hwname NOT REGEXP BINARY '^[a-zA-Z0-9_ =@*!.:/\\\\-]+$'");
+ $ret = Database::exec("ALTER TABLE statistic_hw MODIFY `hwname` char(32) CHARACTER SET ascii NOT NULL,
+ MODIFY `hwtype` char(16) CHARACTER SET ascii NOT NULL");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Changing hwname/hwtype of statistic_hw failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+if (stripos(tableColumnType('machine_x_hw', 'devpath'), 'char(32)') === false) {
+ Database::exec("DELETE FROM machine_x_hw WHERE devpath NOT REGEXP BINARY '^[a-zA-Z0-9_ =@*!.:/\\\\-]+$'");
+ $ret = Database::exec("ALTER TABLE machine_x_hw MODIFY `devpath` char(32) CHARACTER SET ascii NOT NULL");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Changing devpath of machine_x_hw failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+if (!tableHasColumn('machine', 'dataparsetime')) {
+ $ret = Database::exec("ALTER TABLE `machine`
+ ADD COLUMN `dataparsetime` int(10) unsigned NOT NULL DEFAULT '0' AFTER `data`");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding dateparsetime column to machine table failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+if (!tableHasColumn('machine', 'id45mb')) {
+ $ret = Database::exec("ALTER TABLE `machine`
+ ADD COLUMN `id45mb` int(10) unsigned NOT NULL DEFAULT 0 AFTER `id44mb`");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Adding id45mb column to machine table failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+
+// 2022-11-22 Change data column of statistic table from varchar(255) to blob
+if (stripos(tableColumnType('statistic', 'data'), 'blob') === false) {
+ $ret = Database::exec("ALTER TABLE `statistic` MODIFY COLUMN `data` BLOB NOT NULL");
+ if ($ret === false) {
+ finalResponse(UPDATE_FAILED, 'Changing statistic.data to blob failed: ' . Database::lastError());
+ }
+ $res[] = UPDATE_DONE;
+}
+
// Create response
responseFromArray($res);
diff --git a/modules-available/statistics/lang/de/filters.json b/modules-available/statistics/lang/de/filters.json
index 3fe97532..ef423daa 100644
--- a/modules-available/statistics/lang/de/filters.json
+++ b/modules-available/statistics/lang/de/filters.json
@@ -6,19 +6,24 @@
"firstseen": "Erster Boot",
"gbram": "RAM (GB)",
"hddgb": "ID44 (GB)",
+ "hddrpm": "HDD U\/min",
"hostname": "Hostname",
"kvmstate": "Virtualisierung",
"lastboot": "Letzter Boot",
"lastseen": "Letzte Aktivit\u00e4t",
+ "live_id45free": "ID45 frei (MB)",
"live_memfree": "RAM frei (MB)",
"live_swapfree": "swap frei (MB)",
"live_tmpfree": "ID44 frei (MB)",
"location": "Raum\/Ort",
- "logintime": "Letzter Login",
"macaddr": "MAC-Adresse",
"machineuuid": "System-UUID",
+ "nicspeed": "NIC-Geschwindigkeit",
+ "pcidev": "PCI-Ger\u00e4t",
+ "persistentgb": "ID45 (GB)",
"realcores": "CPU-Kerne (real)",
"runtime": "Laufzeit (Stunden)",
+ "standbycrash": "Crashes im Standby",
"state": "Zustand",
"systemmodel": "System-Modell"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/de/messages.json b/modules-available/statistics/lang/de/messages.json
index e1688cbf..023dac4c 100644
--- a/modules-available/statistics/lang/de/messages.json
+++ b/modules-available/statistics/lang/de/messages.json
@@ -2,12 +2,14 @@
"cleared-n-machines": "{{0}} Clients zur\u00fcckgesetzt",
"deleted-n-machines": "{{0}} Clients gel\u00f6scht",
"ignored-both-in-use": "Rechnerpaar ignoriert, da beide noch in Betrieb zu sein scheinen. ({{0}} und {{1}})",
+ "ignored-no-permission": "{{0}} wurde ignoriert: Keine Berechtigung",
"invalid-cidr-notion": "Ung\u00fcltiges CIDR-Format: {{0}}",
"invalid-date-format": "Ung\u00fcltige Datumsangabe: {{0}}",
"invalid-enum-item": "Die Auswahl {{1}} ist ung\u00fcltig f\u00fcr {{0}}",
"invalid-filter-argument": "Das Argument {{1}} ist nicht g\u00fcltig f\u00fcr den Filter {{0}}",
"invalid-filter-key": "{{0}} ist kein g\u00fcltiges Filterkriterium",
"invalid-ip-address": "Ung\u00fcltige IP-Adresse: {{0}}",
+ "invalid-pciid": "Ung\u00fcltige PCI-ID {{0}}",
"invalid-replace-format": "Ung\u00fcltiges Parameterformat ({{0}})",
"no-replacement-matches": "Keine Rechner gefunden, die den oben genannten Kriterien entsprechen",
"notes-saved": "Anmerkungen gespeichert",
diff --git a/modules-available/statistics/lang/de/module.json b/modules-available/statistics/lang/de/module.json
index 902a9573..23fc52df 100644
--- a/modules-available/statistics/lang/de/module.json
+++ b/modules-available/statistics/lang/de/module.json
@@ -1,5 +1,10 @@
{
+ "location-column-header-count": "Rechner",
+ "location-column-header-load": "Besetzt",
"module_name": "Client-Statistiken",
"page_title": "Client-Statistiken",
+ "submenu_hints": "Hinweise",
+ "submenu_projectors": "Beamer",
+ "submenu_replace": "Rechner ersetzen",
"unused": "Ungenutzt"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/de/permissions.json b/modules-available/statistics/lang/de/permissions.json
index cd1e7a4e..7e5c880d 100644
--- a/modules-available/statistics/lang/de/permissions.json
+++ b/modules-available/statistics/lang/de/permissions.json
@@ -1,10 +1,12 @@
{
"hardware.projectors.edit": "Beamerzuweisung bearbeiten",
"hardware.projectors.view": "Beamerzuweisung anzeigen",
+ "hints": "Hinweise",
"machine.delete": "Rechner l\u00f6schen.",
"machine.note.edit": "Anmerkungen bearbeiten",
"machine.note.view": "Anmerkungen anzeigen",
"machine.view-details": "Clientinformationen anzeigen",
+ "replace": "Ersetzen",
"view.list": "Clientliste anzeigen",
"view.summary": "Visualisierung anzeigen"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/de/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index 43665a78..064805c2 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -1,13 +1,16 @@
{
"lang_64bitSupport": "64\u2009Bit Gast-Support",
+ "lang_MbitPerSecond": "MBit\/s",
"lang_address": "Adresse",
"lang_apply": "Anwenden",
+ "lang_baseSystem": "Grundsystem",
"lang_biosDate": "Ver\u00f6ffentlichungsdatum",
"lang_biosFixes": "BIOS-Fehlerkorrekturen",
"lang_biosUpdate": "BIOS Update",
"lang_biosUpdateLink": "Zur Herstellerseite",
"lang_biosVersion": "BIOS-Version",
"lang_bootedWithoutAnyRunmode": "Ohne besonderen Betriebsmodus gestartet",
+ "lang_boottimeTooltip": "Bootzeit, Kernel-Init bis Login-Screen",
"lang_clientInDifferentRunmode": "Aktueller Betriebsmodus",
"lang_clientList": "Liste ausgew\u00e4hlter Rechner",
"lang_configVars": "Konfigurationsvariablen",
@@ -19,17 +22,26 @@
"lang_cputemp": "CPU-Temperatur",
"lang_details": "Details",
"lang_devices": "Ger\u00e4te",
+ "lang_duplex": "Duplex",
"lang_duration": "Dauer",
"lang_event": "Ereignis",
"lang_eventType": "Typ",
"lang_firstSeen": "Erste Aktivit\u00e4t",
"lang_free": "frei",
+ "lang_fullInfo": "Alle Werte",
"lang_gbRam": "RAM",
+ "lang_graphLectureTitle": "Blaue F\u00e4rbung = Anzahl der Veranstaltungen",
"lang_hardwareSummary": "Hardware",
"lang_hasNotes": "Zu diesem Rechner wurden Notizen hinterlegt",
+ "lang_hddUnused": "Ungenutzter Festplattenspeicher",
+ "lang_hddUnusedId44": "Mit unpartitioniertem Speicherbereich auf HDD\/SSD, und ohne ID44",
+ "lang_hddUnusedId45": "Mit unpartitioniertem Speicherbereich auf einer SSD, und ohne ID45",
"lang_hdds": "Festplatten",
"lang_hostname": "Hostname",
+ "lang_id44size": "ID44 Gr\u00f6\u00dfe",
+ "lang_id45size": "ID45 Gr\u00f6\u00dfe",
"lang_inUseMachines": "In Verwendung",
+ "lang_installedCountMax": "Slots belegt \/ frei",
"lang_ip": "IP-Adresse",
"lang_knownMachines": "Bekannte Clients",
"lang_kvmState": "Status",
@@ -38,6 +50,8 @@
"lang_labelFilter": "Aktive Filter (UND-Logik)",
"lang_lastBoot": "Letzter Boot",
"lang_lastSeen": "Zuletzt gesehen",
+ "lang_legacyCpuVmx": "Veraltete CPU (VMware)",
+ "lang_legacyCpuVmxText": "Diese Rechner haben eine CPU, die von neueren VMware-Versionen nicht mehr unterst\u00fctzt wird. Um VMware-VMs auf diesen Rechnern zu nutzen, m\u00fcssen diese mit einem Grundsystem betrieben werden, welches noch den VMware Player 12.5.x enth\u00e4lt, z.B. 30r1. Bedenken Sie jedoch, dass \u00e4ltere Grundsysteme neuere bwLehrpool-Funktionen nicht enthalten, und somit in ihrer Funktionalit\u00e4t eingeschr\u00e4nkt sein k\u00f6nnen.",
"lang_listDropdown": "Als Text",
"lang_location": "Ort",
"lang_logHeadline": "Logging",
@@ -50,6 +64,7 @@
"lang_machineOff": "Der Rechner ist ausgeschaltet, oder hat kein bwLehrpool gebootet",
"lang_machineStandby": "Im Standby",
"lang_machineSummary": "Zusammenfassung",
+ "lang_manufacturer": "Hersteller",
"lang_maximumAbbrev": "Max.",
"lang_mediaIntegrityErrors": "\"Media Integrity Errors\"",
"lang_memoryStats": "Arbeitsspeicher",
@@ -61,6 +76,10 @@
"lang_moduleHeading": "Client-Statistiken",
"lang_more": "Mehr",
"lang_newMachines": "Neue Ger\u00e4te",
+ "lang_nicDuplex": "Duplex",
+ "lang_nicSlowSpeed": "Langsame Netzwerkkarte",
+ "lang_nicSlowSpeedText": "Diese Rechner sind mit weniger als Gigabit Ethernet mit dem Netzwerk verbunden. Bootvorg\u00e4nge und VM-Starts k\u00f6nnen sp\u00fcrbar verlangsamt sein. Wenn sich die Anbindung dieser Rechner nicht verbessern l\u00e4sst, versuchen Sie, eine ID45-Partition auf diesen Rechnern einzurichten, und lokales Caching in den Konfigurationsvariablen zu aktivieren, um die Performance von konsekutiven Bootvorg\u00e4ngen und VM-Starts zu verbessern.",
+ "lang_nicSpeed": "Geschwindigkeit",
"lang_noEdid": "Kein EDID",
"lang_noProjectorsDefined": "Keine Beamer-Overrides definiert",
"lang_notes": "Anmerkungen",
@@ -72,12 +91,18 @@
"lang_partitionSize": "Gr\u00f6\u00dfe",
"lang_pcmodel": "PC-Modell",
"lang_pendingSectors": "Potentiell defekte Sektoren",
+ "lang_persistentPart": "Persistent",
+ "lang_persistentPartID": "ID45",
"lang_powerOnTime": "Betriebszeit",
"lang_projector": "Beamer",
"lang_projectors": "Beamer",
"lang_ram": "Arbeitsspeicher",
"lang_ramSize": "Gr\u00f6\u00dfe",
- "lang_ramSlots": "Speicher-Slots",
+ "lang_ramSizeCurrentMax": "Gr\u00f6\u00dfe \/ Maximal",
+ "lang_ramUnderclocked": "Unter Maximaltakt laufener RAM",
+ "lang_ramUnderclockedText": "Dies sind Rechner mit Speicherriegeln, die unter ihrem Maximaltakt laufen. Entweder l\u00e4sst sich die Leistung des Rechners steigern, indem im BIOS der Takt angehoben wird, oder die Speicherriegel k\u00f6nnen mit anderen Rechnern getauscht werden, um eine homogene Best\u00fcckung zu erreichen.",
+ "lang_ramUpgrade": "RAM aufr\u00fcsten",
+ "lang_ramUpgradeText": "Die folgenden Rechner haben wenig RAM. F\u00fcr den Betrieb mit VMs wird das Aufr\u00fcsten des Speichers empfohlen.",
"lang_realCores": "Kerne",
"lang_reallocatedSectors": "Defekte Sektoren",
"lang_reboot": "Neustart",
@@ -85,32 +110,43 @@
"lang_rebootKexecCheck": "Schneller Reboot direkt in bwLehrpool (kexec)",
"lang_remoteActions": "Ferngesteuerte Aktionen",
"lang_remoteExec": "Befehl Ausf\u00fchren",
+ "lang_remoteSpeedcheck": "Geschwindigkeitstest (Netzwerk)",
"lang_replace": "Ersetzen",
"lang_replaceInstructions": "Hier k\u00f6nnen Sie Metadaten automatisch \u00fcbertragen, wenn in einem Raum die Rechner ausgetauscht wurden. Dies setzt voraus, dass alle neuen Rechner die gleiche IP Adresse erhalten haben wie der Rechner, der zuvor am entsprechenden Platz stand, und die neuen Rechner alle einmal gestartet wurden. In der Liste unten sehen Sie alle Rechnerpaare, auf die folgendes zutrifft: 1) Die IP-Adressen sind identisch 2) Der letzte Boot des einen Rechners liegt vor dem ersten Boot des anderen Rechners. W\u00e4hlen Sie alle Rechnerpaare aus, f\u00fcr die eine Ersetzung stattfinden soll. Bei der Ersetzung werden alle Logeintr\u00e4ge, Sitzungslogs, Position im Raumplan und evtl. spezielle Betriebsmodi vom alten Rechner auf den neuen \u00dcbertragen.",
"lang_replaceMachinesHeading": "Rechner ersetzen",
- "lang_replaceNew": "Alter Rechner",
- "lang_replaceOld": "Neuer Rechner",
+ "lang_replaceNew": "Neuer Rechner",
+ "lang_replaceOld": "Alter Rechner",
"lang_resetClearIp": "IP-Adresse zur\u00fccksetzen",
"lang_roomplan": "Raumplan",
"lang_runMode": "Betriebsmodus",
"lang_runmodeMachines": "Mit besonderem Betriebsmodus",
"lang_screens": "Bildschirme",
+ "lang_selectColumns": "Spalten ein\/ausblenden",
"lang_serialNo": "Serien-Nr",
"lang_showList": "Liste",
"lang_showVisualization": "Visualisierung",
"lang_shutdown": "Herunterfahren",
"lang_shutdownConfirm": "Ausgew\u00e4hlte Rechner wirklich herunterfahren?",
+ "lang_slot": "Slot",
+ "lang_slots": "Slots",
+ "lang_smartSelfTestFailed": "SMART Status FAILED",
"lang_sockets": "Sockel",
+ "lang_speed": "Geschwindigkeit",
+ "lang_speedCurrent": "Aktuelle Geschwindigkeit",
+ "lang_speedDesign": "Maximalgeschwindigkeit",
"lang_sureClearIp": "Die IP-Adresse der ausgew\u00e4hlten Rechner wird auf 0.0.0.0 gesetzt, wodurch die Zuordnung zum aktuellen Raum aufgehoben wird.\r\nDie Rechner bleiben mit ihren sonstigen Daten in der Datenbank vorhanden, und sobald ein Rechner das n\u00e4chste mal startet, wird die IP-Adresse wieder aktualisiert. Diese Funktion ist dann n\u00fctzlich, wenn einige Rechner in einem Raum abgebaut wurden, und in der Zukunft in einem anderen Raum wieder aufgebaut werden sollen. Durch zur\u00fccksetzen der IP-Adresse werden die Rechner in der Zwischenzeit nicht mehr im alten Raum angezeigt, was die \u00dcbersicht verbessern kann, bleiben aber \u00fcber ihre sonstigen Merkmale weiterhin in den Statistiken aufsuchbar.",
"lang_sureDeletePermanent": "M\u00f6chten Sie diese(n) Rechner wirklich unwiderruflich aus der Datenbank entfernen?\r\n\r\nWichtig: L\u00f6schen verhindert nicht, dass ein Rechner nach erneutem Starten von bwLehrpool wieder in die Datenbank aufgenommen wird.",
"lang_sureReplaceNoUndo": "Wollen Sie die Daten der ausgew\u00e4hlten Rechner \u00fcbertragen? Diese Aktion kann nicht r\u00fcckg\u00e4ngig gemacht werden.",
"lang_swap": "Swap",
- "lang_tempPart": "Temp. Partition",
+ "lang_tempPart": "Tempor\u00e4r",
+ "lang_tempPartID": "ID44",
"lang_tempPartStats": "Tempor\u00e4re Partition",
"lang_thoseAreProjectors": "Diese Modellnamen werden als Beamer behandelt, auch wenn die EDID-Informationen des Ger\u00e4tes anderes berichten.",
"lang_timebarDesc": "Visuelle Darstellung der letzten Tage. Rote Abschnitte zeigen, wann der Rechner belegt war, gr\u00fcne, wann er nicht verwendet wurde, aber eingeschaltet war. Die leicht abgedunkelten Abschnitte markieren N\u00e4chte (22 bis 8 Uhr).",
"lang_tmpGb": "Temp-HDD",
"lang_total": "Gesamt",
+ "lang_type": "Typ",
+ "lang_unused": "Ungenutzt",
"lang_usageDetails": "Nutzungsdetails",
"lang_usageState": "Zustand",
"lang_uuid": "UUID",
diff --git a/modules-available/statistics/lang/en/filters.json b/modules-available/statistics/lang/en/filters.json
index bd262d32..79372115 100644
--- a/modules-available/statistics/lang/en/filters.json
+++ b/modules-available/statistics/lang/en/filters.json
@@ -6,19 +6,24 @@
"firstseen": "First boot",
"gbram": "RAM (GB)",
"hddgb": "ID44 (GB)",
+ "hddrpm": "HDD rpm",
"hostname": "Host name",
"kvmstate": "Virtualization",
"lastboot": "Last boot",
"lastseen": "Last activity",
+ "live_id45free": "ID45 free (MB)",
"live_memfree": "RAM free (MB)",
"live_swapfree": "swap free (MB)",
"live_tmpfree": "ID44 free (MB)",
"location": "Room\/Location",
- "logintime": "Last login",
"macaddr": "MAC address",
"machineuuid": "System UUID",
+ "nicspeed": "NIC speed",
+ "pcidev": "PCI device",
+ "persistentgb": "ID45 (GB)",
"realcores": "CPU cores (real)",
"runtime": "Uptime (hours)",
+ "standbycrash": "Crashes in Standby",
"state": "State",
"systemmodel": "System model"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/messages.json b/modules-available/statistics/lang/en/messages.json
index e4974923..139076d2 100644
--- a/modules-available/statistics/lang/en/messages.json
+++ b/modules-available/statistics/lang/en/messages.json
@@ -2,12 +2,14 @@
"cleared-n-machines": "Reset {{0}} clients",
"deleted-n-machines": "Deleted {{0}} clients",
"ignored-both-in-use": "Ignoring machine pair as both still seem to be in use. ({{0}} and {{1}})",
+ "ignored-no-permission": "Ignoring {{0}}: No permission",
"invalid-cidr-notion": "Invalid CIDR argument: {{0}}",
"invalid-date-format": "Invalid date format: {{0}}",
"invalid-enum-item": "Selection {{1}} is invalid for {{0}}",
"invalid-filter-argument": "{{1}} is not a vald argument for filter {{0}}",
"invalid-filter-key": "{{0}} is not a valid filter",
"invalid-ip-address": "Invalid IP address",
+ "invalid-pciid": "Invalid PCI-ID {{0}}",
"invalid-replace-format": "Invalid parameter format ({{0}})",
"no-replacement-matches": "No machines match the criteria from above",
"notes-saved": "Notes have been saved",
diff --git a/modules-available/statistics/lang/en/module.json b/modules-available/statistics/lang/en/module.json
index d923ce7b..6e8ffa82 100644
--- a/modules-available/statistics/lang/en/module.json
+++ b/modules-available/statistics/lang/en/module.json
@@ -1,4 +1,10 @@
{
+ "location-column-header-count": "Clients",
+ "location-column-header-load": "Used",
"module_name": "Client Statistics",
+ "page_title": "Client statistics",
+ "submenu_hints": "Hints",
+ "submenu_projectors": "Projectors",
+ "submenu_replace": "Replace machines",
"unused": "Unused"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/permissions.json b/modules-available/statistics/lang/en/permissions.json
index 373fcf07..4bfd92a2 100644
--- a/modules-available/statistics/lang/en/permissions.json
+++ b/modules-available/statistics/lang/en/permissions.json
@@ -1,10 +1,12 @@
{
"hardware.projectors.edit": "Edit beamer assignment",
"hardware.projectors.view": "Show beamer assignment",
+ "hints": "Hints",
"machine.delete": "Delete clients.",
"machine.note.edit": "Edit notes",
"machine.note.view": "Show notes",
"machine.view-details": "Show client details",
+ "replace": "Replace",
"view.list": "Show client list",
"view.summary": "Show visualization"
} \ No newline at end of file
diff --git a/modules-available/statistics/lang/en/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 3fcbc049..10acfdb1 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -1,13 +1,16 @@
{
"lang_64bitSupport": "64\u2009Bit guest support",
+ "lang_MbitPerSecond": "MBit\/s",
"lang_address": "Address",
"lang_apply": "Apply",
+ "lang_baseSystem": "Base system",
"lang_biosDate": "Release date",
"lang_biosFixes": "BIOS fixes",
"lang_biosUpdate": "BIOS update",
"lang_biosUpdateLink": "Go to vendor's site",
"lang_biosVersion": "BIOS version",
"lang_bootedWithoutAnyRunmode": "Booted without any mode",
+ "lang_boottimeTooltip": "Startup duration, kernel init to login screen",
"lang_clientInDifferentRunmode": "Current mode",
"lang_clientList": "List of selected machines",
"lang_configVars": "Config Variables",
@@ -19,17 +22,26 @@
"lang_cputemp": "CPU temperature",
"lang_details": "Details",
"lang_devices": "Devices",
+ "lang_duplex": "Duplex",
"lang_duration": "Duration",
"lang_event": "Event",
"lang_eventType": "Type",
"lang_firstSeen": "First seen",
"lang_free": "free",
+ "lang_fullInfo": "All values",
"lang_gbRam": "RAM",
+ "lang_graphLectureTitle": "Blue shading = number of active lectures",
"lang_hardwareSummary": "Hardware",
"lang_hasNotes": "Notes have been added to this client",
+ "lang_hddUnused": "Unused hard drive space",
+ "lang_hddUnusedId44": "With unpartitioned space on HDD\/SSD, and without any ID44 partition",
+ "lang_hddUnusedId45": "With unpartitioned space on SSD, and without any ID45 partition",
"lang_hdds": "Hard disk drives",
"lang_hostname": "Hostname",
+ "lang_id44size": "ID44 size",
+ "lang_id45size": "ID45 size",
"lang_inUseMachines": "In use",
+ "lang_installedCountMax": "Slots in use \/ free",
"lang_ip": "IP address",
"lang_knownMachines": "Known clients",
"lang_kvmState": "State",
@@ -38,6 +50,8 @@
"lang_labelFilter": "Active filters (AND logic)",
"lang_lastBoot": "Last boot",
"lang_lastSeen": "Last seen",
+ "lang_legacyCpuVmx": "Legacy CPU (VMware)",
+ "lang_legacyCpuVmxText": "These machines have CPUs that are not supported by recent VMware versions. To run VMware VMs on these machines, you need to switch to an older netboot system that still contains VMware Player 12.5.x, e.g. 30r1. Please keep in mind that those older versions lack newer bwLwlehrpool features, so using those might lead to functionality missing or being buggy.",
"lang_listDropdown": "As text",
"lang_location": "Location",
"lang_logHeadline": "Logging",
@@ -50,6 +64,7 @@
"lang_machineOff": "Machine is powered down, or is not running bwLehrpool",
"lang_machineStandby": "In standby mode",
"lang_machineSummary": "Summary",
+ "lang_manufacturer": "Manufacturer",
"lang_maximumAbbrev": "max.",
"lang_mediaIntegrityErrors": "Media Integrity Errors",
"lang_memoryStats": "Memory",
@@ -61,6 +76,10 @@
"lang_moduleHeading": "Client Statistics",
"lang_more": "More",
"lang_newMachines": "New machines",
+ "lang_nicDuplex": "Duplex",
+ "lang_nicSlowSpeed": "Slow network link",
+ "lang_nicSlowSpeedText": "These machines are not connected via Gigabit ethernet. Bootup and VM startup can be notably slower. If you cannot improve the connectivity, try adding an ID45 partition to these clients, and enable local caching under \"config variables\".",
+ "lang_nicSpeed": "Speed",
"lang_noEdid": "No EDID",
"lang_noProjectorsDefined": "No projector overrides defined",
"lang_notes": "Notes",
@@ -72,12 +91,18 @@
"lang_partitionSize": "Size",
"lang_pcmodel": "System model",
"lang_pendingSectors": "Sectors pending reallocation",
+ "lang_persistentPart": "Persistent",
+ "lang_persistentPartID": "ID45",
"lang_powerOnTime": "Power on time",
"lang_projector": "Projector",
"lang_projectors": "Projectors",
"lang_ram": "Memory",
"lang_ramSize": "Size",
- "lang_ramSlots": "Memory slots",
+ "lang_ramSizeCurrentMax": "Current \/ Max",
+ "lang_ramUnderclocked": "Memory running slower than rated for",
+ "lang_ramUnderclockedText": "These clients are equipped with memory sticks that are running slower than what they are rated for. Maybe you can increase the memory speed in the BIOS setup, or swap them with those from another machine that can benefit from faster memory.",
+ "lang_ramUpgrade": "Memory upgrade",
+ "lang_ramUpgradeText": "These machines have little memory. To run VMs on these, we recommend adding more memory..",
"lang_realCores": "Cores",
"lang_reallocatedSectors": "Bad sectors",
"lang_reboot": "Reboot",
@@ -85,32 +110,43 @@
"lang_rebootKexecCheck": "Quick reboot to bwLehrpool (kexec)",
"lang_remoteActions": "Remote actions",
"lang_remoteExec": "Execute command",
+ "lang_remoteSpeedcheck": "Test network speed",
"lang_replace": "Replace",
"lang_replaceInstructions": "If some PCs\/clients have been physically replaced, you can re-assign log entries, session data, position information etc. from the old machine to the new one. This requires that the new machine gets assigned the same IP address as the old one and, if the room planner is used -- that it is placed in the same spot as the old one. The list below shows all machine pairs where 1) the last boot of one machine lies before the first boot of the other one 2) both machines had the same IP address last time they booted. The replacement action will reassign all log events, room plan location and special run mode from the old machine to the new machine.",
"lang_replaceMachinesHeading": "Replace machines",
- "lang_replaceNew": "Old machine",
- "lang_replaceOld": "New machine",
+ "lang_replaceNew": "New machine",
+ "lang_replaceOld": "Old machine",
"lang_resetClearIp": "Reset IP address",
"lang_roomplan": "Location",
"lang_runMode": "Mode of operation",
"lang_runmodeMachines": "With special mode of operation",
"lang_screens": "Screens",
+ "lang_selectColumns": "Show\/hide columns",
"lang_serialNo": "Serial no",
"lang_showList": "List",
"lang_showVisualization": "Visualization",
"lang_shutdown": "Shutdown",
"lang_shutdownConfirm": "Shutdown selected machines?",
+ "lang_slot": "Slot",
+ "lang_slots": "Slots",
+ "lang_smartSelfTestFailed": "SMART Status FAILED",
"lang_sockets": "Sockets",
+ "lang_speed": "Speed",
+ "lang_speedCurrent": "Current speed",
+ "lang_speedDesign": "Maximum speed",
"lang_sureClearIp": "The IP address of the selected machine(s) is set to 0.0.0.0, which removes the assignment to the current room\/location.\r\nThe computers otherwise remain in the database, and as soon as a computer starts the next time, the IP address is updated again. This function is useful if some computers have been removed from one room and are to be set up in another room in the future. By resetting the IP address now, those machines are no longer displayed in the old room, which de-clutters the list view, but they remain searchable in the statistics via their other characteristics.",
"lang_sureDeletePermanent": "Are your sure you want to delete the selected machine(s) from the database? This cannot be undone.\r\n\r\nNote: Deleting machines from the database does not prevent booting up bwLehrpool again, which would recreate their respective database entries.",
"lang_sureReplaceNoUndo": "Are you sure you want to replace the selected machine pairs? This action cannot be undone.",
"lang_swap": "swap",
- "lang_tempPart": "Temp. partition",
+ "lang_tempPart": "Temporary",
+ "lang_tempPartID": "ID44",
"lang_tempPartStats": "Temporary partition",
"lang_thoseAreProjectors": "These model names will always be treated as beamers, even if the device's EDID data says otherwise.",
"lang_timebarDesc": "Visual representation of the last few days. Red parts mark periods where the client was occupied, green parts where the client was idle. Dimmed parts mark nights (10pm to 8am).",
"lang_tmpGb": "Temp HDD",
"lang_total": "Total",
+ "lang_type": "Type",
+ "lang_unused": "Unused",
"lang_usageDetails": "Detailed usage",
"lang_usageState": "State",
"lang_uuid": "UUID",
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 20ff929a..4f11e835 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -2,7 +2,6 @@
class Page_Statistics extends Page
{
- private $query;
private $show;
/**
@@ -22,6 +21,17 @@ class Page_Statistics extends Page
$this->transformLegacyQuery();
}
+ /*
+ Dictionary::translate('submenu_projectors');
+ Dictionary::translate('submenu_replace');
+ Dictionary::translate('submenu_hints');
+ */
+
+ foreach (['projectors', 'replace', 'hints'] as $section) {
+ Dashboard::addSubmenu('?do=statistics&show=' . $section,
+ Dictionary::translate('submenu_' . $section));
+ }
+
$this->show = Request::any('show', false, 'string');
if ($this->show === false && Request::isGet()) {
if (Request::get('uuid') !== false) {
@@ -81,6 +91,8 @@ class Page_Statistics extends Page
$this->rebootControl(false);
} elseif ($action === 'wol') {
$this->wol();
+ } elseif ($action === 'benchmark') {
+ $this->vmstoreBenchmark();
} elseif ($action === 'prepare-exec') {
if (Module::isAvailable('rebootcontrol')) {
RebootControl::prepareExec();
@@ -133,7 +145,7 @@ class Page_Statistics extends Page
/**
* @param bool $reboot true = reboot, false = shutdown
*/
- private function rebootControl($reboot)
+ private function rebootControl(bool $reboot)
{
if (!Module::isAvailable('rebootcontrol'))
return;
@@ -159,6 +171,25 @@ class Page_Statistics extends Page
}
}
+ /**
+ * @param bool $reboot true = reboot, false = shutdown
+ */
+ private function vmstoreBenchmark()
+ {
+ if (!Module::isAvailable('vmstore'))
+ return;
+ $ids = Request::post('uuid', [], 'array');
+ $ids = array_values($ids);
+ if (empty($ids)) {
+ Message::addError('main.parameter-empty', 'uuid');
+ return;
+ }
+ $this->getAllowedMachines(".vmstore.benchmark", $ids, $allowedMachines);
+ if (empty($allowedMachines))
+ return;
+ VmStoreBenchmark::prepareSelectDialog($allowedMachines);
+ }
+
private function getAllowedMachines($permission, $ids, &$allowedMachines)
{
$allowedLocations = User::getAllowedLocations($permission);
@@ -171,7 +202,7 @@ class Page_Statistics extends Page
$ids = array_flip($ids);
$allowedMachines = [];
$seenLocations = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
unset($ids[$row['machineuuid']]);
settype($row['locationid'], 'int');
if (in_array($row['locationid'], $allowedLocations)) {
@@ -202,7 +233,7 @@ class Page_Statistics extends Page
$res = Database::simpleQuery('SELECT machineuuid, locationid FROM machine WHERE machineuuid IN (:ids)', compact('ids'));
$ids = array_flip($ids);
$delete = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
unset($ids[$row['machineuuid']]);
if (in_array($row['locationid'], $allowedLocations)) {
$delete[] = $row['machineuuid'];
@@ -236,38 +267,31 @@ class Page_Statistics extends Page
return;
}
- $sortColumn = Request::any('sortColumn');
- $sortDirection = Request::any('sortDirection');
-
- $filters = StatisticsFilter::parseQuery();
- $filterSet = new StatisticsFilterSet($filters);
- $filterSet->setSort($sortColumn, $sortDirection);
-
- if (!$filterSet->setAllowedLocationsFromPermission('view.' . $this->show)) {
- Message::addError('main.no-permission');
- Util::redirect('?do=main');
- }
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']);
- }
- }
-
protected function doAjax()
{
if (!User::load())
return;
- if (Request::any('action') === 'bios') {
+ $action = Request::any('action');
+ if ($action === 'bios') {
require_once 'modules/statistics/pages/machine.inc.php';
SubPage::ajaxCheckBios();
return;
}
+ if ($action === 'json-lookup') {
+ $reply = [];
+ foreach (Request::post('list', [], 'array') as $item) {
+ $name = PciId::getPciId(PciId::AUTO, $item, true);
+ if ($name === false) {
+ $name = '?????';
+ }
+ $reply[$item] = $name;
+ }
+ header('Content-Type: application/json');
+ die(json_encode($reply));
+ }
$param = Request::any('lookup', false, 'string');
if ($param === false) {
@@ -275,61 +299,14 @@ class Page_Statistics extends Page
}
$add = '';
if (preg_match('/^([a-f0-9]{4}):([a-f0-9]{4})$/', $param, $out)) {
- $cat = 'DEVICE';
- $host = $out[2] . '.' . $out[1];
$add = ' (' . $param . ')';
- } elseif (preg_match('/^([a-f0-9]{4})$/', $param, $out)) {
- $cat = 'VENDOR';
- $host = $out[1];
- } elseif (preg_match('/^c\.([a-f0-9]{2})([a-f0-9]{2})$/', $param, $out)) {
- $cat = 'CLASS';
- $host = $out[2] . '.' . $out[1] . '.c';
- } else {
- die('Invalid format requested');
}
- $cached = Page_Statistics::getPciId($cat, $param);
- if ($cached !== false && $cached['dateline'] > time()) {
- echo $cached['value'], $add;
- exit;
+ $cached = PciId::getPciId(PciId::AUTO, $param, true);
+ if ($cached === false) {
+ $cached = 'Unknown';
}
- $res = dns_get_record($host . '.pci.id.ucw.cz', DNS_TXT);
- if (is_array($res)) {
- foreach ($res as $entry) {
- if (isset($entry['txt']) && substr($entry['txt'], 0, 2) === 'i=') {
- $string = substr($entry['txt'], 2);
- Page_Statistics::setPciId($cat, $param, $string);
- echo $string, $add;
- exit;
- }
- }
- }
- if ($cached !== false) {
- echo $cached['value'], $add;
- exit;
- }
- die('Not found');
- }
-
- public static function getPciId($cat, $id)
- {
- static $cache = [];
- $key = $cat . '-' . $id;
- if (isset($cache[$key]))
- return $cache[$key];
- return $cache[$key] = Database::queryFirst('SELECT value, dateline FROM pciid WHERE category = :cat AND id = :id LIMIT 1',
- array('cat' => $cat, 'id' => $id));
- }
-
- private static function setPciId($cat, $id, $value)
- {
- Database::exec('INSERT INTO pciid (category, id, value, dateline) VALUES (:cat, :id, :value, :timeout)'
- . ' ON DUPLICATE KEY UPDATE value = VALUES(value), dateline = VALUES(dateline)',
- array(
- 'cat' => $cat,
- 'id' => $id,
- 'value' => $value,
- 'timeout' => time() + mt_rand(10, 30) * 86400,
- ), true);
+ echo $cached, $add;
+ exit;
}
}
diff --git a/modules-available/statistics/pages/hints.inc.php b/modules-available/statistics/pages/hints.inc.php
new file mode 100644
index 00000000..bfb28c24
--- /dev/null
+++ b/modules-available/statistics/pages/hints.inc.php
@@ -0,0 +1,221 @@
+<?php
+
+class SubPage
+{
+
+ public static function doPreprocess()
+ {
+ User::assertPermission('hints');
+ }
+
+ public static function doRender()
+ {
+ $locs = User::getAllowedLocations('hints');
+ if (in_array(0, $locs)) {
+ $locs = [];
+ }
+ self::showLegacyCpu($locs);
+ self::showMemoryUpgrade($locs);
+ self::showSlowNics($locs);
+ self::showUnusedSpace($locs);
+ self::showMemorySlow($locs);
+ }
+
+ private static function isNonClientRunmode(string $machineUuid): bool
+ {
+ static $cache = null;
+ if ($cache === null) {
+ if (!Module::isAvailable('runmode')) {
+ $cache = [];
+ } else {
+ $cache = RunMode::getAllClients(false, false);
+ }
+ }
+ return isset($cache[$machineUuid]);
+ }
+
+ /**
+ * Machines that have less than 8GB of RAM. Highlight those
+ * that still have free memory slots.
+ */
+ private static function showMemoryUpgrade(array $locs)
+ {
+ $q = new HardwareQuery(HardwareInfo::MAINBOARD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineWhere('lastseen', '>', strtotime('-60 days'));
+ $q->addLocalColumn('Memory Slot Occupied');
+ $q->addGlobalColumn('Memory Slot Count');
+ $q->addGlobalColumn('Memory Maximum Capacity');
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addMachineColumn('state');
+ $q->addLocalColumn('Memory Installed Capacity')->addCondition('<', 8 * 1024 * 1024 * 1024);
+ $list = [];
+ foreach ($q->query() as $row) {
+ if (self::isNonClientRunmode($row['machineuuid']))
+ continue;
+ if (HardwareParser::convertSize($row['Memory Installed Capacity'], 'M', false)
+ >= HardwareParser::convertSize($row['Memory Maximum Capacity'], 'M', false)) {
+ $row['size_class'] = 'danger';
+ }
+ if ($row['Memory Slot Occupied'] >= $row['Memory Slot Count']) {
+ $row['count_class'] = 'warning';
+ }
+ $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']);
+ $list[] = $row;
+ }
+ if (empty($list))
+ return;
+ ArrayUtil::sortByColumn($list, 'hostname');
+ Render::addTemplate('hints-ram-upgrade', ['list' => $list]);
+ }
+
+ /**
+ * Show machines where RAM modules are running slower
+ * than their design speed.
+ */
+ private static function showMemorySlow(array $locs)
+ {
+ $q = new HardwareQuery(HardwareInfo::RAM_MODULE);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineWhere('lastseen', '>', strtotime('-60 days'));
+ //$q->addLocalColumn('Locator');
+ //$q->addLocalColumn('Bank Locator');
+ $q->addGlobalColumn('Form Factor');
+ $q->addGlobalColumn('Type');
+ $q->addGlobalColumn('Size');
+ $q->addGlobalColumn('Manufacturer');
+ $q->addLocalColumn('Serial Number');
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addMachineColumn('state');
+ $col = $q->addGlobalColumn('Speed');
+ $col->addCondition('>', $q->addLocalColumn('Configured Memory Speed'));
+ $list = [];
+ foreach ($q->query(['machineuuid', 'Size', 'Manufacturer', 'Speed', 'Configured Memory Speed']) as $row) {
+ // Sometimes configured speed reports as 2666 while rated speed is 2667
+ // Cast as these have a MT/s suffic, triggering a PHP notice about malformed numbers
+ if ((int)$row['Configured Memory Speed'] + 33 >= (int)$row['Speed'])
+ continue;
+ $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']);
+ $list[] = $row;
+ }
+ if (empty($list))
+ return;
+ ArrayUtil::sortByColumn($list, 'hostname');
+ Render::addTemplate('hints-ram-underclocked', ['list' => $list]);
+ }
+
+ /**
+ * Show machines that have unpartitioned space available,
+ * and no ID44 or ID45.
+ */
+ private static function showUnusedSpace(array $locs)
+ {
+ $id44 = $id45 = [];
+ // ID44
+ $q = new HardwareQuery(HardwareInfo::HDD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineWhere('lastseen', '>', strtotime('-60 days'));
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addLocalColumn('unused')->addCondition('>', 2000000000); // 2 GB
+ $q->addMachineWhere('id44mb', '<', 20000); // 20 GB
+ $q->addMachineColumn('state');
+ foreach ($q->query() as $row) {
+ $row['unused_s'] = Util::readableFileSize($row['unused']);
+ $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2);
+ $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']);
+ $id44[] = $row;
+ }
+ // ID45
+ $q = new HardwareQuery(HardwareInfo::HDD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineWhere('lastseen', '>', strtotime('-60 days'));
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addLocalColumn('unused')->addCondition('>', 50000000000); // 50 GB
+ $q->addMachineWhere('id44mb', '>', 20000); // 20 GB
+ $q->addMachineWhere('id45mb', '<', 20000); // 20 GB
+ $q->addMachineColumn('state');
+ // Only suggest SSD based systems, caching on spinning rust is usually slower than GBit
+ $q->addGlobalColumn('rotation_rate')->addCondition('=', 0);
+ foreach ($q->query() as $row) {
+ $row['unused_s'] = Util::readableFileSize($row['unused']);
+ $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2);
+ $row['id45mb_s'] = Util::readableFileSize($row['id45mb'], -1, 2);
+ $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']);
+ $id45[] = $row;
+ }
+ if (empty($id44) && empty($id45))
+ return;
+ ArrayUtil::sortByColumn($id44, 'hostname');
+ ArrayUtil::sortByColumn($id45, 'hostname');
+ Render::addTemplate('hints-hdd-grow', [
+ 'id44' => $id44,
+ 'id45' => $id45,
+ ]);
+ }
+
+ private static function showSlowNics(array $locs)
+ {
+ $list = [];
+ $q = new HardwareQuery(HardwareInfo::MAINBOARD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineWhere('lastseen', '>', strtotime('-60 days'));
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addMachineColumn('state');
+ $q->addLocalColumn('nic-speed')->addCondition('<', 1000);
+ $q->addLocalColumn('nic-duplex');
+ foreach ($q->query() as $row) {
+ if ($row['nic-speed'] == 0) {
+ $row['nic-speed'] = '???';
+ }
+ $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']);
+ $list[] = $row;
+ }
+ if (empty($list))
+ return;
+ ArrayUtil::sortByColumn($list, 'hostname');
+ Render::addTemplate('hints-nic-speed', ['list' => $list]);
+ }
+
+ /**
+ * Show machines that have a CPU that is only supported by VMware 12.5.x,
+ * but not newer versions.
+ */
+ private static function showLegacyCpu(array $locs)
+ {
+ $list = [];
+ $q = new HardwareQuery(HardwareInfo::CPU);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineWhere('lastseen', '>', strtotime('-60 days'));
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addMachineColumn('state');
+ $q->addMachineColumn('cpumodel');
+ $q->addGlobalColumn('vmx-legacy')->addCondition('<>', 0);
+ foreach ($q->query() as $row) {
+ $row['icon'] = StatisticsStyling::machineStateToIcon($row['state']);
+ $list[] = $row;
+ }
+ if (empty($list))
+ return;
+ ArrayUtil::sortByColumn($list, 'hostname');
+ Render::addTemplate('hints-cpu-legacy', ['list' => $list]);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/statistics/pages/list.inc.php b/modules-available/statistics/pages/list.inc.php
index e9af994a..f08cd71c 100644
--- a/modules-available/statistics/pages/list.inc.php
+++ b/modules-available/statistics/pages/list.inc.php
@@ -22,31 +22,44 @@ class SubPage
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showMachineList($filterSet)
+ private static function showMachineList(StatisticsFilterSet $filterSet): void
{
Module::isAvailable('js_stupidtable');
$filterSet->makeFragments($where, $join, $args);
$xtra = '';
- if ($filterSet->isNoId44Filter()) {
- $xtra .= ', data';
- }
if (Module::isAvailable('runmode')) {
$xtra .= ', runmode.module AS rmmodule, runmode.isclient';
if (strpos($join, 'runmode') === false) {
- $join .= ' LEFT JOIN runmode USING (machineuuid) ';
+ $join .= ' LEFT JOIN runmode ON (m.machineuuid = runmode.machineuuid) ';
}
}
- $res = Database::simpleQuery("SELECT m.machineuuid, m.locationid, m.macaddr, m.clientip, m.lastseen,
+ $allRows = Database::queryAll("SELECT m.machineuuid, m.locationid, m.macaddr, m.clientip, m.lastseen,
m.logintime, m.state, m.currentuser, m.currentrunmode, m.realcores, m.mbram, m.kvmstate, m.cpumodel, m.id44mb,
- m.hostname, m.notes IS NOT NULL AS hasnotes,
+ m.id45mb, m.hostname, m.notes IS NOT NULL AS hasnotes,
m.badsectors, Count(s.machineuuid) AS confvars $xtra FROM machine m
- LEFT JOIN setting_machine s USING (machineuuid)
+ LEFT JOIN setting_machine s ON (m.machineuuid = s.machineuuid)
$join WHERE $where GROUP BY m.machineuuid", $args);
- $rows = array();
- $singleMachine = 'none';
+ // If filter results in just one result, redirect to machine details
+ if (count($allRows) === 1) {
+ Util::redirect('?do=statistics&uuid=' . $allRows[0]['machineuuid']);
+ }
+ // Gather additional info that would be ugly to fetch via joins above
+ $uuids = array_column($allRows, 'machineuuid');
+ $machineWithHdds = Database::queryKeyValueList("SELECT mxx.machineuuid, Count(s.hwid) AS num
+ FROM statistic_hw s
+ INNER JOIN machine_x_hw AS mxx ON (s.hwid = mxx.hwid AND s.hwtype = :type
+ AND mxx.disconnecttime = 0 AND mxx.machineuuid IN (:ids))
+ GROUP BY mxx.machineuuid",
+ ['type' => HardwareInfo::HDD, 'ids' => $uuids]);
+ $machineNicSpeed = Database::queryKeyValueList("SELECT mxx.machineuuid, Max(mxhp.`numeric`) AS num
+ FROM statistic_hw s
+ INNER JOIN machine_x_hw AS mxx ON (s.hwid = mxx.hwid AND s.hwtype = :type
+ AND mxx.disconnecttime = 0 AND mxx.machineuuid IN (:ids))
+ INNER JOIN machine_x_hw_prop mxhp ON (mxx.machinehwid = mxhp.machinehwid AND mxhp.prop = :prop)
+ GROUP BY mxx.machineuuid",
+ ['type' => HardwareInfo::MAINBOARD, 'prop' => 'nic-speed', 'ids' => $uuids]);
+ $machineWithConfigOverrides = Database::queryKeyValueList("SELECT machineuuid, Count(machineuuid) AS num
+ FROM setting_machine WHERE machineuuid IN (:ids) GROUP BY machineuuid", ['ids' => $uuids]);
// TODO: Cannot disable checkbox for those where user has no permission, since we got multiple actions now
// We should pass these lists to the output and add some JS magic
// Either disable the delete/reboot/... buttons as soon as at least one "forbidden" client is selected (potentially annoying)
@@ -56,38 +69,54 @@ class SubPage
$shutdownAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.reboot');
$wolAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.wol');
$execAllowedLocations = User::getAllowedLocations('.rebootcontrol.action.exec');
+ $benchmarkAllowedLocations = User::getAllowedLocations('.vmstore.benchmark');
// Only make client clickable if user is allowed to view details page
$detailsAllowedLocations = User::getAllowedLocations("machine.view-details");
$location = self::buildLocationLookup();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- if ($singleMachine === 'none') {
- $singleMachine = $row['machineuuid'];
- } else {
- $singleMachine = false;
- }
+ $rows = [];
+ $colValCount = []; // Count unique values for several columns
+ foreach ($allRows as &$row) {
+ settype($row['locationid'], 'int');
$row['link_details'] = in_array($row['locationid'], $detailsAllowedLocations);
//$row['firstseen'] = Util::prettyTime($row['firstseen']);
$row['lastseen_int'] = $row['lastseen'];
$row['lastseen'] = Util::prettyTime($row['lastseen']);
//$row['lastboot'] = Util::prettyTime($row['lastboot']);
- $row['gbram'] = round(ceil($row['mbram'] / 512) / 2, 1); // Trial and error until we got "expected" rounding..
- $row['gbtmp'] = round($row['id44mb'] / 1024);
+ $row['gbram'] = Dictionary::number(ceil($row['mbram'] / 512) / 2, 1); // Trial and error until we got "expected" rounding..
+ $row['gbtmp'] = Dictionary::number($row['id44mb'] / 1024);
+ $row['gbpersist'] = Dictionary::number($row['id45mb'] / 1024);
$octets = explode('.', $row['clientip']);
if (count($octets) === 4) {
$row['subnet'] = "$octets[0].$octets[1].$octets[2]";
$row['lastoctet'] = $octets[3];
}
- $row['ramclass'] = StatisticsStyling::ramColorClass($row['mbram']);
+ $row['ramclass'] = StatisticsStyling::ramColorClass((int)$row['mbram']);
$row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']);
- $row['hddclass'] = StatisticsStyling::hddColorClass($row['gbtmp']);
+ $row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']);
if (empty($row['hostname'])) {
$row['hostname'] = $row['clientip'];
}
- if (isset($row['data'])) {
- if (!preg_match('#^Disk.* /dev/[^d].* (bytes$|sectors,)#m', $row['data'])) {
- $row['nohdd'] = true;
- }
+ if (isset($machineWithConfigOverrides[$row['machineuuid']])) {
+ $row['confvars'] = $machineWithConfigOverrides[$row['machineuuid']];
+ }
+ if (isset($machineWithHdds[$row['machineuuid']])) {
+ $row['hddcount'] = $machineWithHdds[$row['machineuuid']];
+ } else if ($row['id44mb'] > 0) {
+ // This might be a machine that wasn't booted with a recent system, and hence doesn't have HWinfo in DB
+ // If we have ID44 space in our main table, we most likely got an HDD, so fake a count of 1
+ $row['hddcount'] = 1;
+ }
+ if (!isset($machineNicSpeed[$row['machineuuid']])) {
+ $row['nic-speed'] = 0;
+ $row['nic-speed_s'] = '???';
+ } else {
+ $row['nic-speed'] = $machineNicSpeed[$row['machineuuid']];
+ $row['nic-speed_s'] = Dictionary::number($machineNicSpeed[$row['machineuuid']]);
}
+ if (isset($row['data']) && !$row['data']) {
+ $row['nohdd'] = true;
+ }
+ // Shorten CPU names a bit for prettier display in small column
$row['cpumodel'] = preg_replace('/\(R\)|\(TM\)|\bintel\b|\bamd\b|\bcpu\b|dual-core|\bdual\s+core\b|\bdual\b|\bprocessor\b/i', ' ', $row['cpumodel']);
if (!empty($row['rmmodule'])) {
$data = RunMode::getRunMode($row['machineuuid'], RunMode::DATA_STRINGS);
@@ -116,10 +145,48 @@ class SubPage
if ($row['locationid'] > 0) {
$row['location'] = $location[$row['locationid']];
}
- $rows[] = $row;
+ foreach (['locationid', 'cpumodel', 'nic-speed_s', 'gbram', 'gbtmp'] as $key) {
+ if (!isset($colValCount[$key][$row[$key]])) {
+ $colValCount[$key][$row[$key]] = [];
+ }
+ $colValCount[$key][$row[$key]][] = $row['machineuuid'];
+ }
+ $rows[] =& $row;
}
- if ($singleMachine !== false && $singleMachine !== 'none') {
- Util::redirect('?do=statistics&uuid=' . $singleMachine);
+ // Now if all machines are from the same location, try to load the roomplan
+ // Also, collect all properties that are the same across all machines for display in the sidebar
+ $roomsvg = null;
+ $side = [];
+ if (!empty($rows) && !empty($colValCount)) {
+ if (count($colValCount['locationid']) === 1
+ && ($lid = array_key_first($colValCount['locationid'])) > 0
+ && Module::isAvailable('roomplanner')) {
+ $roomsvg = PvsGenerator::generateSvg($lid, false, 0, 1, true, $colValCount['locationid'][$lid]);
+ }
+ // Handle our selected attributes
+ foreach (['locationid', 'cpumodel', 'nic-speed_s', 'gbram', 'gbtmp'] as $key) {
+ if (count($colValCount[$key]) === 1) {
+ $val = array_key_first($colValCount[$key]);
+ // Suffixes are not localized, but hopefully generic enough for now
+ switch ($key) {
+ case 'locationid':
+ if (!isset($location[$val]))
+ continue 2;
+ $val = $location[$val]['name'];
+ break;
+ case 'gbram':
+ $val .= ' GiB RAM';
+ break;
+ case 'gbtmp':
+ $val .= ' GiB ID-44';
+ break;
+ case 'nic-speed_s':
+ $val .= ' MBit/s';
+ break;
+ }
+ $side[] = $val;
+ }
+ }
}
$data = array(
'rowCount' => count($rows),
@@ -133,11 +200,14 @@ class SubPage
'canDelete' => !empty($deleteAllowedLocations),
'canWol' => !empty($wolAllowedLocations),
'canExec' => !empty($execAllowedLocations),
+ 'canBenchmark' => !empty($benchmarkAllowedLocations),
+ 'roomsvg' => $roomsvg,
+ 'sidebar' => $side,
);
Render::addTemplate('clientlist', $data);
}
- private static function buildLocationLookup()
+ private static function buildLocationLookup(): array
{
$ret = [];
$i = 0;
@@ -147,4 +217,4 @@ class SubPage
return $ret;
}
-}
+} \ No newline at end of file
diff --git a/modules-available/statistics/pages/machine.inc.php b/modules-available/statistics/pages/machine.inc.php
index ea545b16..1d46b523 100644
--- a/modules-available/statistics/pages/machine.inc.php
+++ b/modules-available/statistics/pages/machine.inc.php
@@ -5,7 +5,9 @@ class SubPage
public static function doPreprocess()
{
-
+ if (!Module::isAvailable('js_chart')) {
+ ErrorHandler::traceError('js_chart not available');
+ }
}
public static function doRender()
@@ -36,7 +38,7 @@ class SubPage
'end' => $row['logintime'] + 300,
));
$session = false;
- while ($r = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $r) {
if ($session === false || abs($session['dateline'] - $row['logintime']) > abs($r['dateline'] - $row['logintime'])) {
$session = $r;
}
@@ -54,22 +56,106 @@ class SubPage
$client = Database::queryFirst('SELECT machineuuid, locationid, macaddr, clientip, firstseen, lastseen, logintime, lastboot, state,
mbram, live_tmpsize, live_tmpfree, live_id45size, live_id45free, live_swapsize, live_swapfree,
live_memsize, live_memfree, live_cpuload, live_cputemp,
- Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, data, hostname, currentuser, currentsession, notes
+ Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, id45mb, data, hostname, currentuser, currentsession, notes
FROM machine WHERE machineuuid = :uuid',
array('uuid' => $uuid));
if ($client === false) {
Message::addError('unknown-machine', $uuid);
return;
}
- if (Module::isAvailable('locations') && !Location::isLeaf($client['locationid'])) {
- $client['hasroomplan'] = false;
+ Render::setTitle(empty($client['hostname']) ? $client['clientip'] : $client['hostname']);
+ $locations = [];
+ if ($client['locationid'] > 0 && Module::isAvailable('locations')) {
+ if (!Location::isLeaf($client['locationid'])) {
+ $client['hasroomplan'] = false;
+ }
+ $locations = Location::getLocationRootChain($client['locationid']);
+ }
+ if ($client['locationid'] && $client['hasroomplan'] && Module::isAvailable('roomplanner')) {
+ $client['roomsvg'] = PvsGenerator::generateSvg($client['locationid'], $client['machineuuid'],
+ 0, 1, true);
}
User::assertPermission('machine.view-details', (int)$client['locationid']);
// Hack: Get raw collected data
if (Request::get('raw', false)) {
- Header('Content-Type: text/plain; charset=utf-8');
+ Header('Content-Type: application/json');
die($client['data']);
}
+ // Parse data
+ $hdds = array();
+ if ($client['data'][0] === '{') {
+ $json = json_decode($client['data'], true);
+ if (is_array($json)) {
+ $client += self::parseJson($uuid, $json);
+ $hdds['hdds'] = self::queryHddData($uuid);
+ }
+ } else {
+ self::parseLegacy($client, $hdds);
+ }
+ unset($client['data']);
+ // Get rid of configured speed, if equal to maximum speed
+ foreach ($client['ram'] as &$item) {
+ if (isset($item['Configured Memory Speed']) && $item['Configured Memory Speed'] === $item['Speed']) {
+ unset($item['Configured Memory Speed']);
+ }
+ }
+ unset($item);
+ // PCI
+ // 1) get passthrough groups
+ $passthroughTypes = [];
+ if (!empty($locations)) {
+ $hw = new HardwareQuery(HardwareInfo::PCI_DEVICE, $uuid, true);
+ // TODO: Get list of enabled pass through groups for this client's location
+ $hw->addForeignJoin(true, '@PASSTHROUGH', 'passthrough_group_x_location', 'groupid',
+ 'locationid', $locations);
+ $hw->addGlobalColumn('vendor');
+ $hw->addGlobalColumn('device');
+ $hw->addGlobalColumn('rev');
+ $res = $hw->query();
+ foreach ($res as $row) {
+ $devId = $row['vendor'] . ':' . $row['device'] . ':' . $row['rev'];
+ if (!isset($passthroughTypes[$devId])) {
+ $passthroughTypes[$devId] = [];
+ }
+ $passthroughTypes[$devId][$row['@PASSTHROUGH']] = $row['@PASSTHROUGH'];
+ }
+ }
+ // 2) Sort and mangle list
+ $client['lspci1'] = $client['lspci2'] = [];
+ foreach ($client['lspci'] as $item) {
+ $devId = $item['vendor'] . ':' . $item['device'];
+ $item['vendor_s'] = PciId::getPciId(PciId::VENDOR, $item['vendor']);
+ $item['device_s'] = PciId::getPciId(PciId::DEVICE, $item['vendor'] . $item['device']);
+ if ($item['vendor_s'] === false) {
+ $pciLookup[$item['vendor']] = true;
+ }
+ if ($item['device_s'] === false) {
+ $pciLookup[$devId] = true;
+ }
+ // Passthrough enabled?
+ if (isset($passthroughTypes[$devId . ':' . ($item['rev'] ?? '')])) {
+ $item['pt'] = implode(', ', $passthroughTypes[$devId . ':' . ($item['rev'] ?? '')]);
+ }
+ $class = $item['class'];
+ if ($class === '0300' || $class === '0200' || $class === '0403' || !empty($item['pt'])) {
+ $dst =& $client['lspci1'];
+ } else {
+ $dst =& $client['lspci2'];
+ }
+ if (!isset($dst[$class])) {
+ $dst[$class] = [
+ 'class' => $class,
+ 'class_s' => PciId::getPciId(PciId::DEVCLASS, $class, true),
+ 'entries' => [],
+ ];
+ }
+ $dst[$class]['entries'][] = $item;
+ }
+ unset($dst, $client['lspci']);
+ ksort($client['lspci1']);
+ ksort($client['lspci2']);
+ $client['lspci1'] = array_values($client['lspci1']);
+ $client['lspci2'] = array_values($client['lspci2']);
// Runmode
if (Module::isAvailable('runmode')) {
$data = RunMode::getRunMode($uuid, RunMode::DATA_STRINGS);
@@ -119,8 +205,9 @@ class SubPage
$client['lastboot_s'] .= ' (Up ' . floor($uptime / 86400) . 'd ' . gmdate('H:i', $uptime) . ')';
}
}
- $client['gbram'] = round(ceil($client['mbram'] / 512) / 2, 1);
- $client['gbtmp'] = round($client['id44mb'] / 1024);
+ $client['gbram'] = Dictionary::number(ceil($client['mbram'] / 512) / 2, 1);
+ $client['gbtmp'] = Dictionary::number($client['id44mb'] / 1024);
+ $client['gbid45'] = Dictionary::number($client['id45mb'] / 1024);
foreach (['tmp', 'id45', 'swap', 'mem'] as $item) {
if ($client['live_' . $item . 'size'] == 0)
continue;
@@ -132,53 +219,59 @@ class SubPage
$client['live_cpuidle'] = 100 - $client['live_cpuload'];
}
$client['live_cputemppercent'] = max(0, min(100, 110 - $client['live_cputemp']));
- $client['ramclass'] = StatisticsStyling::ramColorClass($client['mbram']);
+ $client['ramclass'] = StatisticsStyling::ramColorClass((int)$client['mbram']);
$client['kvmclass'] = StatisticsStyling::kvmColorClass($client['kvmstate']);
- $client['hddclass'] = StatisticsStyling::hddColorClass($client['gbtmp']);
- // Parse the giant blob of data
- if (strpos($client['data'], "\r") !== false) {
- $client['data'] = str_replace("\r", "\n", $client['data']);
- }
- $hdds = array();
- if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) {
- foreach ($out as $section) {
- if ($section[1] === 'CPU') {
- Parser::parseCpu($client, $section[2]);
- }
- if ($section[1] === 'dmidecode') {
- Parser::parseDmiDecode($client, $section[2]);
- }
- if ($section[1] === 'Partition tables') {
- Parser::parseHdd($hdds, $section[2]);
- }
- if ($section[1] === 'PCI ID') {
- $client['lspci1'] = $client['lspci2'] = array();
- Parser::parsePci($client['lspci1'], $client['lspci2'], $section[2]);
- }
- if (isset($hdds['hdds']) && $section[1] === 'smartctl') {
- // This currently requires that the partition table section comes first...
- Parser::parseSmartctl($hdds['hdds'], $section[2]);
- }
- }
+ $client['hddclass'] = StatisticsStyling::hddColorClass((int)$client['gbtmp']);
+ // Format HDD data to strings
+ foreach ($hdds['hdds'] as &$hdd) {
+ $hdd['smart_status_failed'] = !($client['smart_status//passed'] ?? 1);
+ self::mangleHdd($hdd);
}
- unset($client['data']);
// BIOS update check
- if (!empty($client['biosrevision'])) {
- $mainboard = $client['mobomanufacturer'] . '##' . $client['mobomodel'];
- $system = $client['pcmanufacturer'] . '##' . $client['pcmodel'];
- $ret = self::checkBios($mainboard, $system, $client['biosdate'], $client['biosrevision']);
+ if (!empty($client['bios']['BIOS Revision']) || !empty($client['bios']['Release Date'])) {
+ if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})#', $client['bios']['Release Date'] ?? '', $out)) {
+ $client['bios']['Release Date'] = $out[2] . '.' . $out[1] . '.' . $out[3];
+ }
+ $mainboard = ($client['mainboard']['Manufacturer'] ?? '') . '##' . ($client['mainboard']['Product Name'] ?? '');
+ $system = ($client['system']['Manufacturer'] ?? '') . '##' . ($client['system']['Product Name'] ?? '');
+ $ret = self::checkBios($mainboard, $system,
+ $client['bios']['Release Date'] ?? null,
+ $client['bios']['BIOS Revision'] ?? null);
if ($ret === false) { // Not loaded, use AJAX
$params = [
'mainboard' => $mainboard,
'system' => $system,
- 'date' => $client['biosdate'],
- 'revision' => $client['biosrevision'],
+ 'date' => $client['bios']['Release Date'] ?? null,
+ 'revision' => $client['bios']['BIOS Revision'] ?? null,
];
$client['biosurl'] = '?do=statistics&action=bios&' . http_build_query($params);
} elseif (!isset($ret['status']) || $ret['status'] !== 0) {
$client['bioshtml'] = Render::parse('machine-bios-update', $ret);
}
}
+ // Last booted system. The boot-system entry is created when the client fetches the config, so
+ // early on, *before* we get the ~poweron event. But in the ~poweron event, the client provides the
+ // kernel uptime, which is subtracted from what we write to lastboot, so it is actually *before*
+ // boot-system.
+ $os = Database::queryFirst("SELECT `data` AS `system`, `dateline`
+ FROM statistic
+ WHERE (dateline >= :lastboot) AND typeid = 'boot-system' AND machineuuid = :uuid
+ ORDER BY dateline ASC LIMIT 1",
+ ['lastboot' => $client['lastboot'], 'uuid' => $uuid]);
+ if ($os !== false) {
+ $client['minilinux'] = $os['system'];
+ $graphical = Database::queryFirst("SELECT `dateline`
+ FROM statistic
+ WHERE (dateline >= :lastboot) AND typeid = 'graphical-startup' AND machineuuid = :uuid
+ ORDER BY dateline ASC LIMIT 1",
+ ['lastboot' => $client['lastboot'], 'uuid' => $uuid]);
+ if ($graphical !== false) {
+ $boottime = $graphical['dateline'] - $client['lastboot'];
+ if ($boottime < 400) { // Sanity-check
+ $client['boottime_s'] = gmdate('i:s', $boottime);
+ }
+ }
+ }
// Get locations
if (Module::isAvailable('locations')) {
$locs = Location::getLocationsAssoc();
@@ -198,10 +291,10 @@ class SubPage
. " LEFT JOIN machine_x_hw_prop p ON (m.machinehwid = p.machinehwid AND p.prop = 'resolution')"
. " LEFT JOIN statistic_hw_prop q ON (m.hwid = q.hwid AND q.prop = 'projector')"
. " WHERE m.machineuuid = :uuid",
- array('screen' => DeviceType::SCREEN, 'uuid' => $uuid));
+ array('screen' => HardwareInfo::SCREEN, 'uuid' => $uuid));
$client['screens'] = array();
$ports = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if ($row['disconnecttime'] != 0)
continue;
$ports[] = $row['connector'];
@@ -211,6 +304,10 @@ class SubPage
Permission::addGlobalTags($client['perms'], null, ['hardware.projectors.edit', 'hardware.projectors.view']);
// Throw output at user
Render::addTemplate('machine-main', $client);
+ if (!empty($pciLookup)) {
+ Render::addTemplate('js-pciquery',
+ ['missing_ids' => json_encode(array_keys($pciLookup))]);
+ }
// Sessions
$NOW = time();
$cutoff = $NOW - 86400 * 7;
@@ -227,7 +324,7 @@ class SubPage
$spans['graph'] = '';
$last = false;
$first = true;
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if (!$client['isclient'] && $row['typeid'] === Statistics::SESSION_LENGTH)
continue; // Don't differentiate between session and idle for non-clients
if ($first && $row['dateline'] > $cutoff && $client['lastboot'] > $cutoff) {
@@ -284,7 +381,6 @@ class SubPage
'duration' => '-',
'glyph' => 'user',
];
- $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']);
} elseif ($client['state'] === 'OFFLINE') {
$spans['graph'] .= '<div style="background:#444;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%">&nbsp;</div>';
$spans['rows'][] = [
@@ -311,8 +407,8 @@ class SubPage
}
if (count($spans['rows']) > 10) {
$spans['hasrows2'] = true;
- $spans['rows2'] = array_slice($spans['rows'], ceil(count($spans['rows']) / 2));
- $spans['rows'] = array_slice($spans['rows'], 0, ceil(count($spans['rows']) / 2));
+ $spans['rows2'] = array_slice($spans['rows'], (int)ceil(count($spans['rows']) / 2));
+ $spans['rows'] = array_slice($spans['rows'], 0, (int)ceil(count($spans['rows']) / 2));
}
$spans['isclient'] = $client['isclient'];
Render::addTemplate('machine-usage', $spans);
@@ -326,7 +422,7 @@ class SubPage
. ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid']));
$count = 0;
$log = array();
- while ($row = $lres->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($lres as $row) {
if (substr($row['description'], -5) === 'on :0' && strpos($row['description'], 'root logged') === false) {
continue;
}
@@ -349,7 +445,197 @@ class SubPage
}
}
- private static function eventToIconName($event)
+ private static function parseLegacy(array &$client, array &$hdds)
+ {
+ // Parse the giant blob of data
+ if (strpos($client['data'], "\r") !== false) {
+ $client['data'] = str_replace("\r", "\n", $client['data']);
+ }
+ if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) {
+ foreach ($out as $section) {
+ if ($section[1] === 'CPU') {
+ HardwareParserLegacy::parseCpu($client, $section[2]);
+ }
+ if ($section[1] === 'dmidecode') {
+ HardwareParserLegacy::parseDmiDecode($client, $section[2]);
+ }
+ if ($section[1] === 'Partition tables') {
+ HardwareParserLegacy::parseHdd($hdds, $section[2]);
+ }
+ if ($section[1] === 'PCI ID') {
+ $client['lspci'] = HardwareParserLegacy::parsePci($section[2]);
+ }
+ if (isset($hdds['hdds']) && $section[1] === 'smartctl') {
+ // This currently requires that the partition table section comes first...
+ HardwareParserLegacy::parseSmartctl($hdds['hdds'], $section[2]);
+ }
+ }
+ }
+ }
+
+ private static function parseJson(string $uuid, array $json): array
+ {
+ $return = [
+ 'lspci' => $json['lspci'] ?? [],
+ 'ram' => array_map(function($item) {
+ return HardwareParser::prepareDmiProperties($item);
+ }, HardwareParser::getDmiHandles($json, 17)),
+ ];
+ foreach ($return['ram'] as $ram) {
+ if (!empty($ram['Form Factor']) && !empty($ram['Type'])) {
+ $return['ramtype'] = $ram['Type'] . '-' . $ram['Form Factor'];
+ break;
+ }
+ }
+ $need = [
+ 'bios' => 0,
+ 'system' => 1,
+ 'mainboard' => 2,
+ ];
+ foreach ($need as $name => $id) {
+ $return[$name] = HardwareParser::prepareDmiProperties(
+ HardwareParser::getDmiHandles($json, $id)[0] ?? []);
+ }
+ $q = new HardwareQuery(HardwareInfo::MAINBOARD, $uuid);
+ $q->addGlobalColumn('Memory Maximum Capacity');
+ $q->addGlobalColumn('Memory Slot Count');
+ $q->addLocalColumn('cpu-sockets');
+ $q->addLocalColumn('cpu-cores');
+ $q->addLocalColumn('cpu-threads');
+ $q->addLocalColumn('nic-speed');
+ $q->addLocalColumn('nic-duplex');
+ $res = $q->query()->fetch();
+ if (is_array($res)) {
+ $return += $res;
+ }
+ return $return;
+ }
+
+ private static function queryHddData(string $uuid): array
+ {
+ $hdds = [];
+ $ret = Database::simpleQuery("SELECT mp.`machinehwid`, mp.`prop`, mp.`value`, mp.`numeric`
+ FROM machine_x_hw_prop mp
+ INNER JOIN machine_x_hw mxhw ON (mp.machinehwid = mxhw.machinehwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0)
+ INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type)
+ UNION SELECT mxhw.`machinehwid`, hwp.`prop`, hwp.`value`, hwp.`numeric`
+ FROM statistic_hw_prop hwp
+ INNER JOIN machine_x_hw mxhw ON (hwp.hwid = mxhw.hwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0)
+ INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type)
+ ",
+ ['type' => HardwareInfo::HDD, 'uuid' => $uuid]);
+ foreach ($ret as $row) {
+ if (!isset($hdds[$row['machinehwid']])) {
+ $hdds[$row['machinehwid']] = ['partitions' => []];
+ }
+ $hdd =& $hdds[$row['machinehwid']];
+ if (preg_match('/^(attr_[0-9]+)_(.*)$/', $row['prop'], $out)) {
+ // SMART attributes
+ if (!isset($hdd[$out[1]])) {
+ $hdd[$out[1]] = [];
+ }
+ $hdd[$out[1]][$out[2]] = $row['numeric'] ?? $row['value'];
+ } elseif (preg_match('/^part_([0-9]+)_(.*)$/', $row['prop'], $out)) {
+ // Partitions
+ if (!isset($hdd['partitions'][$out[1]])) {
+ $hdd['partitions'][$out[1]] = ['id' => 'dev-' . count($hdds) . '-' . $out[1], 'index' => $out[1]];
+ }
+ $hdd['partitions'][$out[1]][$out[2]] = $row['numeric'] ?? $row['value'];
+ } else {
+ $hdd[$row['prop']] = $row['numeric'] ?? $row['value'];
+ }
+ }
+ $result = [];
+ foreach ($hdds as $k => &$hdd) {
+ if (substr($hdd['dev'] ?? '/dev/sr', 0, 7) === '/dev/sr')
+ continue;
+ $hdd['devid'] = 'k' . $k;
+ $hdd['partitions'] = array_values($hdd['partitions']);
+ $result[] = $hdd;
+ }
+ return $result;
+ }
+
+ private static function mangleHdd(array &$hdd)
+ {
+ static $hddidx = 0;
+ if (!isset($hdd['size']) || !is_numeric($hdd['size'])) {
+ $hdd['size'] = 0;
+ }
+ $hdd['hddidx'] = $hddidx++;
+ $hours = $hdd['power_on_time//hours'] ?? $hdd['attr_9']['raw'] ?? $hdd['power_on_hours']
+ ?? $hdd['power_on_time']['hours'] ?? null;
+ if ($hours !== null) {
+ $hdd['PowerOnTime'] = '';
+ $val = (int)str_replace('.', '', $hours);
+ if ($val > 8760) {
+ $hdd['PowerOnTime'] .= floor($val / 8760) . 'Y, ';
+ $val %= 8760;
+ }
+ if ($val > 720) {
+ $hdd['PowerOnTime'] .= floor($val / 720) . 'M, ';
+ $val %= 720;
+ }
+ if ($val > 24) {
+ $hdd['PowerOnTime'] .= floor($val / 24) . 'd, ';
+ $val %= 24;
+ }
+ $hdd['PowerOnTime'] .= $val . 'h';
+ }
+ // Sort by start for building pie-chart
+ $xx = array_column($hdd['partitions'], 'start');
+ array_multisort($xx, SORT_ASC, SORT_NUMERIC,
+ $hdd['partitions']);
+ $used = 0;
+ $json = [];
+ $lastEnd = 0;
+ $minDisplaySize = $hdd['size'] / 150;
+ $i = 0;
+ foreach ($hdd['partitions'] as &$part) {
+ $dist = $part['start'] - $lastEnd;
+ if ($dist > $minDisplaySize) {
+ $json[] = ['value' => $dist, 'color' => '#aaa'];
+ $i++;
+ }
+ if ($part['size'] > $minDisplaySize) {
+ $json[] = ['value' => $part['size'], 'color' => self::typeToColor($part)];
+ $part['idx'] = $i++;
+ }
+ $part['size_s'] = Util::readableFileSize($part['size']);
+ $used += $part['size'];
+ $lastEnd = $part['start'] + $part['size'];
+ if (!isset($part['name']) || isset($part['slxtype'])) {
+ $part['name'] = self::partTypeToName($part['slxtype'] ?? $part['type']);
+ }
+ }
+ $dist = $hdd['size'] - $lastEnd;
+ if ($dist > $minDisplaySize) {
+ $json[] = ['value' => $dist, 'color' => '#aaa'];
+ }
+ $hdd['json'] = json_encode($json);
+ $hdd['size_s'] = Util::readableFileSize($hdd['size']);
+ if ($hdd['size'] - $used > 1000000000) {
+ $hdd['unused_s'] = Util::readableFileSize($hdd['size'] - $used);
+ }
+ // Finally sort by index for table display
+ array_multisort(array_column($hdd['partitions'], 'index'), SORT_ASC,
+ $hdd['partitions']);
+ }
+
+ private static function typeToColor(array $part): string
+ {
+ switch ($part['slxtype'] ?? $part['type']) {
+ case 44:
+ return '#5c1';
+ case 45:
+ return '#0d7';
+ case 82:
+ return '#48f';
+ }
+ return '#e55';
+ }
+
+ private static function eventToIconName($event): string
{
switch ($event) {
case 'session-open':
@@ -393,7 +679,7 @@ class SubPage
die(Render::parse('machine-bios-update', $reply));
}
- private static function checkBios($mainboard, $system, $date, $revision, $json = null)
+ private static function checkBios(string $mainboard, string $system, ?string $date, ?string $revision, $json = null)
{
if ($json === null) {
if (!file_exists(self::BIOS_CACHE) || filemtime(self::BIOS_CACHE) + 3600 < time())
@@ -402,18 +688,18 @@ class SubPage
}
if (!is_array($json) || !isset($json['system']))
return ['error' => 'Malformed JSON, no system key'];
- if (isset($json['system'][$system]) && isset($json['system'][$system]['fixes']) && isset($json['system'][$system]['match'])) {
+ if (isset($json['system'][$system]['fixes']) && isset($json['system'][$system]['match'])) {
$match =& $json['system'][$system];
- } elseif (isset($json['mainboard'][$mainboard]) && isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) {
+ } elseif (isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) {
$match =& $json['mainboard'][$mainboard];
} else {
return ['status' => 0];
}
$key = $match['match'];
- if ($key === 'revision') {
+ if ($key === 'revision' && $revision !== null) {
$cmp = function ($item) { $s = explode('.', $item); return $s[0] * 0x10000 + $s[1]; };
$reference = $cmp($revision);
- } elseif ($key === 'date') {
+ } elseif ($key === 'date' && $date !== null) {
$cmp = function ($item) { $s = explode('.', $item); return $s[2] * 10000 + $s[1] * 100 + $s[0]; };
$reference = $cmp($date);
} else {
@@ -442,4 +728,26 @@ class SubPage
return $retval;
}
-} \ No newline at end of file
+ /**
+ * @param string $type MBR-type or GPT UUID
+ * @return string Name of partition type if known, otherwise, $type is returned
+ */
+ private static function partTypeToName(string $type): string
+ {
+ switch ($type) {
+ case '44':
+ case '45':
+ return 'OpenSLX-ID' . $type;
+ case '82':
+ return 'Linux Swap';
+ case '83':
+ return 'Linux';
+ case '7':
+ return 'NTFS/Windows';
+ case 'ef':
+ return 'EFI';
+ }
+ return HardwareInfo::GPT[$type] ?? $type;
+ }
+
+}
diff --git a/modules-available/statistics/pages/projectors.inc.php b/modules-available/statistics/pages/projectors.inc.php
index cc808cf0..01be2971 100644
--- a/modules-available/statistics/pages/projectors.inc.php
+++ b/modules-available/statistics/pages/projectors.inc.php
@@ -16,7 +16,7 @@ class SubPage
User::assertPermission('hardware.projectors.edit');
$hwid = Request::post('hwid', false, 'int');
if ($hwid === false) {
- Util::traceError('Param hwid missing');
+ ErrorHandler::traceError('Param hwid missing');
}
if ($action === 'addprojector') {
Database::exec('INSERT IGNORE INTO statistic_hw_prop (hwid, prop, value)'
@@ -49,10 +49,10 @@ class SubPage
. " INNER JOIN statistic_hw_prop p ON (h.hwid = p.hwid AND p.prop = :projector)"
. " WHERE h.hwtype = :screen ORDER BY h.hwname ASC", array(
'projector' => 'projector',
- 'screen' => DeviceType::SCREEN,
+ 'screen' => HardwareInfo::SCREEN,
));
$data = array(
- 'projectors' => $res->fetchAll(PDO::FETCH_ASSOC)
+ 'projectors' => $res->fetchAll()
);
Render::addTemplate('projector-list', $data);
}
diff --git a/modules-available/statistics/pages/replace.inc.php b/modules-available/statistics/pages/replace.inc.php
index 9c16aed7..50bfd6cf 100644
--- a/modules-available/statistics/pages/replace.inc.php
+++ b/modules-available/statistics/pages/replace.inc.php
@@ -5,6 +5,7 @@ class SubPage
public static function doPreprocess()
{
+ User::assertPermission('replace');
$action = Request::post('action', false, 'string');
if ($action === 'replace') {
self::handleReplace();
@@ -17,11 +18,13 @@ class SubPage
private static function handleReplace()
{
$replace = Request::post('replace', false, 'array');
- if ($replace === false || empty($replace)) {
+ if (empty($replace)) {
Message::addError('main.parameter-empty', 'replace');
return;
}
$list = [];
+ $allowed = User::getAllowedLocations('replace');
+ // Loop through passed machines, filter out unsuited pairs (both in use) and those without permission
foreach ($replace as $p) {
$split = explode('x', $p);
if (count($split) !== 2) {
@@ -29,13 +32,13 @@ class SubPage
continue;
}
$entry = ['old' => $split[0], 'new' => $split[1]];
- $old = Database::queryFirst('SELECT lastseen FROM machine WHERE machineuuid = :old',
+ $old = Database::queryFirst('SELECT locationid, lastseen FROM machine WHERE machineuuid = :old',
['old' => $entry['old']]);
if ($old === false) {
Message::addError('unknown-machine', $entry['old']);
continue;
}
- $new = Database::queryFirst('SELECT firstseen FROM machine WHERE machineuuid = :new',
+ $new = Database::queryFirst('SELECT locationid, firstseen FROM machine WHERE machineuuid = :new',
['new' => $entry['new']]);
if ($new === false) {
Message::addError('unknown-machine', $entry['new']);
@@ -45,6 +48,16 @@ class SubPage
Message::addWarning('ignored-both-in-use', $entry['old'], $entry['new']);
continue;
}
+ if (!in_array(0, $allowed)) {
+ if (!in_array($old['locationid'], $allowed)) {
+ Message::addWarning('ignored-no-permission', $entry['old']);
+ continue;
+ }
+ if (!in_array($new['locationid'], $allowed)) {
+ Message::addWarning('ignored-no-permission', $entry['new']);
+ continue;
+ }
+ }
$entry['datelimit'] = min($new['firstseen'], $old['lastseen']);
$list[] = $entry;
}
@@ -106,7 +119,10 @@ class SubPage
FROM machine old INNER JOIN machine new ON (old.clientip = new.clientip AND old.lastseen < new.firstseen AND old.lastseen > $oldCutoff AND new.firstseen > $newCutoff)
ORDER BY oldhost ASC, oldip ASC");
$list = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $allowed = User::getAllowedLocations('replace');
+ foreach ($res as $row) {
+ if (!in_array(0, $allowed) && (!in_array($row['oldlid'], $allowed) || !in_array($row['newlid'], $allowed)))
+ continue;
$row['oldlastseen_s'] = Util::prettyTime($row['oldlastseen']);
$row['newfirstseen_s'] = Util::prettyTime($row['newfirstseen']);
$list[] = $row;
diff --git a/modules-available/statistics/pages/summary.inc.php b/modules-available/statistics/pages/summary.inc.php
index ce67070e..905f5d90 100644
--- a/modules-available/statistics/pages/summary.inc.php
+++ b/modules-available/statistics/pages/summary.inc.php
@@ -8,6 +8,9 @@ class SubPage
public static function doPreprocess()
{
User::assertPermission('view.summary');
+ if (!Module::isAvailable('js_chart')) {
+ ErrorHandler::traceError('js_chart not available');
+ }
}
public static function doRender()
@@ -23,7 +26,9 @@ class SubPage
// Prepare chart colors
self::$STATS_COLORS = [];
for ($i = 0; $i < 10; ++$i) {
- self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex((($i + 1) * ($i + 1)) / .3922), dechex(abs((5 - $i) * 51)));
+ self::$STATS_COLORS[] = '#55' . sprintf('%02s%02s', dechex(
+ (int)((($i + 1) * ($i + 1)) / .3922)),
+ dechex((int)(abs((5 - $i) * 51))));
}
$filterSet->filterNonClients();
@@ -38,10 +43,7 @@ class SubPage
Render::closeTag('div');
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showSummary($filterSet)
+ private static function showSummary(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$known = Database::queryFirst("SELECT Count(*) AS val FROM machine m $join WHERE $where", $args);
@@ -53,36 +55,82 @@ class SubPage
} else {
$usedpercent = 0;
}
- $data = array(
+ $data = [
'known' => $known['val'],
'online' => $on['val'],
'used' => $used['val'],
'usedpercent' => $usedpercent,
'badhdd' => $hdd['val'],
- );
+ ];
// Graph
- $cutoff = time() - 2 * 86400;
- $res = Database::simpleQuery("SELECT dateline, data FROM statistic WHERE typeid = '~stats' AND dateline > $cutoff ORDER BY dateline ASC");
- $labels = array();
- $points1 = array('data' => array(), 'label' => 'Online', 'fillColor' => '#efe', 'strokeColor' => '#aea', 'pointColor' => '#7e7', 'pointStrokeColor' => '#fff', 'pointHighlightFill' => '#fff', 'pointHighlightStroke' => '#7e7');
- $points2 = array('data' => array(), 'label' => 'In use', 'fillColor' => '#fee', 'strokeColor' => '#eaa', 'pointColor' => '#e77', 'pointStrokeColor' => '#fff', 'pointHighlightFill' => '#fff', 'pointHighlightStroke' => '#e77');
- $sum = 0;
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $x = explode('#', $row['data']);
- if ($sum === 0) {
- $labels[] = date('H:i', $row['dateline']);
+ $labels = [];
+ $points1 = [];
+ $points2 = [];
+ $lectures = [];
+ // Get locations
+ if ($filterSet->suitableForUsageGraph()) {
+ $locFilter = $filterSet->hasFilter('LocationStatisticsFilter');
+ if ($locFilter === null
+ || ($locFilter->op === '~' && ($locFilter->argument == 0
+ || (is_array($locFilter->argument) && in_array(0, $locFilter->argument))))) {
+ $locations = null;
+ $op = null;
+ } elseif ($locFilter->op === '~') {
+ $locations = array_keys(Location::getRecursiveFlat($locFilter->argument));
+ $op = $locFilter->op;
} else {
- $x[1] = max($x[1], array_pop($points1['data']));
- $x[2] = max($x[2], array_pop($points2['data']));
+ if (is_array($locFilter->argument)) {
+ $locations = $locFilter->argument;
+ } else {
+ $locations = [$locFilter->argument];
+ }
+ $op = $locFilter->op;
}
- $points1['data'][] = $x[1];
- $points2['data'][] = $x[2];
- ++$sum;
- if ($sum === 12) {
- $sum = 0;
+ //error_log($op . ' ' . print_r($locations, true));
+ $cutoff = time() - 2 * 86400;
+ $res = Database::simpleQuery("SELECT dateline, data FROM statistic
+ WHERE typeid = '~stats' AND dateline > $cutoff ORDER BY dateline DESC");
+ // Get max from 4 consecutive values, which should be 4*5 = 20m
+ $sum = 0;
+ foreach ($res as $row) {
+ if ($row['data'][0] === '{') {
+ $x = json_decode($row['data'], true);
+ if (!is_array($x) || !isset($x['usage']))
+ continue;
+ $x = self::mangleStatsJson($x, $locations, $op);
+ } else if ($locations === null) {
+ $x = explode('#', $row['data']);
+ if (count($x) < 3)
+ continue;
+ $x[] = 0;
+ } else {
+ continue;
+ }
+ if ($sum % 4 === 0) {
+ $labels[] = date('H:i', $row['dateline']);
+ } else {
+ $x[1] = max($x[1], array_pop($points1));
+ $x[2] = max($x[2], array_pop($points2));
+ $x[3] += array_pop($lectures);
+ }
+ $points1[] = $x[1];
+ $points2[] = $x[2];
+ $lectures[] = $x[3];
+ ++$sum;
}
}
- $data['json'] = json_encode(array('labels' => $labels, 'datasets' => array($points1, $points2)));
+ if (!empty($points1) && max($points1) > 0) {
+ $labels = array_reverse($labels);
+ $points1 = array_reverse($points1);
+ $points2 = array_reverse($points2);
+ $lectures = array_reverse($lectures);
+ $data['json'] = json_encode(['labels' => $labels,
+ 'datasets' => [
+ ['data' => $points1, 'label' => 'Online', 'borderColor' => '#8f3'],
+ ['data' => $points2, 'label' => 'In use', 'borderColor' => '#e76'],
+ ]]);
+ $data['markings'] = json_encode($lectures);
+ }
if (Module::get('runmode') !== false) {
$res = Database::queryFirst('SELECT Count(*) AS cnt FROM machine m INNER JOIN runmode r USING (machineuuid)'
. " $join WHERE $where", $args);
@@ -92,74 +140,71 @@ class SubPage
Render::addTemplate('summary', $data);
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showSystemModels($filterSet)
+ private static function showSystemModels(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery('SELECT systemmodel, Round(AVG(realcores)) AS cores, Count(*) AS `count` FROM machine m'
. " $join WHERE $where GROUP BY systemmodel ORDER BY `count` DESC, systemmodel ASC", $args);
- $lines = array();
- $json = array();
+ $lines = [];
+ $json = [];
$id = 0;
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if (empty($row['systemmodel'])) {
continue;
}
settype($row['count'], 'integer');
- $row['id'] = 'systemid' . $id;
$row['urlsystemmodel'] = urlencode($row['systemmodel']);
+ $row['idx'] = count($lines);
$lines[] = $row;
- $json[] = array(
+ $json[] = [
'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)],
- 'label' => 'systemid' . $id,
'value' => $row['count'],
- );
+ ];
++$id;
}
self::capChart($json, $lines, 0.92);
- Render::addTemplate('cpumodels', array('rows' => $lines, 'json' => json_encode($json)));
+ Render::addTemplate('cpumodels', ['rows' => $lines, 'json' => json_encode($json)]);
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showMemory($filterSet)
+ private static function alignBySteps(int $value, array $steps): int
+ {
+ for ($i = 1; $i < count($steps); ++$i) {
+ if ($steps[$i] < $value) {
+ continue;
+ }
+ if ($steps[$i] - $value >= $value - $steps[$i - 1]) {
+ --$i;
+ }
+ return $steps[$i];
+ }
+ return $value;
+ }
+
+ private static function showMemory(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery("SELECT mbram, Count(*) AS `count` FROM machine m $join
WHERE $where GROUP BY mbram", $args);
- $lines = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $gb = (int)ceil($row['mbram'] / 1024);
- for ($i = 1; $i < count(StatisticsFilter::SIZE_RAM); ++$i) {
- if (StatisticsFilter::SIZE_RAM[$i] < $gb) {
- continue;
- }
- if (StatisticsFilter::SIZE_RAM[$i] - $gb >= $gb - StatisticsFilter::SIZE_RAM[$i - 1]) {
- --$i;
- }
- $gb = StatisticsFilter::SIZE_RAM[$i];
- break;
- }
- if (isset($lines[$gb])) {
- $lines[$gb] += $row['count'];
- } else {
- $lines[$gb] = $row['count'];
- }
+ $lines = [];
+ foreach ($res as $row) {
+ $gb = self::alignBySteps((int)ceil($row['mbram'] / 1024), StatisticsFilter::SIZE_RAM);
+ $lines[$gb] = ($lines[$gb] ?? 0) + $row['count'];
}
asort($lines);
- $data = array('rows' => array());
- $json = array();
+ $data = ['rows' => []];
+ $json = [];
$id = 0;
foreach (array_reverse($lines, true) as $k => $v) {
- $data['rows'][] = array('gb' => $k, 'count' => $v, 'class' => StatisticsStyling::ramColorClass($k * 1024));
- $json[] = array(
+ $data['rows'][] = [
+ 'idx' => count($data['rows']),
+ 'gb' => $k,
+ 'count' => $v,
+ 'class' => StatisticsStyling::ramColorClass($k * 1024),
+ ];
+ $json[] = [
'color' => self::$STATS_COLORS[$id % count(self::$STATS_COLORS)],
- 'label' => (string)$k,
'value' => $v,
- );
+ ];
++$id;
}
self::capChart($json, $data['rows'], 0.92);
@@ -167,62 +212,47 @@ class SubPage
Render::addTemplate('memory', $data);
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showKvmState($filterSet)
+ private static function showKvmState(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
- $colors = array('UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6');
+ $colors = ['UNKNOWN' => '#666', 'UNSUPPORTED' => '#ea5', 'DISABLED' => '#e55', 'ENABLED' => '#6d6'];
$res = Database::simpleQuery("SELECT kvmstate, Count(*) AS `count` FROM machine m $join
WHERE $where GROUP BY kvmstate ORDER BY `count` DESC", $args);
- $lines = array();
- $json = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $lines = [];
+ $json = [];
+ foreach ($res as $row) {
+ $row['idx'] = count($lines);
$lines[] = $row;
$json[] = array(
- 'color' => isset($colors[$row['kvmstate']]) ? $colors[$row['kvmstate']] : '#000',
- 'label' => $row['kvmstate'],
+ 'color' => $colors[$row['kvmstate']] ?? '#000',
'value' => $row['count'],
);
}
Render::addTemplate('kvmstate', array('rows' => $lines, 'json' => json_encode($json)));
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showId44($filterSet)
+ private static function showId44(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$res = Database::simpleQuery("SELECT id44mb, Count(*) AS `count` FROM machine m $join WHERE $where GROUP BY id44mb", $args);
$lines = array();
$total = 0;
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$total += $row['count'];
- $gb = (int)ceil($row['id44mb'] / 1024);
- for ($i = 1; $i < count(StatisticsFilter::SIZE_ID44); ++$i) {
- if (StatisticsFilter::SIZE_ID44[$i] < $gb) {
- continue;
- }
- if (StatisticsFilter::SIZE_ID44[$i] - $gb >= $gb - StatisticsFilter::SIZE_ID44[$i - 1]) {
- --$i;
- }
- $gb = StatisticsFilter::SIZE_ID44[$i];
- break;
- }
- if (isset($lines[$gb])) {
- $lines[$gb] += $row['count'];
- } else {
- $lines[$gb] = $row['count'];
- }
+ $gb = self::alignBySteps((int)ceil($row['id44mb'] / 1024), StatisticsFilter::SIZE_PARTITION);
+ $lines[$gb] = ($lines[$gb] ?? 0) + $row['count'];
}
asort($lines);
$data = array('rows' => array());
$json = array();
$id = 0;
foreach (array_reverse($lines, true) as $k => $v) {
- $data['rows'][] = array('gb' => $k, 'count' => $v, 'class' => StatisticsStyling::hddColorClass($k));
+ $data['rows'][] = [
+ 'idx' => count($data['rows']),
+ 'gb' => $k,
+ 'count' => $v,
+ 'class' => StatisticsStyling::hddColorClass($k),
+ ];
if ($k === 0) {
$color = '#e55';
} else {
@@ -230,7 +260,6 @@ class SubPage
}
$json[] = array(
'color' => $color,
- 'label' => (string)$k,
'value' => $v,
);
}
@@ -239,19 +268,18 @@ class SubPage
Render::addTemplate('id44', $data);
}
- /**
- * @param \StatisticsFilterSet $filterSet
- */
- private static function showLatestMachines($filterSet)
+ private static function showLatestMachines(StatisticsFilterSet $filterSet): void
{
$filterSet->makeFragments($where, $join, $args);
$args['cutoff'] = ceil(time() / 3600) * 3600 - 86400 * 10;
- $res = Database::simpleQuery("SELECT machineuuid, clientip, hostname, firstseen, mbram, kvmstate, id44mb FROM machine m $join"
- . " WHERE firstseen > :cutoff AND $where ORDER BY firstseen DESC LIMIT 32", $args);
+ $res = Database::simpleQuery("SELECT m.machineuuid, m.clientip, m.hostname, m.firstseen, m.mbram, m.kvmstate, m.id44mb
+ FROM machine m $join
+ WHERE firstseen > :cutoff AND $where
+ ORDER BY firstseen DESC LIMIT 32", $args);
$rows = array();
$count = 0;
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if (empty($row['hostname'])) {
$row['hostname'] = $row['clientip'];
}
@@ -259,9 +287,9 @@ class SubPage
$row['firstseen'] = Util::prettyTime($row['firstseen']);
$row['gbram'] = round(round($row['mbram'] / 500) / 2, 1); // Trial and error until we got "expected" rounding..
$row['gbtmp'] = round($row['id44mb'] / 1024);
- $row['ramclass'] = StatisticsStyling::ramColorClass($row['mbram']);
+ $row['ramclass'] = StatisticsStyling::ramColorClass((int)$row['mbram']);
$row['kvmclass'] = StatisticsStyling::kvmColorClass($row['kvmstate']);
- $row['hddclass'] = StatisticsStyling::hddColorClass($row['gbtmp']);
+ $row['hddclass'] = StatisticsStyling::hddColorClass((int)$row['gbtmp']);
$row['kvmicon'] = $row['kvmstate'] === 'ENABLED' ? '✓' : '✗';
if (++$count > 5) {
$row['collapse'] = 'collapse';
@@ -277,7 +305,7 @@ class SubPage
- private static function capChart(&$json, &$rows, $cutoff, $minSlice = 0.015)
+ private static function capChart(array &$json, array &$rows, float $cutoff, float $minSlice = 0.015): void
{
$total = 0;
foreach ($json as $entry) {
@@ -309,4 +337,28 @@ class SubPage
}
}
+ /**
+ * @param array $json decoded json ~stats data
+ * @param ?int[] $locations
+ */
+ private static function mangleStatsJson(array $json, ?array $locations, ?string $op): array
+ {
+ // Total, On, InUse, Lectures
+ $retval = [0, 0, 0, 0];
+ foreach ($json['usage'] as $lid => $data) {
+ $lid = (int)$lid;
+ if ($locations === null
+ || ($op === '!=' && !in_array($lid, $locations))
+ || ($op !== '!=' && in_array($lid, $locations))) {
+ $retval[0] += $data['t'];
+ $retval[1] += $data['o'] ?? 0;
+ $retval[2] += $data['u'] ?? 0;
+ if (isset($data['event'])) {
+ $retval[3] += 1;
+ }
+ }
+ }
+ return $retval;
+ }
+
}
diff --git a/modules-available/statistics/permissions/permissions.json b/modules-available/statistics/permissions/permissions.json
index 663a8dc4..a5823775 100644
--- a/modules-available/statistics/permissions/permissions.json
+++ b/modules-available/statistics/permissions/permissions.json
@@ -22,5 +22,11 @@
},
"view.list": {
"location-aware": true
+ },
+ "replace": {
+ "location-aware": true
+ },
+ "hints": {
+ "location-aware": true
}
} \ No newline at end of file
diff --git a/modules-available/statistics/style.css b/modules-available/statistics/style.css
index 7e1539ec..7bd60b44 100644
--- a/modules-available/statistics/style.css
+++ b/modules-available/statistics/style.css
@@ -93,3 +93,25 @@
50% { background: #f2dede }
100% { background: unset }
}
+
+.slx-right {
+ float: right;
+}
+@media (min-width: 1650px) {
+ .slx-right {
+ position: fixed;
+ right: 10px;
+ display: block;
+ min-width: 140px;
+ width: calc(100vw - 1550px);
+ float: none !important;
+ }
+}
+
+.infobox {
+ border: 1px solid #aaa;
+ background: #eee;
+ border-radius: 3px;
+ margin: 3px auto;
+ padding: 0 2px;
+} \ No newline at end of file
diff --git a/modules-available/statistics/templates/clientlist.html b/modules-available/statistics/templates/clientlist.html
index af349437..fcb98774 100644
--- a/modules-available/statistics/templates/clientlist.html
+++ b/modules-available/statistics/templates/clientlist.html
@@ -1,46 +1,23 @@
+<div class="slx-right">
+ {{{roomsvg}}}
+ {{#sidebar}}
+ <div class="infobox">{{.}}</div>
+ {{/sidebar}}
+</div>
+
<h2>{{lang_clientList}} ({{rowCount}})</h2>
+<button class="btn btn-default" type="button" data-toggle="modal"
+ data-target="#column-selector">{{lang_selectColumns}}</button>
+
+<div class="clearfix"></div>
<form method="post" action="?do=statistics" id="list-form">
<input type="hidden" name="token" value="{{token}}">
<input type="hidden" name="redirect" value="?{{redirect}}">
-<table class="stupidtable table table-condensed table-striped">
+<table id="client-list" class="stupidtable table table-condensed table-striped">
<thead>
- <tr>
- <td></td>
- <td></td>
- <td class="text-right">
- <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('lastseen')">
- <span class="glyphicon glyphicon-filter"></span>
- </button>
- </td>
- <td>
- <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('kvmstate')">
- <span class="glyphicon glyphicon-filter"></span>
- </button>
- </td>
- <td class="text-right">
- <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('gbram')">
- <span class="glyphicon glyphicon-filter"></span>
- </button>
- </td>
- <td class="text-right">
- <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('hddgb')">
- <span class="glyphicon glyphicon-filter"></span>
- </button>
- </td>
- <td>
- <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('realcores')">
- <span class="glyphicon glyphicon-filter"></span>
- </button>
- </td>
- <td>
- <button type="button" class="btn btn-default btn-xs" onclick="popupFilter('location')">
- <span class="glyphicon glyphicon-filter"></span>
- </button>
- </td>
- </tr>
- <tr>
+ <tr id="thead">
<th data-sort="string">
<div class="checkbox checkbox-inline">
<input type="checkbox" id="toggle-all">
@@ -48,13 +25,15 @@
</div>
{{lang_machine}}
</th>
- <th data-sort="ipv4">{{lang_address}}</th>
- <th data-sort="int" class="text-right">{{lang_lastSeen}}</th>
- <th data-sort="string">{{lang_kvmSupport}}</th>
- <th data-sort="int" class="text-right">{{lang_gbRam}}</th>
- <th data-sort="int" class="text-right">{{lang_tmpGb}}</th>
- <th data-sort="int">{{lang_cpuModel}}</th>
- <th data-sort="string">{{lang_location}}</th>
+ <th data-sort="ipv4" data-column="clientip">{{lang_address}}</th>
+ <th data-sort="int" data-column="nicspeed" class="text-right">{{lang_nicSpeed}}</th>
+ <th data-sort="int" data-column="lastseen" class="text-right">{{lang_lastSeen}}</th>
+ <th data-sort="string" data-column="kvmstate">{{lang_kvmSupport}}</th>
+ <th data-sort="int" data-column="gbram" class="text-right">{{lang_gbRam}}</th>
+ <th data-sort="int" data-column="hddgb" class="text-right">{{lang_tmpGb}}</th>
+ <th data-sort="int" data-column="persistentgb" class="text-right">{{lang_persistentPart}}</th>
+ <th data-sort="int" data-column="realcores">{{lang_cpuModel}}</th>
+ <th data-sort="string" data-column="location">{{lang_location}}</th>
</tr>
</thead>
<tbody>
@@ -118,22 +97,34 @@
{{/currentuser}}
</td>
<td data-sort-value="{{clientip}}">
- <b><a href="?do=Statistics&amp;show=list&amp;filters=clientip={{subnet}}/24">{{subnet}}.</a>{{lastoctet}}</b>
+ <b><a href="?do=statistics&amp;show=list&amp;filters=clientip={{subnet}}/24">{{subnet}}.</a>{{lastoctet}}</b>
<div class="mac text-nowrap">{{macaddr}}</div>
<div class="hidden ip">{{clientip}}</div>
</td>
+ <td data-sort-value="{{nic-speed}}" class="text-right">
+ {{nic-speed_s}}&thinsp;MBit/s
+ </td>
<td data-sort-value="{{lastseen_int}}" class="text-right text-nowrap">{{lastseen}}</td>
<td class="{{kvmclass}}">{{kvmstate}}</td>
- <td data-sort-value="{{gbram}}" class="text-right {{ramclass}}">{{gbram}}&thinsp;GiB</td>
- <td data-sort-value="{{gbtmp}}" class="text-right {{hddclass}}">
+ <td data-sort-value="{{mbram}}" class="text-right {{ramclass}}">{{gbram}}&thinsp;GiB</td>
+ <td data-sort-value="{{id44mb}}" class="text-right {{hddclass}}">
{{gbtmp}}&thinsp;GiB
{{#badsectors}}<div><span data-toggle="tooltip" title="{{lang_reallocatedSectors}}" data-placement="left">
<span class="glyphicon glyphicon-exclamation-sign"></span>
{{badsectors}}
</span></div>{{/badsectors}}
- {{#nohdd}}<div>
+ {{^hddcount}}<div>
<span class="glyphicon glyphicon-hdd red"></span>
- </div>{{/nohdd}}
+ </div>{{/hddcount}}
+ {{#hddcount}}<div>
+ <span class="badge">
+ <span class="glyphicon glyphicon-hdd"></span>
+ {{hddcount}}
+ </span>
+ </div>{{/hddcount}}
+ </td>
+ <td data-sort-value="{{id45mb}}" class="text-right">
+ {{gbpersist}}&thinsp;GiB
</td>
<td data-sort-value="{{realcores}}">{{lang_realCores}}: {{realcores}}<div class="small">{{cpumodel}}</div></td>
<td data-sort-value="{{location.sort}}">{{location.name}}</td>
@@ -146,7 +137,7 @@
<span class="glyphicon glyphicon-refresh"></span>
{{lang_reset}}
</button>
- <div class="btn-group">
+ <div class="btn-group dropup">
<button class="btn btn-default dropdown-toggle btn-machine-action" type="button" id="dropdownMenu1"
data-toggle="dropdown" aria-haspopup="true">
<span class="glyphicon glyphicon-list"></span>
@@ -170,10 +161,15 @@
data-target="#mac-list">
{{lang_uuid}}
</a></li>
+ <hr style="margin: 0px">
+ <li><a href="#" class="list-btn" data-what="hostname ip mac uuid" data-toggle="modal"
+ data-target="#mac-list">
+ {{lang_fullInfo}}
+ </a></li>
</ul>
</div>
{{#rebootcontrol}}
- <div class="btn-group">
+ <div class="btn-group dropup">
<button class="btn btn-default dropdown-toggle btn-machine-action" type="button" id="dropdownMenu2"
data-toggle="dropdown" aria-haspopup="true">
<span class="glyphicon glyphicon-list"></span>
@@ -208,12 +204,18 @@
{{lang_remoteExec}}
</button>
{{/canExec}}
+ {{#canBenchmark}}
+ <button type="submit" name="action" value="benchmark" class="btn btn-primary btn-machine-action">
+ <span class="glyphicon glyphicon-dashboard"></span>
+ {{lang_remoteSpeedcheck}}
+ </button>
+ {{/canBenchmark}}
</div>
</div>
</div>
{{/rebootcontrol}}
{{#canDelete}}
- <div class="btn-group">
+ <div class="btn-group dropup">
<button type="submit" name="action" value="delmachines" class="btn btn-danger btn-machine-action"
data-confirm="{{lang_sureDeletePermanent}}">
<span class="glyphicon glyphicon-trash"></span>
@@ -260,8 +262,25 @@
</div>
</div>
+<div class="modal" id="column-selector" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ </div>
+ <div class="modal-body"></div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default"
+ data-dismiss="modal">{{lang_close}}</button>
+ </div>
+ </div>
+ </div>
+</div>
+
<script type="application/javascript"><!--
+var lookupTable = {};
+
document.addEventListener("DOMContentLoaded", function () {
var $buttons = $('.btn-machine-action');
var $fn = function () {
@@ -302,17 +321,76 @@ document.addEventListener("DOMContentLoaded", function () {
$fn();
});
$('.list-btn').click(function() {
- var what = $(this).data('what');
+ var what = $(this).data('what').split(" ");
var $el = $('#mac-list-content');
$el.empty();
var result = '';
- var num = $('.machine').has('input[type=checkbox]:checked').find('.' + what).each(function() {
- var text = this.innerText;
- if (what === 'mac') text = text.replace(/-/g, ':');
- result += text + "\n";
+ var num = $('.machine').has('input[type=checkbox]:checked').each(function(index, element) {
+ what.forEach(function (w) {
+ $(element).find('.' + w).each(function() {
+ var text = this.innerText;
+ if (w === 'mac') text = text.replace(/-/g, ':');
+ result += text + "\t";
+ });
+ });
+ result += "\n";
}).length;
$el.text(result).prop('rows', Math.min(24, Math.max(5, num)));
});
+ // Generate list for column selection
+ var $cs = $('#column-selector .modal-body');
+ var $filters = $('<tr>');
+ $('#client-list > thead').prepend($filters);
+ var idx = 0;
+ $cs.empty();
+ $('#client-list > thead > tr#thead > th').each(function() {
+ idx++;
+ var $th = $(this);
+ var $td = $('<td>');
+ $filters.append($td);
+ var column = $th.data('column');
+ if (!column)
+ return;
+ $cs.append($('<div class="checkbox">')
+ .append($('<input id="shc-' + column + '" type="checkbox" onclick="toggleColumn(this, \'' + column + '\')" checked="checked">'))
+ .append($('<label for="shc-' + column + '">').text($th.text())));
+ $td.append($('<button type="button" class="btn btn-default btn-xs" onclick="popupFilter(\'' + column + '\')">'
+ + '<span class="glyphicon glyphicon-filter"></span></button>'));
+ lookupTable[column] = idx;
+ });
+ // Load previous visibility settings
+ var colConf;
+ if (window.localStorage && (colConf = window.localStorage.getItem('cl-col-conf'))) {
+ colConf = JSON.parse(colConf);
+ if (colConf) {
+ for (var k in colConf) {
+ if (k.substring(0, 4) === 'shc-' && colConf[k]) {
+ var $cb = $('#' + k);
+ if ($cb.prop('checked')) {
+ $cb.click();
+ }
+ }
+ }
+ }
+ }
});
+function toggleColumn(e, column)
+{
+ var $el = $(e);
+ if (!(column in lookupTable))
+ return;
+ var idx = lookupTable[column];
+ $('#client-list tr > td:nth-child(' + idx + '), #client-list tr > th:nth-child(' + idx + ')')
+ .css('display', $el.is(':checked') ? '' : 'none');
+ var data = {};
+ $('#column-selector .modal-body .checkbox input').each(function() {
+ var $el = $(this);
+ if (!$el.is(':checked')) {
+ data[$el[0].id] = 1;
+ }
+ });
+ window.localStorage.setItem('cl-col-conf', JSON.stringify(data));
+}
+
//--></script>
diff --git a/modules-available/statistics/templates/cpumodels.html b/modules-available/statistics/templates/cpumodels.html
index 91464031..e133bec6 100644
--- a/modules-available/statistics/templates/cpumodels.html
+++ b/modules-available/statistics/templates/cpumodels.html
@@ -16,7 +16,7 @@
</thead>
<tbody>
{{#rows}}
- <tr id="{{id}}" class="{{collapse}}">
+ <tr id="sysmdl-{{idx}}" class="{{collapse}}">
<td data-sort-value="{{systemmodel}}" class="text-left text-nowrap filter-col" data-filter-col="systemmodel">
<table class="slx-ellipsis"><tr><td>
<a class="filter-val" data-filter-val="{{systemmodel}}" href="#">{{systemmodel}}</a>
@@ -40,28 +40,7 @@
</tbody>
</table>
</div>
- <div class="col-md-4">
- <canvas id="cpumodelchart" style="width:100%;height:380px"></canvas>
- <script type="text/javascript">
- document.addEventListener("DOMContentLoaded", function() {
- var data = {{{json}}};
- var sel = false;
- new Chart(document.getElementById('cpumodelchart').getContext('2d')).Pie(data, {
- animation: false,
- tooltipTemplate: "<%if (label){%><%=label%><%}%>",
- customTooltips: function(tooltip) {
- if (sel !== false) sel.removeClass('slx-bold');
- if (!tooltip) {
- sel = false;
- return;
- }
- sel = $('#' + tooltip.text);
- sel.addClass('slx-bold');
- }
- });
- }, false);
- </script>
- </div>
+ <div class="col-md-4 auto-chart" data-chart="{{json}}" data-chart-dest="#sysmdl-"></div>
</div>
</div>
</div>
diff --git a/modules-available/statistics/templates/filterbox.html b/modules-available/statistics/templates/filterbox.html
index e7c1cd9b..f62c4d7c 100644
--- a/modules-available/statistics/templates/filterbox.html
+++ b/modules-available/statistics/templates/filterbox.html
@@ -106,47 +106,23 @@ document.addEventListener("DOMContentLoaded", function () {
e.find('.filter-val').each(function(idx, elem) {
var e = $(elem);
var val = e.data('filter-val');
+ var op = e.data('filter-op');
+ if (!op) op = '=';
if (val === null || val === undefined) return;
e.click(function(ev) {
ev.preventDefault();
- addFilter(col, '=', val);
+ addFilter(col, op, val);
refresh();
});
});
});
+ $('.auto-chart').each(function() {
+ makePieChart($(this));
+ });
+
}, false);
-function popupFilter(field) {
- var $row = addFilter(field, null, null);
- if ($row !== null) {
- $row.find('.arg').focus();
- $row.removeClass('slx-focus')
- setTimeout(function() { $row.addClass('slx-focus'); }, 10);
- }
-}
-
-function addFilter(field, op, argument) {
- if (field === null)
- return null;
- var $row = $('#filter-' + field);
- if ($row.length === 0)
- return null;
- if (argument !== null) {
- $row.find('.op').val(op);
- $row.find('.arg').val(argument);
- }
- // Enable checkbox only if we got a predefined value, or if argument is in a select, as the user might want the preselected item and doesn't notice the checkbox is unchecked
- if (argument !== null || $row.find('select.arg').length !== 0) {
- $row.find('.filter-enable').prop('checked', true);
- }
- $row.show();
- return $row;
-}
-
-function refresh() {
- $('#query-form').submit();
-}
// --></script>
diff --git a/modules-available/statistics/templates/hints-cpu-legacy.html b/modules-available/statistics/templates/hints-cpu-legacy.html
new file mode 100644
index 00000000..44a5b166
--- /dev/null
+++ b/modules-available/statistics/templates/hints-cpu-legacy.html
@@ -0,0 +1,28 @@
+<h2>{{lang_legacyCpuVmx}}</h2>
+
+<p>{{lang_legacyCpuVmxText}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_cpuModel}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <span class="glyphicon {{icon}}"></span>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td class="text-nowrap">
+ {{cpumodel}}
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+</table> \ No newline at end of file
diff --git a/modules-available/statistics/templates/hints-hdd-grow.html b/modules-available/statistics/templates/hints-hdd-grow.html
new file mode 100644
index 00000000..b7c5bff4
--- /dev/null
+++ b/modules-available/statistics/templates/hints-hdd-grow.html
@@ -0,0 +1,67 @@
+<h2>{{lang_hddUnused}}</h2>
+
+<p>{{lang_hddUnusedId44}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_unused}}</th>
+ <th>{{lang_id44size}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#id44}}
+ <tr>
+ <td>
+ <span class="glyphicon {{icon}}"></span>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td>
+ {{unused_s}}
+ </td>
+ <td>
+ {{id44mb_s}}
+ </td>
+ </tr>
+ {{/id44}}
+ </tbody>
+</table>
+
+<p>{{lang_hddUnusedId45}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_unused}}</th>
+ <th>{{lang_id45size}}</th>
+ <th>{{lang_id44size}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#id45}}
+ <tr>
+ <td>
+ <span class="glyphicon {{icon}}"></span>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td>
+ {{unused_s}}
+ </td>
+ <td>
+ {{id45mb_s}}
+ </td>
+ <td>
+ {{id44mb_s}}
+ </td>
+ </tr>
+ {{/id45}}
+ </tbody>
+</table>
diff --git a/modules-available/statistics/templates/hints-nic-speed.html b/modules-available/statistics/templates/hints-nic-speed.html
new file mode 100644
index 00000000..963213cd
--- /dev/null
+++ b/modules-available/statistics/templates/hints-nic-speed.html
@@ -0,0 +1,32 @@
+<h2>{{lang_nicSlowSpeed}}</h2>
+
+<p>{{lang_nicSlowSpeedText}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_nicSpeed}}</th>
+ <th>{{lang_nicDuplex}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <span class="glyphicon {{icon}}"></span>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td class="text-nowrap">
+ {{nic-speed}}&thinsp;{{lang_MbitPerSecond}}
+ </td>
+ <td>
+ {{nic-duplex}}
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+</table> \ No newline at end of file
diff --git a/modules-available/statistics/templates/hints-ram-underclocked.html b/modules-available/statistics/templates/hints-ram-underclocked.html
new file mode 100644
index 00000000..35bdd857
--- /dev/null
+++ b/modules-available/statistics/templates/hints-ram-underclocked.html
@@ -0,0 +1,49 @@
+<h2>{{lang_ramUnderclocked}}</h2>
+
+<p>{{lang_ramUnderclockedText}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_type}}</th>
+ <th>{{lang_speedCurrent}}</th>
+ <th>{{lang_speedDesign}}</th>
+ <th>{{lang_manufacturer}}</th>
+ <th>{{lang_serialNo}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <span class="glyphicon {{icon}}"></span>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td class="text-nowrap">
+ {{Type}} {{Form Factor}}
+ <div>{{Size}}</div>
+ </td>
+ <td class="text-nowrap">
+ {{Configured Memory Speed}}
+ </td>
+ <td class="text-nowrap">
+ {{Speed}}
+ </td>
+ <td class="text-nowrap">
+ {{Manufacturer}}
+ </td>
+ <td>
+ {{Serial Number}}
+ </td>
+ <td class="text-right">
+ <span class="badge">{{group_count}}</span>
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+</table>
diff --git a/modules-available/statistics/templates/hints-ram-upgrade.html b/modules-available/statistics/templates/hints-ram-upgrade.html
new file mode 100644
index 00000000..7b60d419
--- /dev/null
+++ b/modules-available/statistics/templates/hints-ram-upgrade.html
@@ -0,0 +1,32 @@
+<h2>{{lang_ramUpgrade}}</h2>
+
+<p>{{lang_ramUpgradeText}}</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_machine}}</th>
+ <th>{{lang_installedCountMax}}</th>
+ <th>{{lang_ramSizeCurrentMax}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <span class="glyphicon {{icon}}"></span>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td class="{{count_class}}">
+ {{Memory Slot Occupied}} / {{Memory Slot Count}}
+ </td>
+ <td class="{{size_class}}">
+ {{Memory Installed Capacity}} / {{Memory Maximum Capacity}}
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+</table> \ No newline at end of file
diff --git a/modules-available/statistics/templates/id44.html b/modules-available/statistics/templates/id44.html
index ec0dac09..7851ba87 100644
--- a/modules-available/statistics/templates/id44.html
+++ b/modules-available/statistics/templates/id44.html
@@ -15,9 +15,9 @@
</thead>
<tbody>
{{#rows}}
- <tr id="tmpid{{gb}}" class="{{class}} {{collapse}}">
+ <tr id="id44-{{idx}}" class="{{class}} {{collapse}}">
<td data-sort-value="{{gb}}" class="text-left text-nowrap">
- <a class="filter-val" data-filter-val="{{gb}}" href="#">{{gb}}&thinsp;GiB</a>
+ <a class="filter-val" data-filter-val="{{gb}}" data-filter-op="~" href="#">{{gb}}&thinsp;GiB</a>
</td>
<td class="text-right">{{count}}</td>
</tr>
@@ -34,28 +34,7 @@
</tbody>
</table>
</div>
- <div class="col-sm-6">
- <canvas id="temppartchart" style="width:100%;height:250px"></canvas>
- <script type="text/javascript">
- document.addEventListener("DOMContentLoaded", function() {
- var data = {{{json}}};
- var sel = false;
- new Chart(document.getElementById('temppartchart').getContext('2d')).Pie(data, {
- animation: false,
- tooltipTemplate: "<%if (label){%><%=label%><%}%>",
- customTooltips: function(tooltip) {
- if (sel !== false) sel.removeClass('slx-bold');
- if (!tooltip) {
- sel = false;
- return;
- }
- sel = $('#tmpid' + String(tooltip.text));
- sel.addClass('slx-bold');
- }
- });
- }, false);
- </script>
- </div>
+ <div class="col-sm-6 auto-chart" data-chart="{{json}}" data-chart-dest="#id44-"></div>
</div>
</div>
</div>
diff --git a/modules-available/statistics/templates/js-pciquery.html b/modules-available/statistics/templates/js-pciquery.html
new file mode 100644
index 00000000..5d4df867
--- /dev/null
+++ b/modules-available/statistics/templates/js-pciquery.html
@@ -0,0 +1,24 @@
+<script>
+ document.addEventListener('DOMContentLoaded', function() {
+ var missing = {{{missing_ids}}};
+ var doQuery = function() {
+ if (missing && missing.length > 0) {
+ $.ajax({
+ url: '?do=statistics', dataType: "json", method: "POST", data: {
+ token: TOKEN,
+ action: 'json-lookup',
+ list: missing.splice(0, 10) // Query 10 at a time max
+ }
+ }).done(function (data) {
+ if (!data)
+ return;
+ for (var k in data) {
+ $('.query-' + k.replace(':', '-')).text(data[k]);
+ }
+ doQuery();
+ });
+ }
+ }
+ doQuery();
+ });
+</script> \ No newline at end of file
diff --git a/modules-available/statistics/templates/kvmstate.html b/modules-available/statistics/templates/kvmstate.html
index 4f8994d1..b3c65733 100644
--- a/modules-available/statistics/templates/kvmstate.html
+++ b/modules-available/statistics/templates/kvmstate.html
@@ -15,7 +15,7 @@
</thead>
<tbody>
{{#rows}}
- <tr id="kvm{{kvmstate}}">
+ <tr id="kvm-{{idx}}">
<td data-sort-value="{{kvmstate}}" class="text-left text-nowrap">
<a class="filter-val" data-filter-val="{{kvmstate}}" href="#">{{kvmstate}}</a>
</td>
@@ -25,28 +25,7 @@
</tbody>
</table>
</div>
- <div class="col-sm-6">
- <canvas id="kvmchart" style="width:100%;height:250px"></canvas>
- <script type="text/javascript">
- document.addEventListener("DOMContentLoaded", function() {
- var data = {{{json}}};
- var sel = false;
- new Chart(document.getElementById('kvmchart').getContext('2d')).Pie(data, {
- animation: false,
- tooltipTemplate: "<%if (label){%><%=label%><%}%>",
- customTooltips: function(tooltip) {
- if (sel !== false) sel.removeClass('slx-bold');
- if (!tooltip) {
- sel = false;
- return;
- }
- sel = $('#kvm' + tooltip.text);
- sel.addClass('slx-bold');
- }
- });
- }, false);
- </script>
- </div>
+ <div class="col-sm-6 auto-chart" data-chart="{{json}}" data-chart-dest="#kvm-"></div>
</div>
</div>
</div>
diff --git a/modules-available/statistics/templates/machine-hdds.html b/modules-available/statistics/templates/machine-hdds.html
index 4d0409f9..57786510 100644
--- a/modules-available/statistics/templates/machine-hdds.html
+++ b/modules-available/statistics/templates/machine-hdds.html
@@ -4,24 +4,27 @@
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
- <b>{{s_ModelFamily}}</b> {{dev}}
+ <b>{{model_family}}{{^model_family}}{{model}}{{/model_family}}</b> {{dev}}
</div>
<div class="panel-body">
- {{#s_DeviceModel}}
- <div>{{lang_modelNo}}: {{s_DeviceModel}}, {{lang_serialNo}}: {{s_SerialNumber}}</div>
- {{/s_DeviceModel}}
- {{#s_ReallocatedSectorCt}}
- <div class="red">{{lang_reallocatedSectors}}: {{s_ReallocatedSectorCt}}</div>
- {{/s_ReallocatedSectorCt}}
- {{#s_CurrentPendingSector}}
- <div class="red">{{lang_pendingSectors}}: {{s_CurrentPendingSector}}</div>
- {{/s_CurrentPendingSector}}
- {{#s_PowerOnHours}}
- <div>{{lang_powerOnTime}}: {{s_PowerOnHours}}&thinsp;{{lang_hours}} ({{PowerOnTime}})</div>
- {{/s_PowerOnHours}}
- {{#s_MediaandDataIntegrityErrors}}
- <div class="red">{{lang_mediaIntegrityErrors}}: {{s_MediaandDataIntegrityErrors}}</div>
- {{/s_MediaandDataIntegrityErrors}}
+ {{#model}}
+ <div>{{lang_modelNo}}: {{model}}, {{lang_serialNo}}: {{serial_number}}</div>
+ {{/model}}
+ {{#smart_status_failed}}
+ <div class="red">{{lang_smartSelfTestFailed}}</div>
+ {{/smart_status_failed}}
+ {{#attr_5.raw}}
+ <div class="red">{{lang_reallocatedSectors}}: {{attr_5.raw}}</div>
+ {{/attr_5.raw}}
+ {{#attr_197.raw}}
+ <div class="red">{{lang_pendingSectors}}: {{attr_197.raw}}</div>
+ {{/attr_197.raw}}
+ {{#PowerOnTime}}
+ <div>{{lang_powerOnTime}}: {{PowerOnTime}}</div>
+ {{/PowerOnTime}}
+ {{#media_errors}}
+ <div class="red">{{lang_mediaIntegrityErrors}}: {{media_errors}}</div>
+ {{/media_errors}}
<div class="row">
<div class="col-sm-7">
<table class="table table-condensed table-striped table-responsive">
@@ -31,40 +34,32 @@
<th>{{lang_partType}}</th>
</tr>
{{#partitions}}
- <tr id="{{id}}">
- <td>{{name}}</td>
- <td class="text-right text-nowrap">{{size}}&thinsp;GiB</td>
- <td>{{type}}</td>
+ <tr id="part-{{hddidx}}-{{idx}}">
+ <td>{{index}}</td>
+ <td class="text-right text-nowrap">{{size_s}}</td>
+ <td class="text-nowrap">
+ <table class="slx-ellipsis"><tr><td>{{name}}</td></tr></table>
+ </td>
</tr>
{{/partitions}}
</table>
- <div class="slx-bold">{{lang_total}}: {{size}}&thinsp;GiB</div>
+ <div class="slx-bold">{{lang_total}}: {{size_s}}</div>
+ {{#unused_s}}
+ <div class="slx-bold">{{lang_unused}}: {{unused_s}}</div>
+ {{/unused_s}}
</div>
- <div class="col-sm-5">
- <canvas id="{{devid}}-chart" style="width:100%;height:250px"></canvas>
- <script type="text/javascript">
- document.addEventListener("DOMContentLoaded", function() {
- var data = {{{json}}};
- var sel = false;
- new Chart(document.getElementById('{{devid}}-chart').getContext('2d')).Pie(data, {
- animation: false,
- tooltipTemplate: "<%if (label){%><%=label%><%}%>",
- customTooltips: function(tooltip) {
- if (sel !== false) sel.removeClass('info');
- if (!tooltip) {
- sel = false;
- return;
- }
- sel = $('#' + tooltip.text);
- sel.addClass('info');
- }
- });
- }, false);
- </script>
+ <div class="col-sm-5 auto-chart" data-chart="{{json}}" data-chart-dest="#part-{{hddidx}}-">
</div>
</div>
</div>
</div>
</div>
{{/hdds}}
+ <script type="text/javascript">
+ document.addEventListener("DOMContentLoaded", function() {
+ $('.auto-chart').each(function() {
+ makePieChart($(this));
+ });
+ });
+ </script>
</div> \ No newline at end of file
diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html
index 568099e0..be32f9c7 100644
--- a/modules-available/statistics/templates/machine-main.html
+++ b/modules-available/statistics/templates/machine-main.html
@@ -29,6 +29,12 @@
<td class="text-nowrap">{{lang_ip}}</td>
<td>{{clientip}}</td>
</tr>
+ {{#nic-speed}}
+ <tr>
+ <td class="text-nowrap">{{lang_nicSpeed}}</td>
+ <td>{{nic-speed}}&thinsp;MBit/s, {{lang_duplex}}: {{nic-duplex}}</td>
+ </tr>
+ {{/nic-speed}}
{{#hostname}}
<tr>
<td class="text-nowrap">{{lang_hostname}}</td>
@@ -41,7 +47,17 @@
</tr>
<tr>
<td class="text-nowrap">{{lang_lastBoot}}</td>
- <td>{{lastboot_s}}</td>
+ <td>
+ {{lastboot_s}}
+ {{#minilinux}}
+ <div>
+ {{lang_baseSystem}}: {{minilinux}}
+ {{#boottime_s}}
+ (<span title="{{lang_boottimeTooltip}}">{{boottime_s}}</span>)
+ {{/boottime_s}}
+ </div>
+ {{/minilinux}}
+ </td>
</tr>
<tr>
<td class="text-nowrap">{{lang_lastSeen}}</td>
@@ -88,19 +104,22 @@
</td>
</tr>
{{/modeid}}
- {{#hasroomplan}}
+ {{#roomsvg}}
<tr>
<td class="text-nowrap">
{{lang_roomplan}}
</td>
<td>
+ <div>
+ {{{roomsvg}}}
+ </div>
<a href="?do=roomplanner&amp;locationid={{locationid}}" target="_blank"
- onclick="window.open(this.href, '_blank', 'toolbar=0,scrollbars,resizable');return false">
- <img src="api.php?do=roomplanner&amp;show=svg&amp;locationid={{locationid}}&amp;machineuuid={{machineuuid}}&amp;fallback=1"/>
+ onclick="window.open(this.href, '_blank', 'toolbar=0,scrollbars,resizable');return false">
+ {{lang_edit}}
</a>
</td>
</tr>
- {{/hasroomplan}}
+ {{/roomsvg}}
{{#rebootcontrol}}
<tr>
<td class="text-nowrap">
@@ -219,12 +238,14 @@
<tr>
<td class="text-nowrap">{{lang_cpuModel}}</td>
<td>
- {{cpumodel}}
- {{#Sockets}}
+ <a href="?do=statistics&amp;show=list&amp;filter[cpumodel]=1&amp;op[cpumodel]=%3D&amp;arg[cpumodel]={{cpumodel}}">
+ {{cpumodel}}
+ </a>
+ {{#cpu-sockets}}
<div class="small">
- {{lang_sockets}}: {{Sockets}}, {{lang_cores}}: {{Realcores}}, {{lang_virtualCores}}: {{Virtualcores}}
+ {{lang_sockets}}: {{cpu-sockets}}, {{lang_cores}}: {{cpu-cores}}, {{lang_virtualCores}}: {{cpu-threads}}
</div>
- {{/Sockets}}
+ {{/cpu-sockets}}
{{#live_cpuload_s}}
<div class="meter">
<div class="text left">{{lang_cpuload}}</div>
@@ -243,13 +264,18 @@
</tr>
<tr>
<td class="text-nowrap">{{lang_pcmodel}}</td>
- <td>{{pcmodel}} ({{pcmanufacturer}})</td>
+ <td>
+ {{#system.Product Name}}
+ <a href="?do=statistics&amp;show=list&amp;filter[systemmodel]=1&amp;op[systemmodel]=%3D&amp;arg[systemmodel]={{system.Product Name}}+({{system.Manufacturer}})">
+ {{system.Product Name}} ({{system.Manufacturer}})
+ </a>
+ {{/system.Product Name}}
+ </td>
</tr>
<tr>
<td class="text-nowrap">{{lang_mobomodel}}</td>
- <td>{{mobomodel}} ({{mobomanufacturer}})</td>
+ <td>{{mainboard.Product Name}} ({{mainboard.Manufacturer}})</td>
</tr>
- {{#biosdate}}
<tr>
<td class="text-nowrap">
<div>{{lang_biosVersion}}</div>
@@ -257,19 +283,23 @@
</td>
<td class="text-nowrap">
<div id="bios-panel" class="pull-right"style="max-width:30%">{{{bioshtml}}}</div>
- <div>{{biosversion}} (<b>{{biosrevision}}</b>)</div>
- <div>{{biosdate}}</div>
+ <div>{{bios.Version}} (<b>{{bios.BIOS Revision}}</b>)</div>
+ <div>{{bios.Release Date}}</div>
</td>
</tr>
- {{/biosdate}}
<tr class="{{ramclass}}">
<td class="text-nowrap">{{lang_ram}}</td>
<td>
<div>
{{gbram}}&thinsp;GiB
- {{#maxram}}({{lang_maximumAbbrev}} {{maxram}}){{/maxram}}
- {{ramtype}}
+ {{#Memory Maximum Capacity}}
+ / {{lang_maximumAbbrev}} {{Memory Maximum Capacity}}
+ {{/Memory Maximum Capacity}}
+ {{#Memory Slot Count}}
+ ({{Memory Slot Count}} {{lang_slots}})
+ {{/Memory Slot Count}}
</div>
+ <div>{{ramtype}}</div>
{{#live_memsize}}
<div class="meter">
<div class="text left">{{lang_ram}}</div>
@@ -286,29 +316,64 @@
{{/live_swapsize}}
</td>
</tr>
- {{#extram}}
<tr>
- <td class="text-nowrap">{{lang_ramSlots}}</td>
- <td>
- {{ramslotcount}}:
- {{#ramslot}}
- [ <span title="{{manuf}}">{{size}}</span> ]
- {{/ramslot}}
+ <td colspan="2">
+ <table class="table-responsive slx-table text-nowrap">
+ <thead>
+ <tr class="small">
+ <td>{{lang_slot}}</td>
+ <td></td>
+ <td>{{lang_speed}}</td>
+ <td>{{lang_manufacturer}}</td>
+ <td>{{lang_serialNo}}</td>
+ </tr>
+ </thead>
+ {{#ram}}
+ {{#Speed}}
+ <tr>
+ <td>
+ {{Locator}},
+ {{Bank Locator}}
+ {{^Bank Locator}}{{#Set}}Set {{Set}}{{/Set}}{{/Bank Locator}}
+ </td>
+ <td class="slx-bold">{{Size}}</td>
+ <td>{{#Configured Memory Speed}}{{Configured Memory Speed}} / {{/Configured Memory Speed}}{{Speed}}</td>
+ <td>{{Manufacturer}}</td>
+ <td>{{Serial Number}}</td>
+ </tr>
+ {{/Speed}}
+ {{/ram}}
+ </table>
</td>
</tr>
- {{/extram}}
<tr class="{{hddclass}}">
- <td class="text-nowrap">{{lang_tempPart}}</td>
+ <td class="text-nowrap">{{lang_tempPartID}}
+ <div class="text-muted">
+ {{lang_tempPart}}
+ </div>
+ </td>
<td>
<div>
{{gbtmp}}&thinsp;GiB
</div>
{{#live_tmpsize}}
- <div class="meter">
- <div class="text right">{{live_tmpfree_s}} {{lang_free}}</div>
- <div class="bar" style="width:{{live_tmppercent}}%"></div>
- </div>
+ <div class="meter">
+ <div class="text right">{{live_tmpfree_s}} {{lang_free}}</div>
+ <div class="bar" style="width:{{live_tmppercent}}%"></div>
+ </div>
{{/live_tmpsize}}
+ </td>
+ </tr>
+ <tr>
+ <td class="text-nowrap">{{lang_persistentPartID}}
+ <div class="text-muted">
+ {{lang_persistentPart}}
+ </div>
+ </td>
+ <td>
+ <div>
+ {{gbid45}}&thinsp;GiB
+ </div>
{{#live_id45size}}
<div class="meter">
<div class="text right">{{live_id45free_s}} {{lang_free}}</div>
@@ -362,16 +427,28 @@
</table>
<h4>{{lang_devices}}</h4>
{{#lspci1}}
- <div><span class="{{lookupClass}}">{{class}}</span></div>
+ <div><span>{{class_s}}</span></div>
{{#entries}}
- <div class="small">&emsp;└ <span class="{{lookupVen}}">{{ven}}</span> <span class="{{lookupDev}}">{{dev}}</span></div>
+ <div class="small">
+ &emsp;└
+ <span class="badge">{{pt}}</span>
+ <span{{^vendor_s}} class="query-{{vendor}}"{{/vendor_s}}>{{vendor_s}}</span>
+ <span{{^device_s}} class="query-{{vendor}}-{{device}}"{{/device_s}}>{{device_s}}</span>
+ <a href="?do=passthrough&amp;show=hwlist#{{vendor}}-{{device}}">[{{vendor}}:{{device}}]</a>
+ </div>
{{/entries}}
{{/lspci1}}
<div id="lspci" class="collapse">
{{#lspci2}}
- <div><span class="{{lookupClass}}">{{class}}</span></div>
+ <div><span>{{class_s}}</span></div>
{{#entries}}
- <div class="small">&emsp;└ <span class="{{lookupVen}}">{{ven}}</span> <span class="{{lookupDev}}">{{dev}}</span></div>
+ <div class="small">
+ &emsp;└
+ <span class="badge">{{pt}}</span>
+ <span{{^vendor_s}} class="query-{{vendor}}"{{/vendor_s}}>{{vendor_s}}</span>
+ <span{{^device_s}} class="query-{{vendor}}-{{device}}"{{/device_s}}>{{device_s}}</span>
+ <a href="?do=passthrough&amp;show=hwlist#{{vendor}}-{{device}}">[{{vendor}}:{{device}}]</a>
+ </div>
{{/entries}}
{{/lspci2}}
</div>
@@ -380,13 +457,3 @@
</div>
</div>
</div>
-<script type="application/javascript"><!--
-document.addEventListener("DOMContentLoaded", function () {
- $('span.do-lookup').each(function () {
- $(this).load('?do=statistics&lookup=' + $(this).text());
- });
- {{#biosurl}}
- $('#bios-panel').load('{{{biosurl}}}');
- {{/biosurl}}
-}, false);
-// --></script>
diff --git a/modules-available/statistics/templates/memory.html b/modules-available/statistics/templates/memory.html
index f6f4c446..0ccbca98 100644
--- a/modules-available/statistics/templates/memory.html
+++ b/modules-available/statistics/templates/memory.html
@@ -15,9 +15,9 @@
</thead>
<tbody>
{{#rows}}
- <tr id="ramid{{gb}}" class="{{class}} {{collapse}}">
+ <tr id="ram-{{idx}}" class="{{class}} {{collapse}}">
<td class="text-left text-nowrap" data-sort-value="{{gb}}">
- <a class="filter-val" data-filter-val="{{gb}}" href="#">{{gb}}&thinsp;GiB</a>
+ <a class="filter-val" data-filter-val="{{gb}}" data-filter-op="~" href="#">{{gb}}&thinsp;GiB</a>
</td>
<td class="text-right">{{count}}</td>
</tr>
@@ -34,28 +34,7 @@
</tbody>
</table>
</div>
- <div class="col-sm-6">
- <canvas id="ramsizechart" style="width:100%;height:250px"></canvas>
- <script type="text/javascript">
- document.addEventListener("DOMContentLoaded", function() {
- var data = {{{json}}};
- var sel = false;
- new Chart(document.getElementById('ramsizechart').getContext('2d')).Pie(data, {
- animation: false,
- tooltipTemplate: "<%if (label){%><%=label%><%}%>",
- customTooltips: function(tooltip) {
- if (sel !== false) sel.removeClass('slx-bold');
- if (!tooltip) {
- sel = false;
- return;
- }
- sel = $('#ramid' + tooltip.text);
- sel.addClass('slx-bold');
- }
- });
- }, false);
- </script>
- </div>
+ <div class="col-sm-6 auto-chart" data-chart="{{json}}" data-chart-dest="#ram-"></div>
</div>
</div>
</div>
diff --git a/modules-available/statistics/templates/summary.html b/modules-available/statistics/templates/summary.html
index 3ede7bc5..751a9bed 100644
--- a/modules-available/statistics/templates/summary.html
+++ b/modules-available/statistics/templates/summary.html
@@ -23,17 +23,61 @@
</div>
</div>
<div>
+ {{#json}}
<canvas id="usagehist" style="width:100%;height:150px"></canvas>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
+
+ var markings = {{{markings}}};
+ var markMax = Math.max(...markings) * 3;
+ var showLegend = markMax > 0;
+ if (markMax < 8) markMax = 8;
+
+ var oldDraw = Chart.prototype._drawDatasets;
+
+ Chart.prototype._drawDatasets = function () {
+ if (this.chartArea) {
+ var ctx = this.ctx;
+ var chartArea = this.chartArea;
+
+ var meta = this.getDatasetMeta(0);
+
+ ctx.save();
+ var end = Math.min(meta.data.length, markings.length) - 1;
+ for (var i = 0; i < end; ++i) {
+ var start = meta.data[i].x;
+ var stop = meta.data[i+1].x;
+ ctx.fillStyle = 'rgba(16, 64, 255, ' + (!!markings[i] * .05 + markings[i] / markMax) + ')';
+ ctx.fillRect(start, chartArea.top, stop - start, chartArea.bottom - chartArea.top);
+ }
+ ctx.restore();
+ }
+
+ // Perform regular chart draw
+ oldDraw.call(this);
+ };
+
var data = {{{json}}};
var sel = false;
- new Chart(document.getElementById('usagehist').getContext('2d')).Line(data, {
+ new Chart(document.getElementById('usagehist').getContext('2d'), {type: 'line', data: data, options: {
+ responsive: true,
animation: false,
- pointHitDetectionRadius: 5
- });
+ pointRadius: 0,
+ pointHitRadius: 6,
+ interaction: { mode: 'index' },
+ plugins: {
+ subtitle: {
+ display: showLegend,
+ text: '{{lang_graphLectureTitle}}',
+ position: 'bottom',
+ },
+ legend: {position: 'left' },
+ }
+ }});
+
}, false);
</script>
+ {{/json}}
</div>
</div>