diff options
Diffstat (limited to 'modules-available/statistics/pages/machine.inc.php')
-rw-r--r-- | modules-available/statistics/pages/machine.inc.php | 753 |
1 files changed, 753 insertions, 0 deletions
diff --git a/modules-available/statistics/pages/machine.inc.php b/modules-available/statistics/pages/machine.inc.php new file mode 100644 index 00000000..1d46b523 --- /dev/null +++ b/modules-available/statistics/pages/machine.inc.php @@ -0,0 +1,753 @@ +<?php + +class SubPage +{ + + public static function doPreprocess() + { + if (!Module::isAvailable('js_chart')) { + ErrorHandler::traceError('js_chart not available'); + } + } + + public static function doRender() + { + self::showMachine(Request::get('uuid', false, 'string')); + } + + private static function fillSessionInfo(&$row) + { + if (!empty($row['currentuser'])) { + $row['username'] = $row['currentuser']; + if (strlen($row['currentsession']) === 36 && Module::isAvailable('dozmod')) { + $lecture = Database::queryFirst("SELECT lectureid, displayname FROM sat.lecture WHERE lectureid = :lectureid", + array('lectureid' => $row['currentsession'])); + if ($lecture !== false) { + $row['currentsession'] = $lecture['displayname']; + $row['lectureid'] = $lecture['lectureid']; + } + $row['session'] = $row['currentsession']; + return; + } + } + $res = Database::simpleQuery('SELECT dateline, username, data FROM statistic' + . " WHERE clientip = :ip AND typeid = '.vmchooser-session-name'" + . ' AND dateline BETWEEN :start AND :end', array( + 'ip' => $row['clientip'], + 'start' => $row['logintime'] - 60, + 'end' => $row['logintime'] + 300, + )); + $session = false; + foreach ($res as $r) { + if ($session === false || abs($session['dateline'] - $row['logintime']) > abs($r['dateline'] - $row['logintime'])) { + $session = $r; + } + } + if ($session !== false) { + $row['session'] = $session['data']; + if (empty($row['currentuser'])) { + $row['username'] = $session['username']; + } + } + } + + private static function showMachine($uuid) + { + $client = Database::queryFirst('SELECT machineuuid, locationid, macaddr, clientip, firstseen, lastseen, logintime, lastboot, state, + mbram, live_tmpsize, live_tmpfree, live_id45size, live_id45free, live_swapsize, live_swapfree, + live_memsize, live_memfree, live_cpuload, live_cputemp, + Length(position) AS hasroomplan, kvmstate, cpumodel, id44mb, id45mb, data, hostname, currentuser, currentsession, notes + FROM machine WHERE machineuuid = :uuid', + array('uuid' => $uuid)); + if ($client === false) { + Message::addError('unknown-machine', $uuid); + return; + } + Render::setTitle(empty($client['hostname']) ? $client['clientip'] : $client['hostname']); + $locations = []; + if ($client['locationid'] > 0 && Module::isAvailable('locations')) { + if (!Location::isLeaf($client['locationid'])) { + $client['hasroomplan'] = false; + } + $locations = Location::getLocationRootChain($client['locationid']); + } + if ($client['locationid'] && $client['hasroomplan'] && Module::isAvailable('roomplanner')) { + $client['roomsvg'] = PvsGenerator::generateSvg($client['locationid'], $client['machineuuid'], + 0, 1, true); + } + User::assertPermission('machine.view-details', (int)$client['locationid']); + // Hack: Get raw collected data + if (Request::get('raw', false)) { + Header('Content-Type: application/json'); + die($client['data']); + } + // Parse data + $hdds = array(); + if ($client['data'][0] === '{') { + $json = json_decode($client['data'], true); + if (is_array($json)) { + $client += self::parseJson($uuid, $json); + $hdds['hdds'] = self::queryHddData($uuid); + } + } else { + self::parseLegacy($client, $hdds); + } + unset($client['data']); + // Get rid of configured speed, if equal to maximum speed + foreach ($client['ram'] as &$item) { + if (isset($item['Configured Memory Speed']) && $item['Configured Memory Speed'] === $item['Speed']) { + unset($item['Configured Memory Speed']); + } + } + unset($item); + // PCI + // 1) get passthrough groups + $passthroughTypes = []; + if (!empty($locations)) { + $hw = new HardwareQuery(HardwareInfo::PCI_DEVICE, $uuid, true); + // TODO: Get list of enabled pass through groups for this client's location + $hw->addForeignJoin(true, '@PASSTHROUGH', 'passthrough_group_x_location', 'groupid', + 'locationid', $locations); + $hw->addGlobalColumn('vendor'); + $hw->addGlobalColumn('device'); + $hw->addGlobalColumn('rev'); + $res = $hw->query(); + foreach ($res as $row) { + $devId = $row['vendor'] . ':' . $row['device'] . ':' . $row['rev']; + if (!isset($passthroughTypes[$devId])) { + $passthroughTypes[$devId] = []; + } + $passthroughTypes[$devId][$row['@PASSTHROUGH']] = $row['@PASSTHROUGH']; + } + } + // 2) Sort and mangle list + $client['lspci1'] = $client['lspci2'] = []; + foreach ($client['lspci'] as $item) { + $devId = $item['vendor'] . ':' . $item['device']; + $item['vendor_s'] = PciId::getPciId(PciId::VENDOR, $item['vendor']); + $item['device_s'] = PciId::getPciId(PciId::DEVICE, $item['vendor'] . $item['device']); + if ($item['vendor_s'] === false) { + $pciLookup[$item['vendor']] = true; + } + if ($item['device_s'] === false) { + $pciLookup[$devId] = true; + } + // Passthrough enabled? + if (isset($passthroughTypes[$devId . ':' . ($item['rev'] ?? '')])) { + $item['pt'] = implode(', ', $passthroughTypes[$devId . ':' . ($item['rev'] ?? '')]); + } + $class = $item['class']; + if ($class === '0300' || $class === '0200' || $class === '0403' || !empty($item['pt'])) { + $dst =& $client['lspci1']; + } else { + $dst =& $client['lspci2']; + } + if (!isset($dst[$class])) { + $dst[$class] = [ + 'class' => $class, + 'class_s' => PciId::getPciId(PciId::DEVCLASS, $class, true), + 'entries' => [], + ]; + } + $dst[$class]['entries'][] = $item; + } + unset($dst, $client['lspci']); + ksort($client['lspci1']); + ksort($client['lspci2']); + $client['lspci1'] = array_values($client['lspci1']); + $client['lspci2'] = array_values($client['lspci2']); + // Runmode + if (Module::isAvailable('runmode')) { + $data = RunMode::getRunMode($uuid, RunMode::DATA_STRINGS); + if ($data !== false) { + $client += $data; + } + } + // Rebootcontrol + if (Module::get('rebootcontrol') !== false) { + $client['canReboot'] = (User::hasPermission('.rebootcontrol.action.reboot', (int)$client['locationid'])); + $client['canShutdown'] = (User::hasPermission('.rebootcontrol.action.shutdown', (int)$client['locationid'])); + $client['canWol'] = (User::hasPermission('.rebootcontrol.action.wol', (int)$client['locationid'])); + $client['canExec'] = (User::hasPermission('.rebootcontrol.action.exec', (int)$client['locationid'])); + $client['rebootcontrol'] = $client['canReboot'] || $client['canShutdown'] || $client['canWol'] || $client['canExec']; + } + // Baseconfig + if (Module::get('baseconfig') !== false + && User::hasPermission('.baseconfig.view', (int)$client['locationid'])) { + $cvs = Database::queryFirst('SELECT Count(*) AS cnt FROM setting_machine WHERE machineuuid = :uuid', ['uuid' => $uuid]); + $client['overriddenVars'] = is_array($cvs) ? $cvs['cnt'] : 0; + $client['hasBaseconfig'] = true; + } + if (!isset($client['isclient'])) { + $client['isclient'] = true; + } + // Mangle fields + $NOW = time(); + if (!$client['isclient']) { + if ($client['state'] === 'IDLE') { + $client['state'] = 'OCCUPIED'; + } + } else { + if ($client['state'] === 'OCCUPIED') { + self::fillSessionInfo($client); + } + } + $client['state_' . $client['state']] = true; + $client['firstseen_s'] = date('d.m.Y H:i', $client['firstseen']); + $client['lastseen_s'] = date('d.m.Y H:i', $client['lastseen']); + $client['logintime_s'] = date('d.m.Y H:i', $client['logintime']); + if ($client['lastboot'] == 0) { + $client['lastboot_s'] = '-'; + } else { + $uptime = $NOW - $client['lastboot']; + $client['lastboot_s'] = date('d.m.Y H:i', $client['lastboot']); + if ($client['state'] === 'IDLE' || $client['state'] === 'OCCUPIED') { + $client['lastboot_s'] .= ' (Up ' . floor($uptime / 86400) . 'd ' . gmdate('H:i', $uptime) . ')'; + } + } + $client['gbram'] = Dictionary::number(ceil($client['mbram'] / 512) / 2, 1); + $client['gbtmp'] = Dictionary::number($client['id44mb'] / 1024); + $client['gbid45'] = Dictionary::number($client['id45mb'] / 1024); + foreach (['tmp', 'id45', 'swap', 'mem'] as $item) { + if ($client['live_' . $item . 'size'] == 0) + continue; + $client['live_' . $item . 'percent'] = round(($client['live_' . $item . 'free'] / $client['live_' . $item . 'size']) * 100, 2); + $client['live_' . $item . 'free_s'] = Util::readableFileSize($client['live_' . $item . 'free'], -1, 2); + } + if ($client['live_cpuload'] <= 100) { + $client['live_cpuload_s'] = $client['live_cpuload'] . "\xe2\x80\x89%"; + $client['live_cpuidle'] = 100 - $client['live_cpuload']; + } + $client['live_cputemppercent'] = max(0, min(100, 110 - $client['live_cputemp'])); + $client['ramclass'] = StatisticsStyling::ramColorClass((int)$client['mbram']); + $client['kvmclass'] = StatisticsStyling::kvmColorClass($client['kvmstate']); + $client['hddclass'] = StatisticsStyling::hddColorClass((int)$client['gbtmp']); + // Format HDD data to strings + foreach ($hdds['hdds'] as &$hdd) { + $hdd['smart_status_failed'] = !($client['smart_status//passed'] ?? 1); + self::mangleHdd($hdd); + } + // BIOS update check + if (!empty($client['bios']['BIOS Revision']) || !empty($client['bios']['Release Date'])) { + if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})#', $client['bios']['Release Date'] ?? '', $out)) { + $client['bios']['Release Date'] = $out[2] . '.' . $out[1] . '.' . $out[3]; + } + $mainboard = ($client['mainboard']['Manufacturer'] ?? '') . '##' . ($client['mainboard']['Product Name'] ?? ''); + $system = ($client['system']['Manufacturer'] ?? '') . '##' . ($client['system']['Product Name'] ?? ''); + $ret = self::checkBios($mainboard, $system, + $client['bios']['Release Date'] ?? null, + $client['bios']['BIOS Revision'] ?? null); + if ($ret === false) { // Not loaded, use AJAX + $params = [ + 'mainboard' => $mainboard, + 'system' => $system, + 'date' => $client['bios']['Release Date'] ?? null, + 'revision' => $client['bios']['BIOS Revision'] ?? null, + ]; + $client['biosurl'] = '?do=statistics&action=bios&' . http_build_query($params); + } elseif (!isset($ret['status']) || $ret['status'] !== 0) { + $client['bioshtml'] = Render::parse('machine-bios-update', $ret); + } + } + // Last booted system. The boot-system entry is created when the client fetches the config, so + // early on, *before* we get the ~poweron event. But in the ~poweron event, the client provides the + // kernel uptime, which is subtracted from what we write to lastboot, so it is actually *before* + // boot-system. + $os = Database::queryFirst("SELECT `data` AS `system`, `dateline` + FROM statistic + WHERE (dateline >= :lastboot) AND typeid = 'boot-system' AND machineuuid = :uuid + ORDER BY dateline ASC LIMIT 1", + ['lastboot' => $client['lastboot'], 'uuid' => $uuid]); + if ($os !== false) { + $client['minilinux'] = $os['system']; + $graphical = Database::queryFirst("SELECT `dateline` + FROM statistic + WHERE (dateline >= :lastboot) AND typeid = 'graphical-startup' AND machineuuid = :uuid + ORDER BY dateline ASC LIMIT 1", + ['lastboot' => $client['lastboot'], 'uuid' => $uuid]); + if ($graphical !== false) { + $boottime = $graphical['dateline'] - $client['lastboot']; + if ($boottime < 400) { // Sanity-check + $client['boottime_s'] = gmdate('i:s', $boottime); + } + } + } + // Get locations + if (Module::isAvailable('locations')) { + $locs = Location::getLocationsAssoc(); + $next = (int)$client['locationid']; + $output = array(); + while (isset($locs[$next])) { + array_unshift($output, $locs[$next]); + $next = $locs[$next]['parentlocationid']; + } + $client['locations'] = $output; + } + // Screens TODO Move everything else to hw table instead of blob parsing above + // `devicetype`, `devicename`, `subid`, `machineuuid` + $res = Database::simpleQuery("SELECT m.hwid, h.hwname, m.devpath AS connector, m.disconnecttime," + . " p.value AS resolution, q.prop AS projector FROM machine_x_hw m" + . " INNER JOIN statistic_hw h ON (m.hwid = h.hwid AND h.hwtype = :screen)" + . " LEFT JOIN machine_x_hw_prop p ON (m.machinehwid = p.machinehwid AND p.prop = 'resolution')" + . " LEFT JOIN statistic_hw_prop q ON (m.hwid = q.hwid AND q.prop = 'projector')" + . " WHERE m.machineuuid = :uuid", + array('screen' => HardwareInfo::SCREEN, 'uuid' => $uuid)); + $client['screens'] = array(); + $ports = array(); + foreach ($res as $row) { + if ($row['disconnecttime'] != 0) + continue; + $ports[] = $row['connector']; + $client['screens'][] = $row; + } + array_multisort($ports, SORT_ASC, $client['screens']); + Permission::addGlobalTags($client['perms'], null, ['hardware.projectors.edit', 'hardware.projectors.view']); + // Throw output at user + Render::addTemplate('machine-main', $client); + if (!empty($pciLookup)) { + Render::addTemplate('js-pciquery', + ['missing_ids' => json_encode(array_keys($pciLookup))]); + } + // Sessions + $NOW = time(); + $cutoff = $NOW - 86400 * 7; + //if ($cutoff < $client['firstseen']) $cutoff = $client['firstseen']; + $scale = 100 / ($NOW - $cutoff); + $res = Database::simpleQuery('SELECT dateline, typeid, data FROM statistic' + . " WHERE dateline > :cutoff AND typeid IN (:sessionLength, :offlineLength) AND machineuuid = :uuid ORDER BY dateline ASC", array( + 'cutoff' => $cutoff - 86400 * 14, + 'uuid' => $uuid, + 'sessionLength' => Statistics::SESSION_LENGTH, + 'offlineLength' => Statistics::OFFLINE_LENGTH, + )); + $spans['rows'] = array(); + $spans['graph'] = ''; + $last = false; + $first = true; + foreach ($res as $row) { + if (!$client['isclient'] && $row['typeid'] === Statistics::SESSION_LENGTH) + continue; // Don't differentiate between session and idle for non-clients + if ($first && $row['dateline'] > $cutoff && $client['lastboot'] > $cutoff) { + // Special case: offline before + $spans['graph'] .= '<div style="background:#444;left:0;width:' . round((min($row['dateline'], $client['lastboot']) - $cutoff) * $scale, 2) . '%"> </div>'; + } + $first = false; + if ($row['dateline'] + $row['data'] < $cutoff || $row['data'] > 864000) { + continue; + } + if ($last !== false && abs($last['dateline'] - $row['dateline']) < 30 + && abs($last['data'] - $row['data']) < 30 + ) { + continue; + } + if ($last !== false && $last['dateline'] + $last['data'] > $row['dateline']) { + $point = $last['dateline'] + $last['data']; + $row['data'] -= ($point - $row['dateline']); + $row['dateline'] = $point; + } + if ($row['dateline'] < $cutoff) { + $row['data'] -= ($cutoff - $row['dateline']); + $row['dateline'] = $cutoff; + } + $row['from'] = Util::prettyTime($row['dateline']); + $row['duration'] = floor($row['data'] / 86400) . 'd ' . gmdate('H:i', $row['data']); + if ($row['typeid'] === Statistics::OFFLINE_LENGTH) { + $row['glyph'] = 'off'; + $color = '#444'; + } elseif ($row['typeid'] === Statistics::SUSPEND_LENGTH) { + $row['glyph'] = 'pause'; + $color = '#686'; + } else { + $row['glyph'] = 'user'; + $color = '#e77'; + } + $spans['graph'] .= '<div style="background:' . $color . ';left:' . round(($row['dateline'] - $cutoff) * $scale, 2) . '%;width:' . round(($row['data']) * $scale, 2) . '%"> </div>'; + if ($client['isclient']) { + $spans['rows'][] = $row; + } + $last = $row; + } + if ($first && $client['lastboot'] > $cutoff) { + // Special case: offline before + $spans['graph'] .= '<div style="background:#444;left:0;width:' . round(($client['lastboot'] - $cutoff) * $scale, 2) . '%"> </div>'; + } elseif ($first) { + // Not seen in last two weeks + $spans['graph'] .= '<div style="background:#444;left:0;width:100%"> </div>'; + } + if ($client['state'] === 'OCCUPIED') { + $spans['graph'] .= '<div style="background:#e99;left:' . round(($client['logintime'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['logintime'] + 900) * $scale, 2) . '%"> </div>'; + $spans['rows'][] = [ + 'from' => Util::prettyTime($client['logintime']), + 'duration' => '-', + 'glyph' => 'user', + ]; + } elseif ($client['state'] === 'OFFLINE') { + $spans['graph'] .= '<div style="background:#444;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%"> </div>'; + $spans['rows'][] = [ + 'from' => Util::prettyTime($client['lastseen']), + 'duration' => '-', + 'glyph' => 'off', + ]; + } elseif ($client['state'] === 'STANDBY') { + $spans['graph'] .= '<div style="background:#686;left:' . round(($client['lastseen'] - $cutoff) * $scale, 2) . '%;width:' . round(($NOW - $client['lastseen'] + 900) * $scale, 2) . '%"> </div>'; + $spans['rows'][] = [ + 'from' => Util::prettyTime($client['lastseen']), + 'duration' => '-', + 'glyph' => 'pause', + ]; + } + $t = explode('-', date('Y-n-j-G', $cutoff)); + if ($t[3] >= 8 && $t[3] <= 22) { + $start = mktime(22, 0, 0, $t[1], $t[2], $t[0]); + } else { + $start = mktime(22, 0, 0, $t[1], $t[2] - 1, $t[0]); + } + for ($i = $start; $i < $NOW; $i += 86400) { + $spans['graph'] .= '<div style="background:rgba(0,0,90,.2);left:' . round(($i - $cutoff) * $scale, 2) . '%;width:' . round((10 * 3600) * $scale, 2) . '%"> </div>'; + } + if (count($spans['rows']) > 10) { + $spans['hasrows2'] = true; + $spans['rows2'] = array_slice($spans['rows'], (int)ceil(count($spans['rows']) / 2)); + $spans['rows'] = array_slice($spans['rows'], 0, (int)ceil(count($spans['rows']) / 2)); + } + $spans['isclient'] = $client['isclient']; + Render::addTemplate('machine-usage', $spans); + // Any hdds? + if (!empty($hdds['hdds'])) { + Render::addTemplate('machine-hdds', $hdds); + } + // Client log + if (Module::get('syslog') !== false) { + $lres = Database::simpleQuery('SELECT logid, dateline, logtypeid, clientip, description, extra FROM clientlog' + . ' WHERE machineuuid = :uuid ORDER BY logid DESC LIMIT 25', array('uuid' => $client['machineuuid'])); + $count = 0; + $log = array(); + foreach ($lres as $row) { + if (substr($row['description'], -5) === 'on :0' && strpos($row['description'], 'root logged') === false) { + continue; + } + $row['date'] = Util::prettyTime($row['dateline']); + $row['icon'] = self::eventToIconName($row['logtypeid']); + $log[] = $row; + if (++$count === 10) { + break; + } + } + Render::addTemplate('syslog', array( + 'machineuuid' => $client['machineuuid'], + 'list' => $log, + )); + } + // Notes + if (User::hasPermission('machine.note.*', (int)$client['locationid'])) { + Permission::addGlobalTags($client['perms'], (int)$client['locationid'], ['machine.note.edit']); + Render::addTemplate('machine-notes', $client); + } + } + + private static function parseLegacy(array &$client, array &$hdds) + { + // Parse the giant blob of data + if (strpos($client['data'], "\r") !== false) { + $client['data'] = str_replace("\r", "\n", $client['data']); + } + if (preg_match_all('/##### ([^#]+) #+$(.*?)^#####/ims', $client['data'] . '########', $out, PREG_SET_ORDER)) { + foreach ($out as $section) { + if ($section[1] === 'CPU') { + HardwareParserLegacy::parseCpu($client, $section[2]); + } + if ($section[1] === 'dmidecode') { + HardwareParserLegacy::parseDmiDecode($client, $section[2]); + } + if ($section[1] === 'Partition tables') { + HardwareParserLegacy::parseHdd($hdds, $section[2]); + } + if ($section[1] === 'PCI ID') { + $client['lspci'] = HardwareParserLegacy::parsePci($section[2]); + } + if (isset($hdds['hdds']) && $section[1] === 'smartctl') { + // This currently requires that the partition table section comes first... + HardwareParserLegacy::parseSmartctl($hdds['hdds'], $section[2]); + } + } + } + } + + private static function parseJson(string $uuid, array $json): array + { + $return = [ + 'lspci' => $json['lspci'] ?? [], + 'ram' => array_map(function($item) { + return HardwareParser::prepareDmiProperties($item); + }, HardwareParser::getDmiHandles($json, 17)), + ]; + foreach ($return['ram'] as $ram) { + if (!empty($ram['Form Factor']) && !empty($ram['Type'])) { + $return['ramtype'] = $ram['Type'] . '-' . $ram['Form Factor']; + break; + } + } + $need = [ + 'bios' => 0, + 'system' => 1, + 'mainboard' => 2, + ]; + foreach ($need as $name => $id) { + $return[$name] = HardwareParser::prepareDmiProperties( + HardwareParser::getDmiHandles($json, $id)[0] ?? []); + } + $q = new HardwareQuery(HardwareInfo::MAINBOARD, $uuid); + $q->addGlobalColumn('Memory Maximum Capacity'); + $q->addGlobalColumn('Memory Slot Count'); + $q->addLocalColumn('cpu-sockets'); + $q->addLocalColumn('cpu-cores'); + $q->addLocalColumn('cpu-threads'); + $q->addLocalColumn('nic-speed'); + $q->addLocalColumn('nic-duplex'); + $res = $q->query()->fetch(); + if (is_array($res)) { + $return += $res; + } + return $return; + } + + private static function queryHddData(string $uuid): array + { + $hdds = []; + $ret = Database::simpleQuery("SELECT mp.`machinehwid`, mp.`prop`, mp.`value`, mp.`numeric` + FROM machine_x_hw_prop mp + INNER JOIN machine_x_hw mxhw ON (mp.machinehwid = mxhw.machinehwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0) + INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type) + UNION SELECT mxhw.`machinehwid`, hwp.`prop`, hwp.`value`, hwp.`numeric` + FROM statistic_hw_prop hwp + INNER JOIN machine_x_hw mxhw ON (hwp.hwid = mxhw.hwid AND mxhw.machineuuid = :uuid AND mxhw.disconnecttime = 0) + INNER JOIN statistic_hw sh ON (mxhw.hwid = sh.hwid AND sh.hwtype = :type) + ", + ['type' => HardwareInfo::HDD, 'uuid' => $uuid]); + foreach ($ret as $row) { + if (!isset($hdds[$row['machinehwid']])) { + $hdds[$row['machinehwid']] = ['partitions' => []]; + } + $hdd =& $hdds[$row['machinehwid']]; + if (preg_match('/^(attr_[0-9]+)_(.*)$/', $row['prop'], $out)) { + // SMART attributes + if (!isset($hdd[$out[1]])) { + $hdd[$out[1]] = []; + } + $hdd[$out[1]][$out[2]] = $row['numeric'] ?? $row['value']; + } elseif (preg_match('/^part_([0-9]+)_(.*)$/', $row['prop'], $out)) { + // Partitions + if (!isset($hdd['partitions'][$out[1]])) { + $hdd['partitions'][$out[1]] = ['id' => 'dev-' . count($hdds) . '-' . $out[1], 'index' => $out[1]]; + } + $hdd['partitions'][$out[1]][$out[2]] = $row['numeric'] ?? $row['value']; + } else { + $hdd[$row['prop']] = $row['numeric'] ?? $row['value']; + } + } + $result = []; + foreach ($hdds as $k => &$hdd) { + if (substr($hdd['dev'] ?? '/dev/sr', 0, 7) === '/dev/sr') + continue; + $hdd['devid'] = 'k' . $k; + $hdd['partitions'] = array_values($hdd['partitions']); + $result[] = $hdd; + } + return $result; + } + + private static function mangleHdd(array &$hdd) + { + static $hddidx = 0; + if (!isset($hdd['size']) || !is_numeric($hdd['size'])) { + $hdd['size'] = 0; + } + $hdd['hddidx'] = $hddidx++; + $hours = $hdd['power_on_time//hours'] ?? $hdd['attr_9']['raw'] ?? $hdd['power_on_hours'] + ?? $hdd['power_on_time']['hours'] ?? null; + if ($hours !== null) { + $hdd['PowerOnTime'] = ''; + $val = (int)str_replace('.', '', $hours); + if ($val > 8760) { + $hdd['PowerOnTime'] .= floor($val / 8760) . 'Y, '; + $val %= 8760; + } + if ($val > 720) { + $hdd['PowerOnTime'] .= floor($val / 720) . 'M, '; + $val %= 720; + } + if ($val > 24) { + $hdd['PowerOnTime'] .= floor($val / 24) . 'd, '; + $val %= 24; + } + $hdd['PowerOnTime'] .= $val . 'h'; + } + // Sort by start for building pie-chart + $xx = array_column($hdd['partitions'], 'start'); + array_multisort($xx, SORT_ASC, SORT_NUMERIC, + $hdd['partitions']); + $used = 0; + $json = []; + $lastEnd = 0; + $minDisplaySize = $hdd['size'] / 150; + $i = 0; + foreach ($hdd['partitions'] as &$part) { + $dist = $part['start'] - $lastEnd; + if ($dist > $minDisplaySize) { + $json[] = ['value' => $dist, 'color' => '#aaa']; + $i++; + } + if ($part['size'] > $minDisplaySize) { + $json[] = ['value' => $part['size'], 'color' => self::typeToColor($part)]; + $part['idx'] = $i++; + } + $part['size_s'] = Util::readableFileSize($part['size']); + $used += $part['size']; + $lastEnd = $part['start'] + $part['size']; + if (!isset($part['name']) || isset($part['slxtype'])) { + $part['name'] = self::partTypeToName($part['slxtype'] ?? $part['type']); + } + } + $dist = $hdd['size'] - $lastEnd; + if ($dist > $minDisplaySize) { + $json[] = ['value' => $dist, 'color' => '#aaa']; + } + $hdd['json'] = json_encode($json); + $hdd['size_s'] = Util::readableFileSize($hdd['size']); + if ($hdd['size'] - $used > 1000000000) { + $hdd['unused_s'] = Util::readableFileSize($hdd['size'] - $used); + } + // Finally sort by index for table display + array_multisort(array_column($hdd['partitions'], 'index'), SORT_ASC, + $hdd['partitions']); + } + + private static function typeToColor(array $part): string + { + switch ($part['slxtype'] ?? $part['type']) { + case 44: + return '#5c1'; + case 45: + return '#0d7'; + case 82: + return '#48f'; + } + return '#e55'; + } + + private static function eventToIconName($event): string + { + switch ($event) { + case 'session-open': + return 'glyphicon-log-in'; + case 'session-close': + return 'glyphicon-log-out'; + case 'partition-swap': + return 'glyphicon-info-sign'; + case 'partition-temp': + case 'smartctl-realloc': + return 'glyphicon-exclamation-sign'; + default: + return 'glyphicon-minus'; + } + } + + const BIOS_CACHE = '/tmp/bwlp-bios.json'; + + public static function ajaxCheckBios() + { + $mainboard = Request::any('mainboard', false, 'string'); + $system = Request::any('system', false, 'string'); + $date = Request::any('date', false, 'string'); + $revision = Request::any('revision', false, 'string'); + $reply = self::checkBios($mainboard, $system, $date, $revision); + if ($reply === false) { + $data = Download::asString(CONFIG_BIOS_URL, 3, $err); + if ($err < 200 || $err >= 300) { + $reply = ['error' => 'HTTP: ' . $err]; + } else { + file_put_contents(self::BIOS_CACHE, $data); + $data = json_decode($data, true); + $reply = self::checkBios($mainboard, $system, $date, $revision, $data); + } + } + if ($reply === false) { + $reply = ['error' => 'Internal Error']; + } + if (isset($reply['status']) && $reply['status'] === 0) + exit; // Show nothing, 0 means OK + die(Render::parse('machine-bios-update', $reply)); + } + + private static function checkBios(string $mainboard, string $system, ?string $date, ?string $revision, $json = null) + { + if ($json === null) { + if (!file_exists(self::BIOS_CACHE) || filemtime(self::BIOS_CACHE) + 3600 < time()) + return false; + $json = json_decode(file_get_contents(self::BIOS_CACHE), true); + } + if (!is_array($json) || !isset($json['system'])) + return ['error' => 'Malformed JSON, no system key']; + if (isset($json['system'][$system]['fixes']) && isset($json['system'][$system]['match'])) { + $match =& $json['system'][$system]; + } elseif (isset($json['mainboard'][$mainboard]['fixes']) && isset($json['mainboard'][$mainboard]['match'])) { + $match =& $json['mainboard'][$mainboard]; + } else { + return ['status' => 0]; + } + $key = $match['match']; + if ($key === 'revision' && $revision !== null) { + $cmp = function ($item) { $s = explode('.', $item); return $s[0] * 0x10000 + $s[1]; }; + $reference = $cmp($revision); + } elseif ($key === 'date' && $date !== null) { + $cmp = function ($item) { $s = explode('.', $item); return $s[2] * 10000 + $s[1] * 100 + $s[0]; }; + $reference = $cmp($date); + } else { + return ['error' => 'Invalid comparison key: ' . $key]; + } + $retval = ['fixes' => []]; + $level = 0; + foreach ($match['fixes'] as $fix) { + if ($cmp($fix[$key]) > $reference) { + class_exists('Dictionary'); // Trigger setup of lang stuff + $lang = isset($fix['text'][LANG]) ? LANG : 'en'; + $fix['text'] = $fix['text'][$lang]; + $retval['fixes'][] = $fix; + $level = max($level, $fix['level']); + } + } + $retval['url'] = $match['url']; + $retval['status'] = $level; + if ($level > 5) { + $retval['class'] = 'danger'; + } elseif ($level > 3) { + $retval['class'] = 'warning'; + } else { + $retval['class'] = 'info'; + } + return $retval; + } + + /** + * @param string $type MBR-type or GPT UUID + * @return string Name of partition type if known, otherwise, $type is returned + */ + private static function partTypeToName(string $type): string + { + switch ($type) { + case '44': + case '45': + return 'OpenSLX-ID' . $type; + case '82': + return 'Linux Swap'; + case '83': + return 'Linux'; + case '7': + return 'NTFS/Windows'; + case 'ef': + return 'EFI'; + } + return HardwareInfo::GPT[$type] ?? $type; + } + +} |