handleServiceAction(substr($action, 8)); break; case 'apt-update': User::assertPermission('apt.update'); $aptAction = 'UPDATE'; break; case 'apt-upgrade': User::assertPermission('apt.upgrade'); $aptAction = 'UPGRADE'; break; case 'apt-full-upgrade': User::assertPermission('apt.upgrade'); $aptAction = 'FULL_UPGRADE'; break; case 'apt-autoremove': User::assertPermission('apt.autoremove'); $aptAction = 'AUTOREMOVE'; break; case 'apt-fix': User::assertPermission('apt.fix'); $aptAction = 'FIX'; break; default: } if ($aptAction !== null) { if (!Taskmanager::isRunning(Taskmanager::status(self::TM_UPDATE_UUID))) { $task = Taskmanager::submit('AptUpgrade', ['mode' => $aptAction, 'id' => self::TM_UPDATE_UUID]); Taskmanager::release($task); } Util::redirect('?do=systemstatus#id-ListUpgradable_pane'); } if (Request::isPost()) { Util::redirect('?do=systemstatus'); } User::assertPermission('*'); } private function handleServiceAction(string $action) { $service = Request::post('service', Request::REQUIRED, 'string'); $task = Taskmanager::submit('Systemctl', ['operation' => $action, 'service' => $service]); $extra = ''; $cmp = preg_replace('/(@.*|\.service)$/', '', $service); User::assertPermission("restart.$cmp"); if ($cmp === 'dmsd') { $extra = '#id-DmsdLog_pane'; } elseif ($cmp === 'ldadp') { $extra = '#id-LdadpLog_pane'; } elseif ($cmp === 'dnbd3-server') { $extra = '#id-Dnbd3Log_pane'; } Util::redirect('?do=systemstatus&taskid=' . $task['id'] . '&taskname=' . urlencode($service) . $extra); } protected function doRender() { $data = array(); $data['taskid'] = Request::get('taskid', '', 'string'); $data['taskname'] = Request::get('taskname', 'Reboot', 'string'); $tabs = ['DmsdLog', 'Netstat', 'PsList', 'LdadpLog', 'LighttpdLog', 'Dnbd3Log', 'ListUpgradable']; $data['tabs'] = array(); // Dictionary::translate('tab_DmsdLog') Dictionary::translate('tab_LdadpLog') Dictionary::translate('tab_Netstat') // Dictionary::translate('tab_LighttpdLog') Dictionary::translate('tab_PsList') Dictionary::translate('tab_Dnbd3Log') // Dictionary::translate('tab_ListUpgradable') foreach ($tabs as $tab) { $data['tabs'][] = array( 'type' => $tab, 'name' => Dictionary::translate('tab_' . $tab), 'enabled' => User::hasPermission('tab.' . $tab), 'important' => $tab === 'ListUpgradable' && (SystemStatus::getAptLastDbUpdateTime() + 864000 < time() || SystemStatus::getUpgradableSecurityCount() > 0), ); } Permission::addGlobalTags($data['perms'], null, ['serverreboot']); $pkgs = SystemStatus::getPackagesRequiringReboot(); if (!empty($pkgs)) { $data['packages'] = implode(', ', $pkgs); } Render::addTemplate('_page', $data); } protected function doAjax() { User::load(); if (!User::isLoggedIn()) return; $action = 'ajax' . Request::any('action'); if (method_exists($this, $action)) { $this->$action(); Message::renderList(); } else { // get_class() !== get_class($this) echo "Action $action not known in " . get_class($this); } } protected function ajaxListUpgradable() { User::assertPermission("tab.listupgradable"); if (User::hasPermission('apt.update') && Taskmanager::isRunning(Taskmanager::status(self::TM_UPDATE_UUID))) { echo Render::parse('sys-update-update', [ 'taskid' => self::TM_UPDATE_UUID, 'rnd' => mt_rand(), ]); return; } $task = SystemStatus::getUpgradableTask(); // Estimate last time package list was updated $lastPackageInstalled = SystemStatus::getDpkgLastPackageChanges(); $lastListDownloadAttempt = SystemStatus::getAptLastUpdateAttemptTime(); $updateDbTime = SystemStatus::getAptLastDbUpdateTime(); $perms = []; Permission::addGlobalTags($perms, 0, ['apt.update', 'apt.upgrade', 'apt.autoremove', 'apt.fix']); if ($task !== false) { $task = Taskmanager::waitComplete($task, 30000); if (Taskmanager::isFailed($task) || !Taskmanager::isFinished($task)) { Taskmanager::addErrorMessage($task); return; } if (!Taskmanager::isFailed($task) && empty($task['data']['packages'])) { $task['data']['error'] = ''; } } else { $task['data']['error'] = 'ECONNREFUSED'; } foreach ($task['data']['packages'] as &$pkg) { if (substr($pkg['source'], -9) === '-security') { $pkg['row_class'] = 'bg-danger'; } else { $pkg['row_class'] = ''; } } unset($pkg); echo Render::parse('sys-update-main', [ 'task' => $task['data'], 'lastDownload' => Util::prettyTime($lastListDownloadAttempt), 'lastChanged' => Util::prettyTime($updateDbTime), 'lastInstalled' => Util::prettyTime($lastPackageInstalled), 'perm' => $perms, 'list_old' => $lastListDownloadAttempt + 86400 < time(), 'needReboot' => implode(', ', SystemStatus::getPackagesRequiringReboot()), ]); } protected function ajaxDiskStat() { User::assertPermission("show.overview.diskstat"); if (!SystemStatus::diskStat($systemUsage, $storeUsage, $currentSource, $wantedSource)) return; $data = ['system' => $this->convertDiskStat($systemUsage, 3000)]; if ($wantedSource === false) { // Not configured yet, nothing to display $data['notConfigured'] = true; } elseif ($wantedSource === $currentSource) { // Fine and dandy $data['store'] = $this->convertDiskStat($storeUsage, 250000); } elseif ($currentSource === false) { // No current source, nothing mounted $data['storeMissing'] = true; } else { // Something else mounted $data['wrongStore'] = $currentSource; } echo Render::parse('diskstat', $data); } protected function ajaxAddressList() { User::assertPermission("show.overview.addresses"); $task = Taskmanager::submit('LocalAddressesList'); if ($task === false) return; $task = Taskmanager::waitComplete($task, 3000); if (empty($task['data']['addresses'])) { Taskmanager::addErrorMessage($task); return; } $sort = array(); $primary = Property::getServerIp(); foreach ($task['data']['addresses'] as &$addr) { $sort[] = $addr['type'] . $addr['ip']; if ($addr['ip'] === $primary) $addr['primary'] = true; } array_multisort($sort, SORT_STRING, $task['data']['addresses']); echo Render::parse('addresses', array( 'addresses' => $task['data']['addresses'] )); } private function sysInfo(): array { $data = array(); $memInfo = file_get_contents('/proc/meminfo'); $stat = file_get_contents('/proc/stat'); preg_match_all('/\b(\w+):\s+(\d+)\s/', $memInfo, $out, PREG_SET_ORDER); foreach ($out as $e) { $data[$e[1]] = $e[2]; } /** @var array{user: numeric, nice: numeric, system: numeric, idle: numeric, iowait: numeric, irq: numeric, softirq: numeric} $out */ if (preg_match('/\bcpu\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)(\s|$)/', $stat, $out)) { $data['CpuTotal'] = $out['user'] + $out['nice'] + $out['system'] + $out['idle'] + $out['iowait'] + $out['irq'] + $out['softirq']; $data['CpuIdle'] = $out['idle'] + $out['iowait']; $data['CpuSystem'] = $out['irq'] + $out['softirq']; } return $data; } protected function ajaxSystemInfo() { User::assertPermission("show.overview.systeminfo"); $cpuInfo = file_get_contents('/proc/cpuinfo'); $uptime = file_get_contents('/proc/uptime'); $cpuCount = preg_match_all('/\bprocessor\s/', $cpuInfo, $out); $out = parse_ini_file('/etc/os-release'); $data = array( 'cpuCount' => $cpuCount, 'memTotal' => '???', 'memFree' => '???', 'swapTotal' => '???', 'swapUsed' => '???', 'uptime' => '???', 'kernel' => php_uname('r'), 'distribution' => $out['PRETTY_NAME'] ?? (($out['NAME'] ?? '???') . ' ' . ($out['VERSION'] ?? '???')), ); if (preg_match('/^(\d+)\D/', $uptime, $out)) { $data['uptime'] = floor($out[1] / 86400) . ' ' . Dictionary::translate('lang_days') . ', ' . floor(($out[1] % 86400) / 3600) . ' ' . Dictionary::translate('lang_hours'); } $info = $this->sysInfo(); if (isset($info['MemTotal']) && isset($info['MemFree']) && isset($info['SwapTotal'])) { $avail = $info['MemAvailable'] ?? ($info['MemFree'] + $info['Buffers'] + $info['Cached']); $data['memTotal'] = Util::readableFileSize($info['MemTotal'] * 1024); $data['memFree'] = Util::readableFileSize($avail * 1024); $data['memPercent'] = 100 - round(($avail / $info['MemTotal']) * 100); $data['swapTotal'] = Util::readableFileSize($info['SwapTotal'] * 1024); $data['swapUsed'] = Util::readableFileSize(($info['SwapTotal'] - $info['SwapFree']) * 1024); $data['swapPercent'] = 100 - round(($info['SwapFree'] / $info['SwapTotal']) * 100); if ($data['swapTotal'] > 0 && $data['memPercent'] > 75) { $data['swapWarning'] = ($data['swapPercent'] > 80 || $info['SwapFree'] < 400000); } } if (isset($info['CpuIdle']) && isset($info['CpuSystem']) && isset($info['CpuTotal'])) { $data['cpuLoad'] = 100 - round(($info['CpuIdle'] / $info['CpuTotal']) * 100); $data['cpuSystem'] = round(($info['CpuSystem'] / $info['CpuTotal']) * 100); $data['cpuLoadOk'] = true; $data['CpuTotal'] = $info['CpuTotal']; $data['CpuIdle'] = $info['CpuIdle']; } echo Render::parse('systeminfo', $data); } protected function ajaxSysPoll() { User::assertPermission("show.overview.systeminfo"); $info = $this->sysInfo(); $data = array( 'CpuTotal' => $info['CpuTotal'], 'CpuIdle' => $info['CpuIdle'], 'MemPercent' => 100 - round((($info['MemFree'] + $info['Buffers'] + $info['Cached']) / $info['MemTotal']) * 100), 'SwapPercent' => 100 - round(($info['SwapFree'] / $info['SwapTotal']) * 100) ); Header('Content-Type: application/json; charset=utf-8'); die(json_encode($data)); } protected function ajaxServices() { User::assertPermission("show.overview.services"); $data = array('services' => array()); $tasks = array(); $todo = ['dmsd', 'tftpd-hpa']; if (Module::get('dnbd3') !== false) { $todo[] = 'dnbd3-server'; $todo[] = 'dnbd3-master-proxy'; } foreach ($todo as $svc) { $tasks[] = array( 'name' => $svc, 'task' => Taskmanager::submit('Systemctl', ['service' => $svc, 'operation' => 'is-active']) ); } $ldapIds = $ldadp = false; if (Module::isAvailable('sysconfig')) { $ldapIds = ConfigModuleBaseLdap::getActiveModuleIds(); if (!empty($ldapIds)) { $ldadp = array( // No name - no display 'task' => ConfigModuleBaseLdap::ldadp('check', $ldapIds) // TODO: Proper --check usage ); $tasks[] =& $ldadp; } } $deadline = time() + 10; do { $done = true; foreach ($tasks as &$task) { if (!is_string($task['task']) && (Taskmanager::isFailed($task['task']) || Taskmanager::isFinished($task['task']))) continue; $task['task'] = Taskmanager::waitComplete($task['task'], 100); if (!Taskmanager::isFailed($task['task']) && !Taskmanager::isFinished($task['task'])) { $done = false; } } unset($task); } while (!$done && time() < $deadline); foreach ($tasks as $task) { if (!isset($task['name'])) continue; $fail = Taskmanager::isFailed($task['task']); $entry = array( 'name' => $task['name'], 'service' => $task['name'], 'fail' => $fail, ); if ($fail) { if (!isset($task['task']['data'])) { $entry['error'] = 'Taskmanager Error'; } elseif (isset($task['task']['data']['messages'])) { $entry['error'] = $task['task']['data']['messages']; } } $data['services'][] = $entry; } if ($ldadp !== false) { //error_log(print_r($ldadp, true)); preg_match_all('/^ldadp@(\d+)\.service\s+(\S+)$/m', $ldadp['task']['data']['messages'], $out, PREG_SET_ORDER); $instances = []; foreach ($out as $instance) { $instances[$instance[1]] = $instance[2]; } foreach ($ldapIds as $id) { $status = $instances[$id] ?? 'failed'; $fail = ($status !== 'running'); $data['services'][] = [ 'name' => 'LDAP/AD Proxy #' . $id, 'service' => 'ldadp@' . $id, 'fail' => $fail, 'error' => $fail ? $status : false, ]; } } echo Render::parse('services', $data); } protected function ajaxDmsdLog() { $this->showJournal('dmsd.service', 'tab.dmsdlog'); } protected function ajaxDnbd3Log() { $this->showJournal('dnbd3-server.service', 'tab.dnbd3log'); } protected function showJournal($service, $permission) { $cmp = preg_replace('/(@.*|\.service)$/', '', $service); User::assertPermission($permission); $output = [ 'name' => $service, 'service' => $service, 'task' => Taskmanager::submit('Systemctl', ['operation' => 'journal', 'service' => $service]), 'restart_disabled' => User::hasPermission('restart.' . $cmp) ? '' : 'disabled', ]; echo Render::parse('ajax-journal', ['modules' => [$output]]); } private function grepLighttpdLog(string $file, int $num): array { $fh = @fopen($file, 'r'); if ($fh === false) return ['Error opening ' . $file]; $ret = []; fseek($fh, -($num * 2000), SEEK_END); if (ftell($fh) > 0) { // Throw away first line, as it's most likely incomplete fgets($fh, 1000); } while (($line = fgets($fh, 1000))) { if (strpos($line, ':SSL routines:') === false && strpos($line, ' SSL: -1 5 104 Connection reset by peer') === false && strpos($line, 'GET/HEAD with content-length') === false && strpos($line, 'POST-request, but content-length missing') === false) { $ret[] = $line; } } fclose($fh); return array_slice($ret, -$num); } protected function ajaxLighttpdLog() { User::assertPermission("tab.lighttpdlog"); $lines = $this->grepLighttpdLog('/var/log/lighttpd/error.log', 60); if (count($lines) < 50) { $lines = array_merge( $this->grepLighttpdLog('/var/log/lighttpd/error.log.1', 60 - count($lines)), $lines); } echo '
', htmlspecialchars(implode('', $lines), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '
'; } protected function ajaxLdadpLog() { User::assertPermission("tab.ldadplog"); if (!Module::isAvailable('sysconfig')) { die('SysConfig module not enabled'); } $ids = ConfigModuleBaseLdap::getActiveModuleIds(); //error_log(print_r($ids, true)); $output = []; foreach ($ids as $id) { $module = ConfigModule::get($id); if ($module === null) { $name = "#$id"; } else { $name = $module->title(); } $service = "ldadp@{$id}.service"; $output[] = [ 'name' => $name, 'service' => $service, 'task' => Taskmanager::submit('Systemctl', ['operation' => 'journal', 'service' => $service]), ]; } //error_log(print_r($output, true)); echo Render::parse('ajax-journal', ['modules' => $output]); } protected function ajaxNetstat() { User::assertPermission("tab.netstat"); $taskId = Taskmanager::submit('Netstat'); if ($taskId === false) return; $status = Taskmanager::waitComplete($taskId, 3500); $data = $status['data']['messages'] ?? 'Taskmanager error'; echo '
', htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '
'; } protected function ajaxPsList() { User::assertPermission("tab.pslist"); $taskId = Taskmanager::submit('PsList'); if ($taskId === false) return; $status = Taskmanager::waitComplete($taskId, 3500); $data = $status['data']['messages'] ?? 'Taskmanager error'; echo '
', htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '
'; } /** * @return array{percent: numeric, size: string, free: string, color: string, filesystem: string} */ private function convertDiskStat(array $stat, int $minFreeMb): array { return [ 'percent' => $stat['usedPercent'], 'size' => Util::readableFileSize($stat['sizeKb'] * 1024), 'free' => Util::readableFileSize($stat['freeKb'] * 1024), 'color' => $this->usageColor($stat, $minFreeMb), 'filesystem' => $stat['fileSystem'], ]; } private function usageColor(array $stat, int $minFreeMb): string { $freeMb = round($stat['freeKb'] / 1024); // All good is half space free, or 4x the min free amount, whatever is more $okFreeMb = max($minFreeMb * 4, round($stat['sizeKb']) / (1024 * 2)); if ($freeMb > $okFreeMb) { $usedPercent = 0; } elseif ($freeMb < $minFreeMb) { $usedPercent = 100; } else { $usedPercent = 100 - round(($freeMb - $minFreeMb) / ($okFreeMb - $minFreeMb) * 100); } if ($usedPercent <= 50) { $r = $b = $usedPercent / 3; $g = (100 - $usedPercent * (50 / 80)); } elseif ($usedPercent <= 70) { $r = 55 + ($usedPercent - 50) * (30 / 20); $g = 60; $b = 0; } else { $r = ($usedPercent - 70) / 3 + 90; $g = (100 - $usedPercent) * (60 / 30); $b = 0; } $r = dechex((int)round($r * 2.55)); $g = dechex((int)round($g * 2.55)); $b = dechex((int)round($b * 2.55)); return sprintf("%02s%02s%02s", $r, $g, $b); } }