summaryrefslogtreecommitdiffstats
path: root/modules-available/statistics/pages
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/statistics/pages')
-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
6 files changed, 870 insertions, 203 deletions
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;
+ }
+
}