summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--inc/arrayutil.inc.php8
-rw-r--r--inc/crypto.inc.php3
-rw-r--r--inc/database.inc.php4
-rw-r--r--install.php2
-rw-r--r--modules-available/minilinux/inc/linuxbootentryhook.inc.php20
-rw-r--r--modules-available/passthrough/config.json7
-rw-r--r--modules-available/passthrough/inc/passthrough.inc.php53
-rw-r--r--modules-available/passthrough/install.inc.php9
-rw-r--r--modules-available/passthrough/page.inc.php130
-rw-r--r--modules-available/passthrough/permissions/permissions.json3
-rw-r--r--modules-available/passthrough/templates/hardware-list.html105
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php24
-rw-r--r--modules-available/statistics/api.inc.php50
-rw-r--r--modules-available/statistics/inc/hardwareinfo.inc.php85
-rw-r--r--modules-available/statistics/inc/hardwareparser.inc.php631
-rw-r--r--modules-available/statistics/inc/hardwareparserlegacy.inc.php284
-rw-r--r--modules-available/statistics/inc/hardwarequery.inc.php157
-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/install.inc.php67
-rw-r--r--modules-available/statistics/lang/de/messages.json1
-rw-r--r--modules-available/statistics/lang/de/module.json3
-rw-r--r--modules-available/statistics/lang/de/template-tags.json20
-rw-r--r--modules-available/statistics/lang/en/template-tags.json1
-rw-r--r--modules-available/statistics/page.inc.php78
-rw-r--r--modules-available/statistics/pages/hints.inc.php109
-rw-r--r--modules-available/statistics/pages/machine.inc.php293
-rw-r--r--modules-available/statistics/permissions/permissions.json3
-rw-r--r--modules-available/statistics/templates/hints-hdd-grow.html65
-rw-r--r--modules-available/statistics/templates/hints-ram-underclocked.html44
-rw-r--r--modules-available/statistics/templates/hints-ram-upgrade.html31
-rw-r--r--modules-available/statistics/templates/js-pciquery.html24
-rw-r--r--modules-available/statistics/templates/machine-hdds.html46
-rw-r--r--modules-available/statistics/templates/machine-main.html104
34 files changed, 2363 insertions, 593 deletions
diff --git a/inc/arrayutil.inc.php b/inc/arrayutil.inc.php
index 490b5a4f..c1306ac7 100644
--- a/inc/arrayutil.inc.php
+++ b/inc/arrayutil.inc.php
@@ -12,13 +12,7 @@ class ArrayUtil
*/
public static function flattenByKey(array $list, string $key)
{
- $ret = [];
- foreach ($list as $item) {
- if (array_key_exists($key, $item)) {
- $ret[] = $item[$key];
- }
- }
- return $ret;
+ return array_column($list, $key);
}
/**
diff --git a/inc/crypto.inc.php b/inc/crypto.inc.php
index 56f5073c..d3dd60dc 100644
--- a/inc/crypto.inc.php
+++ b/inc/crypto.inc.php
@@ -10,7 +10,8 @@ class Crypto
*/
public static function hash6($password)
{
- $salt = substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16);
+ $salt = substr(str_replace('+', '.',
+ base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16);
$hash = crypt($password, '$6$' . $salt);
if (strlen($hash) < 60) Util::traceError('Error hashing password using SHA-512');
return $hash;
diff --git a/inc/database.inc.php b/inc/database.inc.php
index 09006f3e..48d8e3c6 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -375,9 +375,9 @@ class Database
* @param string $aiKey name of the AUTO_INCREMENT column
* @param array $uniqueValues assoc array containing columnName => value mapping
* @param array $additionalValues assoc array containing columnName => value mapping
- * @return int[] list of AUTO_INCREMENT values matching the list of $values
+ * @return int AUTO_INCREMENT value matching the given unique values entry
*/
- public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false)
+ public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false): int
{
// Sanity checks
if (array_key_exists($aiKey, $uniqueValues)) {
diff --git a/install.php b/install.php
index 60cf9495..1737e7dc 100644
--- a/install.php
+++ b/install.php
@@ -262,7 +262,7 @@ function tableCreate($table, $structure, $fatalOnError = true)
if (tableExists($table)) {
return UPDATE_NOOP;
}
- $ret = Database::exec("CREATE TABLE IF NOT EXISTS `{$table}` ( {$structure} ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
+ $ret = Database::exec("CREATE TABLE IF NOT EXISTS `{$table}` ( {$structure} ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
if ($ret !== false) {
return UPDATE_DONE;
}
diff --git a/modules-available/minilinux/inc/linuxbootentryhook.inc.php b/modules-available/minilinux/inc/linuxbootentryhook.inc.php
index e57336f0..9e10bd6e 100644
--- a/modules-available/minilinux/inc/linuxbootentryhook.inc.php
+++ b/modules-available/minilinux/inc/linuxbootentryhook.inc.php
@@ -15,7 +15,7 @@ class LinuxBootEntryHook extends BootEntryHook
return Dictionary::translateFileModule('minilinux', 'module', 'module_name', true);
}
- public function extraFields()
+ public function extraFields(): array
{
/* For translate module:
* Dictionary::translate('ipxe-kcl-extra');
@@ -32,7 +32,7 @@ class LinuxBootEntryHook extends BootEntryHook
/**
* @return HookEntryGroup[]
*/
- protected function groupsInternal()
+ protected function groupsInternal(): array
{
/*
* Dictionary::translate('default_boot_entry');
@@ -76,7 +76,7 @@ class LinuxBootEntryHook extends BootEntryHook
* @param $localData
* @return BootEntry the actual boot entry instance for given entry, false if invalid id
*/
- public function getBootEntryInternal($localData)
+ public function getBootEntryInternal($localData): BootEntry
{
$id = $localData['id'];
if ($id === 'default') { // Special case
@@ -125,7 +125,7 @@ class LinuxBootEntryHook extends BootEntryHook
return BootEntry::newStandardBootEntry($bios, $efi, $arch);
}
- private function generateExecData($effectiveId, $remoteData, $localData)
+ private function generateExecData($effectiveId, $remoteData, $localData): ExecData
{
$exec = new ExecData();
// Defaults
@@ -146,9 +146,7 @@ class LinuxBootEntryHook extends BootEntryHook
if (!empty($localData['debug'])) {
// Debug boot enabled
$exec->commandLine = IPxe::modifyCommandLine($exec->commandLine,
- isset($remoteData['debugCommandLineModifier'])
- ? $remoteData['debugCommandLineModifier']
- : '-vga -quiet -splash -loglevel loglevel=7'
+ $remoteData['debugCommandLineModifier'] ?? '-vga -quiet -splash -loglevel loglevel=7'
);
}
// disable all CPU sidechannel attack mitigations etc.
@@ -156,6 +154,14 @@ class LinuxBootEntryHook extends BootEntryHook
$exec->commandLine = IPxe::modifyCommandLine($exec->commandLine,
'noibrs noibpb nopti nospectre_v2 nospectre_v1 l1tf=off nospec_store_bypass_disable no_stf_barrier mds=off mitigations=off');
}
+ // GVT, PCI Pass-thru etc.
+ if (Module::isAvailable('statistics')) {
+ $hwextra = HardwareInfo::getKclModifications();
+ if (!empty($hwextra)) {
+ $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, $hwextra);
+ }
+ }
+ // User-supplied modifications
if (!empty($localData['kcl-extra'])) {
$exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, $localData['kcl-extra']);
}
diff --git a/modules-available/passthrough/config.json b/modules-available/passthrough/config.json
new file mode 100644
index 00000000..3f65995f
--- /dev/null
+++ b/modules-available/passthrough/config.json
@@ -0,0 +1,7 @@
+{
+ "category": "main.settings-client",
+ "dependencies": [
+ "statistics",
+ "locations"
+ ]
+} \ No newline at end of file
diff --git a/modules-available/passthrough/inc/passthrough.inc.php b/modules-available/passthrough/inc/passthrough.inc.php
new file mode 100644
index 00000000..524aea5e
--- /dev/null
+++ b/modules-available/passthrough/inc/passthrough.inc.php
@@ -0,0 +1,53 @@
+<?php
+
+class Passthrough
+{
+
+ public static function getGroupDropdown(array &$row): array
+ {
+ $out = [];
+ if ($row['class'] === '0300') {
+ foreach (['GPU', 'GVT'] as $id) {
+ $out[] = [
+ 'ptid' => $id,
+ 'ptname' => $id,
+ 'selected' => ($row['@PASSTHROUGH'] === $id ? 'selected' : ''),
+ ];
+ }
+ return $out;
+ }
+ static $list = false;
+ if ($list === false) {
+ $list = Database::queryKeyValueList("SELECT groupid, title FROM passthrough_group ORDER BY groupid");
+ self::ensurePrepopulated($list);
+ }
+ $row['custom_groups'] = true;
+ foreach ($list as $id => $title) {
+ if ($id === 'GPU' || $id === 'GVT')
+ continue;
+ $item = ['ptid' => $id, 'ptname' => $id . ' (' . $title . ')'];
+ if ($row['@PASSTHROUGH'] === $id) {
+ $item['selected'] = 'selected';
+ }
+ $out[] = $item;
+ }
+ return $out;
+ }
+
+ private static function ensurePrepopulated(&$list)
+ {
+ $want = [
+ 'GPU' => '[Special] GPU passthrough default group',
+ 'GVT' => '[Special] Intel GVT-g default group',
+ ];
+ foreach ($want as $id => $title) {
+ if (!isset($list[$id])) {
+ Database::exec("INSERT INTO passthrough_group (groupid, title) VALUES (:id, :title)
+ ON DUPLICATE KEY UPDATE title = VALUES(title)",
+ ['id' => $id, 'title' => $title]);
+ $list[$id] = $title;
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/passthrough/install.inc.php b/modules-available/passthrough/install.inc.php
new file mode 100644
index 00000000..e08be38b
--- /dev/null
+++ b/modules-available/passthrough/install.inc.php
@@ -0,0 +1,9 @@
+<?php
+
+$result[] = tableCreate('passthrough_group', "
+ `groupid` varchar(32) CHARACTER SET ascii DEFAULT NULL,
+ `title` varchar(200) NOT NULL,
+ PRIMARY KEY (`groupid`)
+");
+
+responseFromArray($result); \ No newline at end of file
diff --git a/modules-available/passthrough/page.inc.php b/modules-available/passthrough/page.inc.php
new file mode 100644
index 00000000..7dcc8215
--- /dev/null
+++ b/modules-available/passthrough/page.inc.php
@@ -0,0 +1,130 @@
+<?php
+
+class Page_Passthrough extends Page
+{
+
+ protected function doPreprocess()
+ {
+ User::load();
+ User::assertPermission('view');
+ $action = Request::post('action');
+ if ($action === 'save-hwlist') {
+ $this->saveHwList();
+ }
+ if (Request::isPost()) {
+ Util::redirect('?do=passthrough');
+ }
+ }
+
+ private function saveHwList()
+ {
+ $newgroups = Request::post('newgroup', [], 'array');
+ foreach ($newgroups as $id => $title) {
+ $id = strtoupper(preg_replace('/[^a-z0-9_\-]/i', '', $id));
+ if (empty($id))
+ continue;
+ Database::exec("INSERT IGNORE INTO passthrough_group (groupid, title)
+ VALUES (:group, :title)",
+ ['group' => $id, 'title' => $title]);
+ }
+ $groups = Request::post('ptgroup', Request::REQUIRED, 'array');
+ $insert = [];
+ $delete = [];
+ foreach ($groups as $hwid => $group) {
+ if (empty($group)) {
+ $delete[] = $hwid;
+ } else {
+ $insert[] = [$hwid, '@PASSTHROUGH', $group];
+ }
+ }
+ if (!empty($insert)) {
+ Database::exec("INSERT INTO statistic_hw_prop (hwid, prop, `value`) VALUES :list
+ ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)",
+ ['list' => $insert]);
+ }
+ if (!empty($delete)) {
+ Database::exec("DELETE FROM statistic_hw_prop WHERE hwid IN (:list) AND prop = '@PASSTHROUGH'", ['list' => $delete]);
+ }
+ Message::addSuccess('list-updated');
+ Util::redirect('?do=passthrough&show=hwlist');
+ }
+
+ /*
+ *
+ */
+
+ protected function doRender()
+ {
+ $show = Request::get('show');
+ if ($show === 'hwlist') {
+ $this->showHardwareList();
+ } else {
+ Util::redirect('?do=passthrough&show=hwlist');
+ }
+ }
+
+ private function showHardwareList()
+ {
+ $q = new HardwareQuery(HardwareInfo::PCI_DEVICE, null, false);
+ $q->addGlobalColumn('vendor');
+ $q->addGlobalColumn('device');
+ $q->addGlobalColumn('rev');
+ $q->addGlobalColumn('class');
+ $q->addGlobalColumn('@PASSTHROUGH');
+ $rows = [];
+ foreach ($q->query('shw.hwid') as $row) {
+ $row['ptlist'] = Passthrough::getGroupDropdown($row);
+ $rows[] = $row;
+ }
+ // Sort Graphics Cards first, rest by class, vendor, device
+ usort($rows, function ($row1, $row2) {
+ $a = $row1['class'];
+ $b = $row2['class'];
+ if ($a === $b)
+ return hexdec($row1['vendor'].$row1['device']) - hexdec($row2['vendor'] . $row2['device']);
+ if ($a === '0300')
+ return -1;
+ if ($b === '0300')
+ return 1;
+ return hexdec($a) - hexdec($b);
+ });
+ $finalRows = [];
+ $missing = [];
+ $lastClass = '';
+ foreach ($rows as $row) {
+ if ($row['class'] !== $lastClass) {
+ // Add class row header
+ $lastClass = $row['class'];
+ $finalRows[] = [
+ 'class' => $row['class'],
+ 'class_name' => PciId::getPciId(PciId::DEVCLASS, $row['class'], true) ?: 'Unknown',
+ ];
+ }
+ $row['vendor_name'] = PciId::getPciId(PciId::VENDOR, $row['vendor'] ?? '');
+ $row['device_name'] = PciId::getPciId(PciId::DEVICE, $row['vendor'] . ':' . $row['device']);
+ $finalRows[] = $row;
+ // Build up query
+ if ($row['vendor_name'] === false) {
+ $missing[$row['vendor']] = true;
+ }
+ if ($row['device_name'] === false) {
+ $missing[$row['vendor'] . ':' . $row['device']] = true;
+ }
+ }
+ Render::addTemplate('hardware-list', ['list' => $finalRows]);
+ if (!empty($missing)) {
+ Render::addTemplate('js-pciquery',
+ ['missing_ids' => json_encode(array_keys($missing))], 'statistics');
+ }
+ }
+
+ /*
+ *
+ */
+
+ protected function doAjax()
+ {
+ //
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/passthrough/permissions/permissions.json b/modules-available/passthrough/permissions/permissions.json
new file mode 100644
index 00000000..d0932f1e
--- /dev/null
+++ b/modules-available/passthrough/permissions/permissions.json
@@ -0,0 +1,3 @@
+{
+ "view": false
+} \ No newline at end of file
diff --git a/modules-available/passthrough/templates/hardware-list.html b/modules-available/passthrough/templates/hardware-list.html
new file mode 100644
index 00000000..4bff0a39
--- /dev/null
+++ b/modules-available/passthrough/templates/hardware-list.html
@@ -0,0 +1,105 @@
+<form method="post" action="?do=passthrough&amp;show=hwlist">
+ <input type="hidden" name="token" value="{{token}}">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="slx-smallcol">{{lang_deviceIdNumeric}}</th>
+ <th>{{lang_deviceName}}</th>
+ <th class="slx-smallcol">{{lang_useCount}}</th>
+ <th>{{lang_passthroughGroup}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ {{#class_name}}
+ <td colspan="4">
+ <span>{{class}}</span> – <strong>{{class_name}}</strong>
+ </td>
+ {{/class_name}}
+ {{^class_name}}
+ <td>{{vendor}}:{{device}} [{{rev}}]</td>
+ <td>
+ <table class="slx-ellipsis">
+ <tr>
+ <td {{^device_name}}class="query-{{vendor}}-{{device}}"{{/device_name}}>
+ {{device_name}}
+ </td>
+ </tr>
+ </table>
+ <div class="small {{^vendor_name}}query-{{vendor}}{{/vendor_name}}">
+ {{vendor_name}}
+ </div>
+ </td>
+ <td class="text-right">{{connected_count}}</td>
+ <td>
+ <select name="ptgroup[{{hwid}}]" class="form-control {{#custom_groups}}ptgroup-select{{/custom_groups}}">
+ <option value="">{{lang_noPassthroughGroup}}</option>
+ {{#ptlist}}
+ <option value="{{ptid}}" {{selected}}>{{ptname}}</option>
+ {{/ptlist}}
+ </select>
+ </td>
+ {{/class_name}}
+ </tr>
+ {{/list}}
+ </tbody>
+ </table>
+ <div id="new-groups"></div>
+ <div class="buttonbar text-right">
+ <button type="button" data-target="#add-group-form" data-toggle="modal" class="btn btn-default">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_add}}
+ </button>
+ <button type="submit" name="action" value="save-hwlist" class="btn btn-success">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+</form>
+
+<div class="modal fade" id="add-group-form" 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>
+ <b>{{lang_addPassthroughGroup}}</b>
+ </div>
+ <div class="modal-body">
+ <div class="form-group">
+ <label for="group-id">{{lang_groupId}}</label>
+ <input type="text" name="group-id" id="group-id" class="form-control">
+ </div>
+ <div class="form-group">
+ <label for="group-title">{{lang_groupTitle}}</label>
+ <input type="text" name="group-title" id="group-title" class="form-control">
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default"
+ data-dismiss="modal">{{lang_cancel}}</button>
+ <button id="add-group-button" type="button" class="btn btn-success" data-dismiss="modal">
+ <span class="glyphicon glyphicon-plus"></span>
+ {{lang_addGroup}}
+ </button>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script>
+ document.addEventListener('DOMContentLoaded', function() {
+ $('#add-group-button').click(function () {
+ var gid = $('#group-id').val().replace(/[^a-zA-Z0-9_\-]/g, '').toUpperCase();
+ var title = $('#group-title').val().trim();
+ if (gid.length === 0)
+ return;
+ $('#new-groups').append($('<input type="hidden">')
+ .attr('name', 'newgroup[' + gid + ']')
+ .attr('value', title));
+ $('.ptgroup-select').each(function() {
+ $(this).append($('<option>').attr('value', gid).text(gid + ' (' + title + ')'));
+ });
+ });
+ });
+</script> \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
index d4dd9516..2ce098e9 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
@@ -17,30 +17,38 @@ chain -ar /tftp/sl-bios/lpxelinux.0',
'EFI' => [
'EXIT' => 'set slx_exit 1 ||
exit 1',
+ 'SANBOOT' => 'imgfree ||
+console ||
+set filename \EFI\Boot\bootx64.efi ||
+set i:int32 0 ||
+:blubber
+sanboot --no-describe --drive ${i} --filename ${filename} ||
+inc i
+iseq ${i} 10 || goto blubber',
'COMBOOT' => 'set netX/209:string localboot.cfg ||
set netX/210:string http://${serverip}/tftp/sl-efi64/ ||
chain -ar /tftp/sl-efi64/syslinux.efi',
],
];
- public static function getDefault()
+ public static function getDefault(): array
{
- $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,EXIT'));
+ $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,SANBOOT'));
if (empty($ret)) {
- $ret = ['SANBOOT', 'EXIT'];
+ $ret = ['SANBOOT', 'SANBOOT'];
} elseif (count($ret) < 2) {
- $ret[] = 'EXIT';
+ $ret[] = 'SANBOOT';
}
- if (null === self::BOOT_METHODS['PCBIOS'][$ret[0]]) {
+ if (!isset(self::BOOT_METHODS['PCBIOS'][$ret[0]])) {
$ret[0] = 'SANBOOT';
}
- if (null === self::BOOT_METHODS['EFI'][$ret[1]]) {
- $ret[1] = 'EXIT';
+ if (!isset(self::BOOT_METHODS['EFI'][$ret[1]])) {
+ $ret[1] = 'SANBOOT';
}
return ['PCBIOS' => $ret[0], 'EFI' => $ret[1]];
}
- public static function setDefault($pcbios, $efi)
+ public static function setDefault(string $pcbios, string $efi)
{
Property::set(self::PROPERTY_KEY, "$pcbios,$efi");
}
diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php
index 04614521..a945cdf5 100644
--- a/modules-available/statistics/api.inc.php
+++ b/modules-available/statistics/api.inc.php
@@ -1,5 +1,29 @@
<?php
+if (Request::any('action') === 'test' && isLocalExecution()) {
+ $uuid = Request::any('uuid', '', 'string');
+ /*
+ error_log(HardwareInfo::getKclModifications());
+ exit;
+ $x = new HardwareQuery(HardwareInfo::PCI_DEVICE);
+ //$x->addCompare(false, 'Memory Slot Occupied', '>=', true, 'Memory Slot Count');
+ $x->addWhere(true, 'vendor', '=', '8086');
+ $x->addGlobalColumn('device');
+ $res = $x->query();
+ foreach ($res as $row) {
+ error_log(json_encode($row));
+ }
+ exit;
+ */
+ $data = file_get_contents('/tmp/bla.json');
+ Database::exec(
+ "UPDATE machine SET data = :data WHERE machineuuid = :uuid",
+ ['uuid' => $uuid, 'data' => $data]);
+ HardwareParser::parseMachine($uuid, json_decode($data, true));
+ echo 'Kweries: ' . Database::getQueryCount();
+ exit;
+}
+
if (empty($_POST['type'])) die('Missing options.');
$type = mb_strtolower($_POST['type']);
@@ -62,7 +86,23 @@ 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(
'machineuuid'=> $uuid,
@@ -119,7 +159,7 @@ 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 = :machineuuid AND state = :oldstate AND lastseen = :oldlastseen", $new);
if ($res === 0) {
@@ -151,6 +191,10 @@ if ($type[0] === '~') {
$new['locationid'] = $loc; // For Filter Event
}
+ if ($json !== false) {
+ HardwareParser::parseMachine($uuid, $json);
+ }
+
// Check for suspicious hardware changes
if ($old !== false) {
checkHardwareChange($old, $new);
@@ -358,7 +402,7 @@ if ($type[0] === '~') {
// 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,
diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php
new file mode 100644
index 00000000..586b8124
--- /dev/null
+++ b/modules-available/statistics/inc/hardwareinfo.inc.php
@@ -0,0 +1,85 @@
+<?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';
+
+ /**
+ * 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
+ * @return string
+ */
+ public static function getKclModifications($uuid = null, $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 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)
+ return '';
+ $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true);
+ // TODO: Get list of enabled pass through groups for this client's location
+ $hw->addWhere(true, '@PASSTHROUGH', 'IN', ['GPU', 'GVT']);
+ $hw->addGlobalColumn('vendor');
+ $hw->addGlobalColumn('device');
+ $hw->addLocalColumn('slot');
+ $res = $hw->query();
+ $passthrough = [];
+ $slots = [];
+ $gvt = false;
+ foreach ($res as $row) {
+ if ($row['@PASSTHROUGH'] === 'GVT') {
+ $gvt = true;
+ } else {
+ $passthrough[$row['vendor'] . ':' . $row['device']] = 1;
+ //error_log('Passthouorgh: ' . $row['vendor'] . ':' . $row['device']);
+ $slots[preg_replace('/\.[0-9]+$/', '', $row['slot'])] = 1;
+ }
+ }
+ $kcl = '';
+ if ($gvt || !empty($passthrough)) {
+ $kcl = '-iommu -intel_iommu iommu=pt intel_iommu=on'; // TODO AMD
+ }
+ if (!empty($passthrough)) {
+ foreach (array_keys($slots) as $slot) {
+ //error_log('Querying slot ' . $slot);
+ $hw = new HardwareQuery(self::PCI_DEVICE, $best['machineuuid'], true);
+ $hw->addWhere(false, 'slot', '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;
+ }
+
+}
diff --git a/modules-available/statistics/inc/hardwareparser.inc.php b/modules-available/statistics/inc/hardwareparser.inc.php
new file mode 100644
index 00000000..0217835e
--- /dev/null
+++ b/modules-available/statistics/inc/hardwareparser.inc.php
@@ -0,0 +1,631 @@
+<?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];
+
+ 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 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::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 = (int)round($val / self::LOOKUP[$scale]);
+ if ($appendUnit) {
+ $val .= ' ' . ($scale === '' ? 'Byte' : $scale . 'iB'); // NBSP!!
+ }
+ return $val;
+ }
+
+ /**
+ * Decode JEDEC ID to according manufacturer
+ * @param string $string
+ * @return string
+ */
+ public static function decodeJedec(string $string): 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;
+ $id = 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;
+ }
+
+ /**
+ * @param string $key
+ * @param string $value
+ * @return ?int value, or null if not numeric
+ */
+ private static function toNumeric(string $key, string $val)
+ {
+ $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|size|temperature|_start|_value|_thresh|_worst|_time/', $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
+ */
+ private static function writeLocalHardwareData(string $uuid, int $hwid, string $pathId, array $props): int
+ {
+ // Add mapping between hw entity and machine
+ $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]);
+ }
+ }
+
+ private static function writeGlobalHardwareData(string $dbType, array $global): 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));
+ 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. resultion, 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];
+ }
+
+ public static function parseMachine(string $uuid, array $data)
+ {
+ $version = $data['version'] ?? 0;
+ if ($version != 2) {
+ error_log("Received unsupported hw json v$version");
+ return;
+ }
+ // determine misc stuff first
+ $globalMainboardExtra = [];
+ $localMainboardExtra = [];
+ // physical memory array
+ $memArrays = self::getDmiHandles($data, 16);
+ $globalMainboardExtra['Memory Slot Count'] = 0;
+ $globalMainboardExtra['Memory Maximum Capacity'] = 0;
+ foreach ($memArrays as $mem) {
+ $mem = self::prepareDmiProperties($mem);
+ if (isset($mem['Number Of Devices']) && ($mem['Use'] ?? 0) === 'System Memory') {
+ $globalMainboardExtra['Memory Slot Count'] += $mem['Number Of Devices'];
+ }
+ if (isset($mem['Maximum Capacity'])) {
+ $globalMainboardExtra['Memory Maximum Capacity']
+ += self::convertSize($mem['Maximum Capacity'], 'M', false);
+ }
+ }
+ $globalMainboardExtra['Memory Maximum Capacity']
+ = self::convertSize($globalMainboardExtra['Memory Maximum Capacity'] . ' MB', 'G');
+ // BIOS section - need to cross-match this with mainboard or system model, as it doesn't have a meaningful
+ // identifier on its own
+ $bios = self::prepareDmiProperties(self::getDmiHandles($data, 0)[0]);
+ foreach (['Version', 'Release Date', 'Firmware Revision'] as $k) {
+ if (isset($bios[$k])) {
+ $localMainboardExtra['BIOS ' . $k] = $bios[$k];
+ }
+ }
+ if (isset($bios['BIOS Revision'])) {
+ $localMainboardExtra['BIOS Revision'] = $bios['BIOS Revision'];
+ }
+ foreach (['Vendor', 'ROM Size'] as $k) {
+ if (isset($bios[$k])) {
+ $globalMainboardExtra['BIOS ' . $k] = $bios[$k];
+ }
+ }
+ // Using the general helper function
+ $capa = 0;
+ $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, HardwareInfo::RAM_MODULE, function (array $flat) use (&$capa): bool {
+ $size = self::convertSize(($flat['Size'] ?? 0), '', false);
+ if ($size > 65 * 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']
+ );
+ // Fake RAM slots used/total etc. into this
+ $localMainboardExtra['Memory Slot Occupied'] = $ramModCount;
+ $localMainboardExtra['Memory Installed Capacity'] = self::convertSize($capa, 'G', true);
+ foreach (['sockets', 'cores', 'threads'] as $key) {
+ if (!isset($data['cpu'][$key]))
+ continue;
+ $localMainboardExtra['cpu-' . $key] = $data['cpu'][$key];
+ }
+ self::updateHwTypeFromDmi($uuid, $data, 2, HardwareInfo::MAINBOARD, ['Manufacturer', 'Product Name'],
+ [],
+ ['Manufacturer', 'Product Name', 'Type', 'Version'],
+ ['Serial Number', 'Asset Tag', 'Location In Chassis'],
+ $globalMainboardExtra, $localMainboardExtra
+ );
+ self::updateHwTypeFromDmi($uuid, $data, 1, HardwareInfo::DMI_SYSTEM, ['Manufacturer', 'Product Name'],
+ [],
+ ['Manufacturer', 'Product Name', 'Version', 'Wake-up Type'],
+ ['Serial Number', 'UUID', 'SKU Number']
+ );
+ 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'],
+ ['Serial Number', 'Asset Tag', 'Status', 'Plugged', 'Hot Replaceable']
+ );
+ self::updateHwTypeFromDmi($uuid, $data, 4, HardwareInfo::CPU, ['Version'],
+ ['Socket Designation'],
+ ['Type', 'Family', 'Manufacturer', 'Signature', 'Version', 'Core Count', 'Thread Count'],
+ ['Voltage', 'Current Speed', 'Upgrade', 'Core Enabled']);
+ self::updateHwTypeFromDmi($uuid, $data, 9, HardwareInfo::SYSTEM_SLOT, function (array &$entry): bool {
+ if (!isset($entry['Type']))
+ return false;
+ // Split up PCIe info
+ 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'],
+ ['Current Usage', 'Designation']
+ );
+ // ---- lspci ------------------------------------
+ $pciHwIds = [];
+ foreach (($data['lspci'] ?? []) as $dev) {
+ $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 ------------------------------------0Y3R3K
+ $hddHwIds = [];
+ $id44 = $id45 = 0;
+ foreach (($data['drives'] ?? []) as $dev) {
+ if (empty($dev['readlink']))
+ continue;
+ if (!isset($dev['smartctl'])) {
+ $smart = [];
+ } else {
+ $smart =& $dev['smartctl'];
+ }
+ if (!isset($dev['lsblk']['blockdevices'][0])) {
+ $lsblk = [];
+ } else {
+ $lsblk =& $dev['lsblk']['blockdevices'][0];
+ }
+ if (!isset($smart['rotation_rate']) && isset($lsblk['rota']) && !$lsblk['rota']) {
+ $smart['rotation_rate'] = 0;
+ }
+ $size = $lsblk['size'] ?? $smart['user_capacity']['bytes'] ?? 'unknown';
+ $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 - find special ones
+ $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;
+ $type = hexdec($part['type'] ?? '0');
+ if ($type === 0x0 || $type === 0x5 || $type === 0xf || $type === 0x15 || $type === 0x1f
+ || $type === 0x85 || $type === 0xc5 || $type == 0xcf)
+ continue; // Extended partition, ignore
+ $used += $part['size'] * $fac;
+ $id = 'part_' . $i . '_';
+ 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),
+ ]);
+ $hddHwIds[] = $mappingId;
+ unset($smart, $lsblk);
+ } // End loop over disks
+ self::markDisconnected($uuid, HardwareInfo::HDD, $hddHwIds);
+ //
+ // Mark parse date
+ Database::exec("UPDATE machine SET dataparsetime = UNIX_TIMESTAMP(), id44mb = :id44, id45mb = :id45
+ WHERE machineuuid = :uuid", [
+ 'uuid' => $uuid,
+ 'id44' => round($id44 / (1024 * 1024)),
+ 'id45' => round($id45 / (1024 * 1024)),
+ ]);
+ }
+
+ /**
+ * 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 'dell inc.':
+ return 'Dell';
+ case 'fujitsu':
+ case 'fujitsu client computing limited':
+ return 'Fujitsu';
+ case 'hewlett packard':
+ case 'hewlett-packard':
+ return 'HP';
+ case 'hynix semiconduc':
+ case 'hynix/hyundai':
+ case 'hyundai electronics hynix semiconductor inc':
+ case 'hynix semiconductor inc sk hynix':
+ return 'SK Hynix';
+ case 'genuineintel':
+ case 'intel corporation':
+ case 'intel(r) corp.':
+ case 'intel(r) corporation':
+ return 'Intel';
+ case 'samsung sdi':
+ return 'Samsung';
+ }
+ return $in;
+ }
+
+ /**
+ * Takes key-value-array, returns a new array with only the keys listed in $fields.
+ */
+ 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;
+ }
+
+ 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);
+ $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..277f6d4a
--- /dev/null
+++ b/modules-available/statistics/inc/hardwareparserlegacy.inc.php
@@ -0,0 +1,284 @@
+<?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})"/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'] += $out[1];
+ }
+ if ($ramOk && preg_match('/^\s*Maximum Capacity:\s+(\d.+)/i', $line, $out)) {
+ $row['Memory Maximum Capacity'] += 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..6b5662d4
--- /dev/null
+++ b/modules-available/statistics/inc/hardwarequery.inc.php
@@ -0,0 +1,157 @@
+<?php
+
+class HardwareQuery
+{
+
+ private $id = 0;
+ private $joins = [];
+ private $where = [];
+ private $args = [];
+ private $columns = [];
+
+ /**
+ * @param string $type type form HardwareInfo
+ * @param ?string $uuid
+ */
+ public function __construct(string $type, $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 't' . (++$this->id);
+ }
+
+ private function fillTableVars(bool $global, &$srcTable, &$table, &$column)
+ {
+ if ($global) {
+ $srcTable = 'shw';
+ $table = 'statistic_hw_prop';
+ $column = 'hwid';
+ } else {
+ $srcTable = 'mxhw';
+ $table = 'machine_x_hw_prop';
+ $column = 'machinehwid';
+ }
+ }
+
+ /**
+ * @param bool $global
+ * @param string $prop
+ * @param string $op
+ * @param string|string[] $value
+ */
+ public function addWhere(bool $global, string $prop, string $op, $value)
+ {
+ if (isset($this->columns[$prop]))
+ return;
+ $this->fillTableVars($global, $srcTable, $table, $column);
+ $tid = $this->id();
+ $pid = $this->id();
+ $vid = $this->id();
+ $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value';
+ $this->joins[$prop] = "INNER JOIN $table $tid ON ($srcTable.$column = $tid.$column AND
+ $tid.prop = :$pid AND $tid.`$valueCol` $op (:$vid))";
+ $this->args[$pid] = $prop;
+ $this->args[$vid] = $value;
+ $this->columns[$prop] = "$tid.`value` AS `$prop`";
+ }
+
+ public function addMachineWhere(string $column, string $op, $value)
+ {
+ if (isset($this->columns[$column]))
+ return;
+ $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value';
+ $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 addCompare(bool $global1, string $prop1, string $op, string $global2, string $prop2)
+ {
+ $this->fillTableVars($global1, $srcTable1, $table1, $column1);
+ $this->fillTableVars($global2, $srcTable2, $table2, $column2);
+ $tid1 = $this->id();
+ $pid1 = $this->id();
+ $tid2 = $this->id();
+ $pid2 = $this->id();
+ $valueCol = ($op === '<' || $op === '>' || $op === '<=' || $op === '>=') ? 'numeric' : 'value';
+ $this->joins[] = "INNER JOIN $table1 $tid1 ON ($srcTable1.$column1 = $tid1.$column1 AND
+ $tid1.prop = :$pid1)";
+ $this->joins[] = "INNER JOIN $table2 $tid2 ON ($srcTable2.$column2 = $tid2.$column2 AND
+ $tid2.prop = :$pid2 AND $tid1.`$valueCol` $op $tid2.`$valueCol`)";
+ $this->args[$pid1] = $prop1;
+ $this->args[$pid2] = $prop2;
+ $this->columns[$prop1] = "$tid1.`value` AS `$prop1`";
+ $this->columns[$prop2] = "$tid2.`value` AS `$prop2`";
+ }
+
+ public function addGlobalColumn(string $prop)
+ {
+ $this->addColumn(true, $prop);
+ }
+
+ public function addLocalColumn(string $prop)
+ {
+ $this->addColumn(false, $prop);
+ }
+
+ public function addColumn(bool $global, string $prop)
+ {
+ if (isset($this->columns[$prop]))
+ return;
+ $this->fillTableVars($global, $srcTable, $table, $column);
+ $tid = $this->id();
+ $pid = $this->id();
+ $this->joins[$prop] = "LEFT JOIN $table $tid ON ($srcTable.$column = $tid.$column AND $tid.prop = :$pid)";
+ $this->args[$pid] = $prop;
+ $this->columns[$prop] = "$tid.`value` AS `$prop`";
+ }
+
+ public function addMachineColumn(string $column)
+ {
+ 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(string $groupBy = '')
+ {
+ $columns = $this->columns;
+ $columns[] = 'mxhw.machineuuid';
+ $columns[] = 'shw.hwid';
+ if (empty($groupBy) || $groupBy === '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 $groupBy";
+ }
+ $query = 'SELECT ' . implode(', ', $columns)
+ . ' FROM statistic_hw shw '
+ . implode(' ', $this->joins)
+ . ' WHERE ' . implode(' AND ', $this->where)
+ . $groupBy;
+ return Database::simpleQuery($query, $this->args);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/statistics/inc/parser.inc.php b/modules-available/statistics/inc/parser.inc.php
deleted file mode 100644
index 84f98c40..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/install.inc.php b/modules-available/statistics/install.inc.php
index 15d0d633..67c00d8f 100644
--- a/modules-available/statistics/install.inc.php
+++ b/modules-available/statistics/install.inc.php
@@ -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`)
");
@@ -298,6 +302,55 @@ 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) {
+ $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) {
+ $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) {
+ $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) {
+ $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;
+}
// Create response
responseFromArray($res);
diff --git a/modules-available/statistics/lang/de/messages.json b/modules-available/statistics/lang/de/messages.json
index e1688cbf..c3d9bb0b 100644
--- a/modules-available/statistics/lang/de/messages.json
+++ b/modules-available/statistics/lang/de/messages.json
@@ -2,6 +2,7 @@
"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}}",
diff --git a/modules-available/statistics/lang/de/module.json b/modules-available/statistics/lang/de/module.json
index 902a9573..a3006881 100644
--- a/modules-available/statistics/lang/de/module.json
+++ b/modules-available/statistics/lang/de/module.json
@@ -1,5 +1,8 @@
{
"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/template-tags.json b/modules-available/statistics/lang/de/template-tags.json
index 43665a78..d7013da6 100644
--- a/modules-available/statistics/lang/de/template-tags.json
+++ b/modules-available/statistics/lang/de/template-tags.json
@@ -24,12 +24,17 @@
"lang_eventType": "Typ",
"lang_firstSeen": "Erste Aktivit\u00e4t",
"lang_free": "frei",
+ "lang_fullInfo": "Alle Werte",
"lang_gbRam": "RAM",
"lang_hardwareSummary": "Hardware",
"lang_hasNotes": "Zu diesem Rechner wurden Notizen hinterlegt",
+ "lang_hddUnused": "Ungenutzt",
+ "lang_hddUnusedId44": "Mit freiem HDD-Speicher, ohne ID44",
+ "lang_hddUnusedId45": "Mit freiem HDD-Speicher, ohne ID45",
"lang_hdds": "Festplatten",
"lang_hostname": "Hostname",
"lang_inUseMachines": "In Verwendung",
+ "lang_installedCountMax": "Slots belegt \/ frei",
"lang_ip": "IP-Adresse",
"lang_knownMachines": "Bekannte Clients",
"lang_kvmState": "Status",
@@ -50,6 +55,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",
@@ -72,12 +78,17 @@
"lang_partitionSize": "Gr\u00f6\u00dfe",
"lang_pcmodel": "PC-Modell",
"lang_pendingSectors": "Potentiell defekte Sektoren",
+ "lang_persistentPart": "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",
@@ -100,7 +111,12 @@
"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_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.",
@@ -111,6 +127,8 @@
"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/template-tags.json b/modules-available/statistics/lang/en/template-tags.json
index 3fcbc049..43b6aeea 100644
--- a/modules-available/statistics/lang/en/template-tags.json
+++ b/modules-available/statistics/lang/en/template-tags.json
@@ -24,6 +24,7 @@
"lang_eventType": "Type",
"lang_firstSeen": "First seen",
"lang_free": "free",
+ "lang_fullInfo": "All values",
"lang_gbRam": "RAM",
"lang_hardwareSummary": "Hardware",
"lang_hasNotes": "Notes have been added to this client",
diff --git a/modules-available/statistics/page.inc.php b/modules-available/statistics/page.inc.php
index 3e4aa9ce..04d9a515 100644
--- a/modules-available/statistics/page.inc.php
+++ b/modules-available/statistics/page.inc.php
@@ -24,10 +24,12 @@ class Page_Statistics extends Page
/*
Dictionary::translate('submenu_projectors');
Dictionary::translate('submenu_replace');
+ Dictionary::translate('submenu_hints');
*/
- foreach (['projectors', 'replace'] as $section) {
- Dashboard::addSubmenu('?do=statistics&show=' . $section, Dictionary::translate('submenu_' . $section, true));
+ foreach (['projectors', 'replace', 'hints'] as $section) {
+ Dashboard::addSubmenu('?do=statistics&show=' . $section,
+ Dictionary::translate('submenu_' . $section, true));
}
$this->show = Request::any('show', false, 'string');
@@ -251,11 +253,24 @@ class Page_Statistics extends Page
{
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) {
@@ -263,61 +278,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;
- }
- $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;
+ $cached = PciId::getPciId(PciId::AUTO, $param, true);
+ if ($cached === false) {
+ $cached = 'Unknown';
}
- 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..278c0e26
--- /dev/null
+++ b/modules-available/statistics/pages/hints.inc.php
@@ -0,0 +1,109 @@
+<?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::showMemoryUpgrade($locs);
+ self::showMemorySlow($locs);
+ self::showUnusedSpace($locs);
+ }
+
+ private static function showMemoryUpgrade(array $locs)
+ {
+ $q = new HardwareQuery(HardwareInfo::MAINBOARD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addLocalColumn('Memory Slot Occupied');
+ $q->addGlobalColumn('Memory Slot Count');
+ $q->addGlobalColumn('Memory Maximum Capacity');
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addWhere(false, 'Memory Installed Capacity', '<', 8 * 1024 * 1024 * 1024);
+ $list = [];
+ foreach ($q->query() as $row) {
+ if (HardwareParser::convertSize($row['Memory Installed Capacity'], 'M', false)
+ >= HardwareParser::convertSize($row['Memory Maximum Capacity'], 'M', false)) {
+ $row['size_class'] = 'danger';
+ $list[] = $row;
+ } elseif ($row['Memory Slot Occupied'] < $row['Memory Slot Count']) {
+ $row['count_class'] = 'success';
+ array_unshift($list, $row);
+ } else {
+ $list[] = $row;
+ }
+ }
+ Render::addTemplate('hints-ram-upgrade', ['list' => $list]);
+ }
+
+ private static function showMemorySlow(array $locs)
+ {
+ $q = new HardwareQuery(HardwareInfo::RAM_MODULE);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $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->addCompare(true, 'Speed', '>', false, 'Configured Memory Speed');
+ $list = $q->query()->fetchAll();
+ Render::addTemplate('hints-ram-underclocked', ['list' => $list]);
+ }
+
+ private static function showUnusedSpace(array $locs)
+ {
+ $id44 = $id45 = [];
+ // ID44
+ $q = new HardwareQuery(HardwareInfo::HDD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addWhere(false, 'unused', '>', 2000000000); // 2 GB
+ $q->addMachineWhere('id44mb', '<', 20000); // 20 GB
+ foreach ($q->query()->fetchAll() as $row) {
+ $row['unused_s'] = Util::readableFileSize($row['unused']);
+ $row['id44mb_s'] = Util::readableFileSize($row['id44mb'], -1, 2);
+ $id44[] = $row;
+ }
+ // ID45
+ $q = new HardwareQuery(HardwareInfo::HDD);
+ if (!empty($locs)) {
+ $q->addMachineWhere('locationid', 'IN', $locs);
+ }
+ $q->addMachineColumn('clientip');
+ $q->addMachineColumn('hostname');
+ $q->addWhere(false, 'unused', '>', 25000000000); // 25 GB
+ $q->addMachineWhere('id44mb', '>', 20000); // 20 GB
+ $q->addMachineWhere('id45mb', '<', 20000); // 20 GB
+ foreach ($q->query()->fetchAll() 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);
+ $id45[] = $row;
+ }
+ Render::addTemplate('hints-hdd-grow', [
+ 'id44' => $id44,
+ 'id45' => $id45,
+ ]);
+ }
+
+} \ 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 677ff580..62abc8d2 100644
--- a/modules-available/statistics/pages/machine.inc.php
+++ b/modules-available/statistics/pages/machine.inc.php
@@ -54,7 +54,7 @@ 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) {
@@ -70,6 +70,55 @@ class SubPage
Header('Content-Type: text/plain; charset=utf-8');
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);
+ $client['lspci1'] = $client['lspci2'] = [];
+ foreach ($client['lspci'] as $item) {
+ $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[$item['vendor'] . ':' . $item['device']] = true;
+ }
+ $class = $item['class'];
+ if ($class === '0300' || $class === '0200' || $class === '0403') {
+ $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);
@@ -121,6 +170,7 @@ class SubPage
}
$client['gbram'] = round(ceil($client['mbram'] / 512) / 2, 1);
$client['gbtmp'] = round($client['id44mb'] / 1024);
+ $client['gbid45'] = round($client['id45mb'] / 1024);
foreach (['tmp', 'id45', 'swap', 'mem'] as $item) {
if ($client['live_' . $item . 'size'] == 0)
continue;
@@ -135,44 +185,25 @@ class SubPage
$client['ramclass'] = StatisticsStyling::ramColorClass($client['mbram']);
$client['kvmclass'] = StatisticsStyling::kvmColorClass($client['kvmstate']);
$client['hddclass'] = StatisticsStyling::hddColorClass($client['gbtmp']);
- // Parse the giant blob of data
- if (strpos($client['data'], "\r") !== false) {
- $client['data'] = str_replace("\r", "\n", $client['data']);
- }
- $hdds = array();
- if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) {
- foreach ($out as $section) {
- if ($section[1] === 'CPU') {
- Parser::parseCpu($client, $section[2]);
- }
- if ($section[1] === 'dmidecode') {
- Parser::parseDmiDecode($client, $section[2]);
- }
- if ($section[1] === 'Partition tables') {
- Parser::parseHdd($hdds, $section[2]);
- }
- if ($section[1] === 'PCI ID') {
- $client['lspci1'] = $client['lspci2'] = array();
- Parser::parsePci($client['lspci1'], $client['lspci2'], $section[2]);
- }
- if (isset($hdds['hdds']) && $section[1] === 'smartctl') {
- // This currently requires that the partition table section comes first...
- Parser::parseSmartctl($hdds['hdds'], $section[2]);
- }
- }
+ // 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'], $client['bios']['BIOS Revision']);
if ($ret === false) { // Not loaded, use AJAX
$params = [
'mainboard' => $mainboard,
'system' => $system,
- 'date' => $client['biosdate'],
- 'revision' => $client['biosrevision'],
+ 'date' => $client['bios']['Release Date'],
+ 'revision' => $client['bios']['BIOS Revision'],
];
$client['biosurl'] = '?do=statistics&action=bios&' . http_build_query($params);
} elseif (!isset($ret['status']) || $ret['status'] !== 0) {
@@ -211,6 +242,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;
@@ -348,7 +383,179 @@ 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 = [
+ 'cpu' => $json['cpu'] ?? [],
+ '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');
+ $return += $q->query()->fetch();
+ 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] + 1];
+ }
+ $hdd['partitions'][$out[1]][$out[2]] = $row['numeric'] ?? $row['value'];
+ } else {
+ $hdd[$row['prop']] = $row['numeric'] ?? $row['value'];
+ }
+ }
+ foreach ($hdds as $k => &$hdd) {
+ $hdd['devid'] = 'k' . $k;
+ $hdd['partitions'] = array_values($hdd['partitions']);
+ }
+ return array_values($hdds);
+ }
+
+ private static function mangleHdd(array &$hdd)
+ {
+ $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;
+ foreach ($hdd['partitions'] as &$part) {
+ $dist = $part['start'] - $lastEnd;
+ if ($dist > $minDisplaySize) {
+ error_log('Dist: ' . Util::readableFileSize($dist));
+ $json[] = ['value' => $dist, 'color' => '#aaa'];
+ }
+ if ($part['size'] > $minDisplaySize) {
+ $json[] = ['value' => $part['size'], 'color' => self::typeToColor($part), 'label' => $part['id']];
+ }
+ $part['size_s'] = Util::readableFileSize($part['size']);
+ $used += $part['size'];
+ $lastEnd = $part['start'] + $part['size'];
+ if (!isset($part['name'])) {
+ $part['name'] = self::mbrType($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':
@@ -441,4 +648,22 @@ class SubPage
return $retval;
}
+ private static function mbrType(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 $type;
+ }
+
} \ No newline at end of file
diff --git a/modules-available/statistics/permissions/permissions.json b/modules-available/statistics/permissions/permissions.json
index b27ca992..a5823775 100644
--- a/modules-available/statistics/permissions/permissions.json
+++ b/modules-available/statistics/permissions/permissions.json
@@ -25,5 +25,8 @@
},
"replace": {
"location-aware": true
+ },
+ "hints": {
+ "location-aware": true
}
} \ 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..dd856700
--- /dev/null
+++ b/modules-available/statistics/templates/hints-hdd-grow.html
@@ -0,0 +1,65 @@
+<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>
+ <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>
+ <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> \ 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..fffd3322
--- /dev/null
+++ b/modules-available/statistics/templates/hints-ram-underclocked.html
@@ -0,0 +1,44 @@
+<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>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td>
+ <a class="slx-bold" href="?do=statistics&amp;uuid={{machineuuid}}">
+ {{hostname}}{{^hostname}}{{clientip}}{{/hostname}}
+ </a>
+ <div class="small">{{machineuuid}}</div>
+ </td>
+ <td>
+ {{Type}} {{Form Factor}}
+ <div>{{Size}}</div>
+ </td>
+ <td>
+ {{Configured Memory Speed}}
+ </td>
+ <td>
+ {{Speed}}
+ </td>
+ <td>
+ {{Manufacturer}}
+ </td>
+ <td>
+ {{Serial Number}}
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+</table> \ No newline at end of file
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..be0d2532
--- /dev/null
+++ b/modules-available/statistics/templates/hints-ram-upgrade.html
@@ -0,0 +1,31 @@
+<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>
+ <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/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/machine-hdds.html b/modules-available/statistics/templates/machine-hdds.html
index 4d0409f9..84feccfd 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">
@@ -32,13 +35,16 @@
</tr>
{{#partitions}}
<tr id="{{id}}">
+ <td>{{index}}</td>
+ <td class="text-right text-nowrap">{{size_s}}</td>
<td>{{name}}</td>
- <td class="text-right text-nowrap">{{size}}&thinsp;GiB</td>
- <td>{{type}}</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>
@@ -46,7 +52,7 @@
document.addEventListener("DOMContentLoaded", function() {
var data = {{{json}}};
var sel = false;
- new Chart(document.getElementById('{{devid}}-chart').getContext('2d')).Pie(data, {
+ new Chart(document.getElementById('{{devid}}-chart').getContext('2d')).Pie(data, {
animation: false,
tooltipTemplate: "<%if (label){%><%=label%><%}%>",
customTooltips: function(tooltip) {
diff --git a/modules-available/statistics/templates/machine-main.html b/modules-available/statistics/templates/machine-main.html
index 568099e0..71df723d 100644
--- a/modules-available/statistics/templates/machine-main.html
+++ b/modules-available/statistics/templates/machine-main.html
@@ -220,11 +220,11 @@
<td class="text-nowrap">{{lang_cpuModel}}</td>
<td>
{{cpumodel}}
- {{#Sockets}}
+ {{#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 +243,12 @@
</tr>
<tr>
<td class="text-nowrap">{{lang_pcmodel}}</td>
- <td>{{pcmodel}} ({{pcmanufacturer}})</td>
+ <td>{{system.Product Name}} ({{system.Manufacturer}})</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 +256,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,17 +289,36 @@
{{/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>
@@ -304,11 +326,19 @@
{{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_persistentPart}}</td>
+ <td>
+ <div>
+ {{gbid45}}&thinsp;GiB
+ </div>
{{#live_id45size}}
<div class="meter">
<div class="text right">{{live_id45free_s}} {{lang_free}}</div>
@@ -362,16 +392,26 @@
</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{{^vendor_s}} class="query-{{vendor}}"{{/vendor_s}}>{{vendor_s}}</span>
+ <span{{^device_s}} class="query-{{vendor}}-{{device}}"{{/device_s}}>{{device_s}}</span>
+ <span>[{{vendor}}:{{device}}]</span>
+ </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{{^vendor_s}} class="query-{{vendor}}"{{/vendor_s}}>{{vendor_s}}</span>
+ <span{{^device_s}} class="query-{{vendor}}-{{device}}"{{/device_s}}>{{device_s}}</span>
+ <span>[{{vendor}}:{{device}}]</span>
+ </div>
{{/entries}}
{{/lspci2}}
</div>
@@ -380,13 +420,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>