diff options
Diffstat (limited to 'modules-available/minilinux')
20 files changed, 776 insertions, 315 deletions
diff --git a/modules-available/minilinux/hooks/bootup.inc.php b/modules-available/minilinux/hooks/bootup.inc.php new file mode 100644 index 00000000..e33aaa70 --- /dev/null +++ b/modules-available/minilinux/hooks/bootup.inc.php @@ -0,0 +1,3 @@ +<?php + +MiniLinux::updateList();
\ No newline at end of file diff --git a/modules-available/minilinux/hooks/ipxe-bootentry.inc.php b/modules-available/minilinux/hooks/ipxe-bootentry.inc.php index 944cdfa3..b044ce5d 100644 --- a/modules-available/minilinux/hooks/ipxe-bootentry.inc.php +++ b/modules-available/minilinux/hooks/ipxe-bootentry.inc.php @@ -1,145 +1,3 @@ <?php -class LinuxBootEntryHook extends BootEntryHook -{ - - public function name() - { - return Dictionary::translateFileModule('minilinux', 'module', 'module_name', true); - } - - public function extraFields() - { - /* For translate module: - * Dictionary::translate('ipxe-kcl-extra'); - * Dictionary::translate('ipxe-debug'); - */ - return [ - new HookExtraField('kcl-extra', 'string', ''), - new HookExtraField('debug', 'bool', false), - ]; - } - - /** - * @return HookEntryGroup[] - */ - protected function groupsInternal() - { - /* - * Dictionary::translate('default_boot_entry'); - * Dictionary::translate('not_installed_hint'); - */ - $array = []; - $array[] = new HookEntryGroup($this->name(), [ - new HookEntry('default', - Dictionary::translateFileModule('minilinux', 'module', 'default_boot_entry', true), - MiniLinux::updateCurrentBootSetting()) - ]); - $branches = Database::queryAll('SELECT sourceid, branchid, title FROM minilinux_branch ORDER BY title'); - $versions = MiniLinux::queryAllVersionsByBranch(); - // Group by branch for detailed listing - foreach ($branches as $branch) { - if (isset($versions[$branch['branchid']])) { - $group = []; - foreach ($versions[$branch['branchid']] as $version) { - $valid = $version['installed'] != 0; - $title = $version['versionid'] . ' ' . $version['title']; - if (!$valid) { - $title .= ' ' . Dictionary::translateFileModule('minilinux', 'module', 'not_installed_hint'); - } - $group[] = new HookEntry($version['versionid'], $title, $valid); - } - $array[] = new HookEntryGroup($branch['title'] ? $branch['title'] : $branch['branchid'], $group); - } - } - return $array; - } - - /** - * @param $id - * @return BootEntry the actual boot entry instance for given entry, false if invalid id - */ - public function getBootEntryInternal($localData) - { - $id = $localData['id']; - if ($id === 'default') { // Special case - $effectiveId = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE); - } else { - $effectiveId = $id; - } - $res = Database::queryFirst('SELECT installed, data FROM minilinux_version WHERE versionid = :id', ['id' => $effectiveId]); - if ($res === false) { - return BootEntry::newCustomBootEntry(['script' => 'prompt Invalid minilinux boot entry id: ' . $id]); - } - if ($res['installed'] == 0) { - return BootEntry::newCustomBootEntry(['script' => 'prompt Selected version not currently installed on server: ' . $effectiveId]); - } - $remoteData = json_decode($res['data'], true); - $bios = $efi = false; - if (!@is_array($remoteData['agnostic']) && !@is_array($remoteData['efi']) && !@is_array($remoteData['bios'])) { - $remoteData['agnostic'] = []; // We got nothing at all so fake this entry, resulting in a generic default entry - } - if (@is_array($remoteData['agnostic'])) { - $bios = $this->generateExecData($effectiveId, $remoteData['agnostic'], $localData); - $arch = BootEntry::AGNOSTIC; - } else { - if (@is_array($remoteData['efi'])) { - $efi = $this->generateExecData($effectiveId, $remoteData['efi'], $localData); - } - if (@is_array($remoteData['bios'])) { - $bios = $this->generateExecData($effectiveId, $remoteData['bios'], $localData); - } - if ($bios && $efi) { - $arch = BootEntry::BOTH; - } elseif ($bios) { - $arch = BootEntry::BIOS; - } else { - $arch = BootEntry::EFI; - } - } - return BootEntry::newStandardBootEntry($bios, $efi, $arch); - } - - private function generateExecData($effectiveId, $remoteData, $localData) - { - $exec = new ExecData(); - // Defaults - $root = '/boot/' . $effectiveId . '/'; - $exec->executable = 'kernel'; - $exec->initRd = ['initramfs-stage31']; - $exec->imageFree = true; - $exec->commandLine = 'slxbase=boot/%ID% slxsrv=${serverip} quiet splash ${ipappend1} ${ipappend2}'; - // Overrides - foreach (['executable', 'commandLine', 'initRd', 'imageFree'] as $key) { - if (isset($remoteData[$key])) { - $exec->{$key} = $remoteData[$key]; - } - } - // KCL hacks - if (isset($localData['debug']) && $localData['debug']) { - $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, - isset($remoteData['debugCommandLineModifier']) - ? $remoteData['debugCommandLineModifier'] - : '-vga -quiet -splash -loglevel loglevel=7' - ); - } - if (isset($localData['kcl-extra'])) { - $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, $localData['kcl-extra']); - } - $exec->commandLine = str_replace('%ID%', $effectiveId, $exec->commandLine); - $exec->executable = $root . $exec->executable; - foreach ($exec->initRd as &$rd) { - $rd = $root . $rd; - } - unset($rd); - return $exec; - } - - public function isValidId($id) - { - $res = Database::queryFirst('SELECT installed FROM minilinux_version WHERE versionid = :id', ['id' => $id]); - return $res !== false && $res['installed']; - } -} - return new LinuxBootEntryHook();
\ No newline at end of file diff --git a/modules-available/minilinux/hooks/main-warning.inc.php b/modules-available/minilinux/hooks/main-warning.inc.php index 8b052471..1a22468a 100644 --- a/modules-available/minilinux/hooks/main-warning.inc.php +++ b/modules-available/minilinux/hooks/main-warning.inc.php @@ -1,6 +1,6 @@ <?php -if (!is_dir(CONFIG_HTTP_DIR . '/bwlp/default')) { +if (!is_dir(CONFIG_HTTP_DIR . '/default')) { Message::addError('minilinux.please-download-minilinux', true); $needSetup = true; } else { diff --git a/modules-available/minilinux/inc/linuxbootentryhook.inc.php b/modules-available/minilinux/inc/linuxbootentryhook.inc.php new file mode 100644 index 00000000..1424b6b9 --- /dev/null +++ b/modules-available/minilinux/inc/linuxbootentryhook.inc.php @@ -0,0 +1,193 @@ +<?php + +/** + * Class LinuxBootEntryHook. + * Only to be used in the ipxe-bootentry hook, as this depends on + * the existence of BootEntryHook, a class from serversetup-bwlp-ipxe. + * This module is usually not activated when interacting with the + * minilinux module. + */ +class LinuxBootEntryHook extends BootEntryHook +{ + + public function name(): string + { + return Dictionary::translateFileModule('minilinux', 'module', 'module_name'); + } + + public function extraFields(): array + { + /* For translate module: + * Dictionary::translate('ipxe-kcl-extra'); + * Dictionary::translate('ipxe-debug'); + * Dictionary::translate('ipxe-insecure-cpu'); + * Dictionary::translate('ipxe-force-init-dhcp'); + */ + return [ + new HookExtraField('kcl-extra', 'string', ''), + new HookExtraField('debug', 'bool', false), + new HookExtraField('insecure-cpu', 'bool', false), + new HookExtraField('force-init-dhcp', 'bool', false), + ]; + } + + /** + * @return HookEntryGroup[] + */ + protected function groupsInternal(): array + { + /* + * Dictionary::translate('default_boot_entry'); + * Dictionary::translate('not_installed_hint'); + * Dictionary::translate('latest_of_branch'); + */ + $array = []; + $array[] = new HookEntryGroup($this->name(), [ + new HookEntry('default', + Dictionary::translateFileModule('minilinux', 'module', 'default_boot_entry'), + MiniLinux::updateCurrentBootSetting()) + ]); + $branches = Database::queryAll('SELECT sourceid, branchid, title FROM minilinux_branch ORDER BY title'); + $versions = MiniLinux::queryAllVersionsByBranch(); + // Group by branch for detailed listing + foreach ($branches as $branch) { + if (isset($versions[$branch['branchid']])) { + $group = [ + new HookEntry($branch['branchid'], + $branch['branchid'] . ' ' + . Dictionary::translateFileModule('minilinux', 'module', + 'latest_of_branch'), + true), + ]; + foreach ($versions[$branch['branchid']] as $version) { + $valid = $version['installed'] != MiniLinux::INSTALL_MISSING; + $title = $version['versionid'] . ' ' . $version['title']; + if (!$valid) { + $title .= ' ' . Dictionary::translateFileModule('minilinux', 'module', + 'not_installed_hint'); + } + $group[] = new HookEntry($version['versionid'], $title, $valid); + } + $array[] = new HookEntryGroup($branch['title'] ?: $branch['branchid'], $group); + } + } + return $array; + } + + /** + * @return ?BootEntry the actual boot entry instance for given entry, false if invalid id + */ + public function getBootEntryInternal(array $localData): ?BootEntry + { + $id = $localData['id']; + if ($id === 'default') { // Special case + $effectiveId = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE); + } else { + $effectiveId = $id; + } + $res = Database::queryFirst('SELECT versionid, installed, data FROM minilinux_version WHERE versionid = :id', + ['id' => $effectiveId]); + if ($res === false) { + // Maybe this is a branchid, which means latest from according branch (installed only) + $res = Database::queryFirst('SELECT versionid, installed, data FROM minilinux_version + WHERE branchid = :id AND installed = :ok + ORDER BY dateline DESC LIMIT 1', + ['id' => $effectiveId, 'ok' => MiniLinux::INSTALL_OK]); + } + if ($res === false) { + return BootEntry::newCustomBootEntry(['script' => 'prompt Selected version not currently installed on server: ' . $effectiveId]); + } + $effectiveId = $res['versionid']; // In case we selected from a branchid, so above message doesn't show versionid + $remoteData = json_decode($res['data'], true); + $bios = $efi = false; + if (!@is_array($remoteData['agnostic']) && !@is_array($remoteData['efi']) && !@is_array($remoteData['bios'])) { + $remoteData['agnostic'] = []; // We got nothing at all so fake this entry, resulting in a generic default entry + } + if (@is_array($remoteData['agnostic'])) { + $bios = $this->generateExecData($effectiveId, $remoteData['agnostic'], $localData); + $arch = BootEntry::AGNOSTIC; + } else { + if (@is_array($remoteData['efi'])) { + $efi = $this->generateExecData($effectiveId, $remoteData['efi'], $localData); + } + if (@is_array($remoteData['bios'])) { + $bios = $this->generateExecData($effectiveId, $remoteData['bios'], $localData); + } + if ($bios && $efi) { + $arch = BootEntry::BOTH; + } elseif ($bios) { + $arch = BootEntry::BIOS; + } else { + $arch = BootEntry::EFI; + } + } + return BootEntry::newStandardBootEntry($bios, $efi, $arch, 'ml-' . $id); + } + + private function generateExecData($effectiveId, $remoteData, $localData): ExecData + { + $exec = new ExecData(); + // Defaults + $root = '/boot/' . $effectiveId . '/'; + $exec->executable = 'kernel'; + $exec->initRd = ['initramfs-stage31']; + $exec->imageFree = true; + $exec->commandLine = 'slxbase=boot/%ID% slxsrv=${serverip} quiet splash ${ipappend1} ${ipappend2}' + . ' ipv4.ip=${ip} ipv4.router=${gateway} ipv4.dns=${dns} ipv4.hostname=${hostname} ipv4.domain=${domain} ipv4.search=${dnssl}' + . ' ipv4.if=${mac} ipv4.ntpsrv=${ntpsrv} ipv4.subnet=${netmask}'; + // Overrides + foreach (['executable', 'commandLine', 'initRd', 'imageFree'] as $key) { + if (isset($remoteData[$key])) { + $exec->{$key} = $remoteData[$key]; + } + } + // KCL hacks + if (!empty($localData['debug'])) { + // Debug boot enabled + $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, + $remoteData['debugCommandLineModifier'] ?? '-vga -quiet -splash -loglevel loglevel=7' + ); + } + // disable all CPU sidechannel attack mitigations etc. + if (!empty($localData['insecure-cpu'])) { + $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 i915.mitigations=off'); + } + // force that we + if (!empty($localData['force-init-dhcp'])) { + $exec->commandLine = IPxe::modifyCommandLine($exec->commandLine, + '-ipv4.router -ipv4.dns -ipv4.subnet'); + } + // 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']); + } + $exec->commandLine = str_replace('%ID%', $effectiveId, $exec->commandLine); + $exec->executable = $root . $exec->executable; + foreach ($exec->initRd as &$rd) { + if ($rd[0] !== '/') { + $rd = $root . $rd; + } + } + unset($rd); + return $exec; + } + + public function isValidId(string $id): bool + { + if ($id === 'default') + return true; // Meta-version that links to whatever the default is set to + $res = Database::queryFirst('SELECT installed FROM minilinux_version WHERE versionid = :id', ['id' => $id]); + if ($res !== false && $res['installed'] != MiniLinux::INSTALL_MISSING) + return true; + $res = Database::queryFirst('SELECT branchid FROM minilinux_branch WHERE branchid = :id', ['id' => $id]); + return $res !== false; + } +} diff --git a/modules-available/minilinux/inc/minilinux.inc.php b/modules-available/minilinux/inc/minilinux.inc.php index 54536096..cbc797f2 100644 --- a/modules-available/minilinux/inc/minilinux.inc.php +++ b/modules-available/minilinux/inc/minilinux.inc.php @@ -11,22 +11,27 @@ class MiniLinux const INVALID = 'invalid'; + const INSTALL_MISSING = 0; + + const INSTALL_OK = 1; + + const INSTALL_BROKEN = 2; + /* * Update of available versions by querying sources */ /** - * Query all known sources for meta data + * Query all known sources for metadata * @return int number of sources query was just initialized for */ - public static function updateList() + public static function updateList(): int { $stamp = time(); $last = Property::get(self::PROPERTY_KEY_FETCHTIME); - error_log('Last: ' . $last); - if ($last !== false && $last + 10 > $stamp) + if ($last !== false && $last + 3 > $stamp) return 0; // In progress... - Property::set(self::PROPERTY_KEY_FETCHTIME, $stamp, 1); + Property::set(self::PROPERTY_KEY_FETCHTIME, $stamp, 10); Database::exec('LOCK TABLES callback WRITE, minilinux_source WRITE, minilinux_branch WRITE, minilinux_version WRITE'); Database::exec('UPDATE minilinux_source SET taskid = UUID()'); @@ -34,7 +39,8 @@ class MiniLinux Database::exec("UPDATE minilinux_version INNER JOIN minilinux_branch USING (branchid) INNER JOIN minilinux_source USING (sourceid) - SET orphan = orphan + 1 WHERE minilinux_source.lastupdate < $cutoff"); + SET orphan = orphan + 1 + WHERE minilinux_source.lastupdate < $cutoff AND orphan < 100"); $list = Database::queryAll('SELECT sourceid, url, taskid FROM minilinux_source'); foreach ($list as $source) { Taskmanager::submit('DownloadText', array( @@ -49,20 +55,22 @@ class MiniLinux /** * Called when downloading metadata from a specific update source is finished - * @param mixed $task task structure + * + * @param array $task task structure * @param string $sourceid see minilinux_source table */ - public static function listDownloadCallback($task, $sourceid) + public static function listDownloadCallback(array $task, string $sourceid): void { - if ($task['statusCode'] !== 'TASK_FINISHED') + if (!Taskmanager::isFinished($task)) return; $taskId = $task['id']; - $data = json_decode($task['data']['content'], true); - if (!is_array($data)) { - EventLog::warning('Cannot download Linux version meta data for ' . $sourceid); + $data = json_decode($task['data']['content'] ?? '', true); + if (!is_array($data) || empty($data['systems'])) { + EventLog::warning('Cannot download Linux version meta data for ' . $sourceid, + ($task['data']['error'] ?? '') . "\n\nContent:\n" . ($task['data']['content'] ?? '')); $lastupdate = 'lastupdate'; } else { - if (@is_array($data['systems'])) { + if (is_array($data['systems'])) { self::addBranches($sourceid, $data['systems']); } $lastupdate = 'UNIX_TIMESTAMP()'; @@ -71,11 +79,10 @@ class MiniLinux WHERE sourceid = :sourceid AND taskid = :taskid", ['sourceid' => $sourceid, 'taskid' => $taskId]); // Clean up -- delete orphaned versions that are not installed - $orphaned = Database::queryColumnArray('SELECT versionid FROM minilinux_version WHERE orphan > 4 AND installed = 0'); - if (!empty($orphaned)) { - Database::exec('DELETE FROM minilinux_version WHERE versionid IN (:list)', ['list' => $orphaned]); - } - Database::exec('DELETE FROM minilinux_branch', [], true); + Database::exec('DELETE FROM minilinux_version WHERE orphan > 4 AND installed = :missing', + ['missing' => self::INSTALL_MISSING]); + // FKC makes sure we only delete orphaned ones + Database::exec('DELETE IGNORE FROM minilinux_branch WHERE 1', [], true); } private static function addBranches($sourceid, $systems) @@ -84,18 +91,31 @@ class MiniLinux if (!self::isValidIdPart($system['id'])) continue; $branchid = $sourceid . '/' . $system['id']; - $title = empty($system['title']) ? $branchid : $system['title']; - $description = empty($system['description']) ? '' : $system['description']; - Database::exec('INSERT INTO minilinux_branch (branchid, sourceid, title, description) - VALUES (:branchid, :sourceid, :title, :description) - ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description)', [ - 'branchid' => $branchid, - 'sourceid' => $sourceid, - 'title' => $title, - 'description' => $description, - ]); - if (@is_array($system['versions'])) { + $title = mb_substr(empty($system['title']) ? $branchid : $system['title'], 0, 150); + $description = $system['description'] ?? ''; + $color = $system['color'] ?? ''; + if (!empty($system['versions']) && is_array($system['versions'])) { + Database::exec('INSERT INTO minilinux_branch (branchid, sourceid, title, color, description) + VALUES (:branchid, :sourceid, :title, :color, :description) + ON DUPLICATE KEY UPDATE title = VALUES(title), color = VALUES(color), description = VALUES(description)', [ + 'branchid' => $branchid, + 'sourceid' => $sourceid, + 'title' => $title, + 'color' => $color, + 'description' => $description, + ]); self::addVersions($branchid, $system['versions']); + } else { + // Empty branch - only update metadata if branch exists locally + Database::exec('UPDATE minilinux_branch + SET title = :title, color = :color, description = :description + WHERE sourceid = :sourceid AND branchid = :branchid', [ + 'branchid' => $branchid, + 'sourceid' => $sourceid, + 'title' => $title, + 'color' => $color, + 'description' => $description, + ]); } } } @@ -118,7 +138,8 @@ class MiniLinux return; } $versionid = $branchid . '/' . $version['version']; - $title = empty($version['title']) ? '' : $version['title']; + $title = $version['title'] ?? ''; + $description = $version['description'] ?? ''; $dateline = empty($version['releasedate']) ? time() : (int)$version['releasedate']; unset($version['version'], $version['title'], $version['releasedate']); // Sanitize files array @@ -151,18 +172,20 @@ class MiniLinux $version['files'] = array_values($version['files']); } $data = json_encode($version); - Database::exec('INSERT INTO minilinux_version (versionid, branchid, title, dateline, data, orphan) - VALUES (:versionid, :branchid, :title, :dateline, :data, 0) - ON DUPLICATE KEY UPDATE title = VALUES(title), data = VALUES(data), orphan = 0', [ + Database::exec('INSERT INTO minilinux_version (versionid, branchid, title, description, dateline, data, orphan) + VALUES (:versionid, :branchid, :title, :description, :dateline, :data, 0) + ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description), + dateline = VALUES(dateline), data = VALUES(data), orphan = 0', [ 'versionid' => $versionid, 'branchid' => $branchid, - 'title' => $title, + 'title' => mb_substr($title, 0, 150), + 'description' => $description, 'dateline' => $dateline, 'data' => $data, ]); } - private static function isValidIdPart($str) + private static function isValidIdPart(string $str): bool { return preg_match('/^[a-z0-9_\-]+$/', $str) > 0; } @@ -171,10 +194,10 @@ class MiniLinux * Download of specific version */ - public static function validateDownloadTask($versionid, $taskid) + public static function validateDownloadTask(string $versionid, ?string $taskid): ?string { if ($taskid === null) - return false; + return null; $task = Taskmanager::status($taskid); if (Taskmanager::isTask($task) && !Taskmanager::isFailed($task) && (is_dir(CONFIG_HTTP_DIR . '/' . $versionid) || !Taskmanager::isFinished($task))) @@ -182,15 +205,13 @@ class MiniLinux Database::exec('UPDATE minilinux_version SET taskid = NULL WHERE versionid = :versionid AND taskid = :taskid', ['versionid' => $versionid, 'taskid' => $taskid]); - return false; + return null; } /** * Download the files for the given version id - * @param $versionid - * @return bool */ - public static function downloadVersion($versionid) + public static function downloadVersion(string $versionid): ?string { $ver = Database::queryFirst('SELECT s.url, s.pubkey, v.versionid, v.taskid, v.data FROM minilinux_version v INNER JOIN minilinux_branch b USING (branchid) @@ -198,17 +219,17 @@ class MiniLinux WHERE versionid = :versionid', ['versionid' => $versionid]); if ($ver === false) - return false; + return null; $taskid = self::validateDownloadTask($versionid, $ver['taskid']); - if ($taskid !== false) + if ($taskid !== null) return $taskid; $data = json_decode($ver['data'], true); if (!is_array($data)) { EventLog::warning("Cannot download Linux '$versionid': Corrupted meta data.", $ver['data']); - return false; + return null; } if (empty($data['files'])) - return false; + return null; $list = []; $legacyDir = preg_replace(',^[^/]*/,', '', $versionid); foreach ($data['files'] as $file) { @@ -227,6 +248,7 @@ class MiniLinux Database::exec('LOCK TABLES minilinux_version WRITE'); $aff = Database::exec('UPDATE minilinux_version SET taskid = :taskid WHERE versionid = :versionid AND taskid IS NULL', ['taskid' => $uuid, 'versionid' => $versionid]); + $task = false; if ($aff > 0) { $task = Taskmanager::submit('DownloadFiles', [ 'id' => $uuid, @@ -237,22 +259,22 @@ class MiniLinux if (Taskmanager::isFailed($task)) { $task = false; } else { - $task = $task['id']; + $task = (string)$task['id']; } - } else { - $task = false; } Database::exec('UNLOCK TABLES'); if ($task !== false) { // Callback for db column TaskmanagerCallback::addCallback($task, 'mlGotLinux', $versionid); + self::checkStage4($data); } + // Race - someone else wrote a taskid to DB, just call self again to get that one if ($aff === 0) return self::downloadVersion($versionid); return $task; } - public static function fileToId($versionid, $fileName) + public static function fileToId(string $versionid, string $fileName): string { return 'x' . substr(md5($fileName . $versionid), 0, 8); } @@ -262,10 +284,10 @@ class MiniLinux */ /** - * Geenrate messages regarding setup und update availability. + * Generate messages regarding setup und update availability. * @return bool true if severe problems were found, false otherwise */ - public static function generateUpdateNotice() + public static function generateUpdateNotice(): bool { // Messages in here are with module name, as required by the // main-warning hook. @@ -308,7 +330,7 @@ class MiniLinux * actually installed locally. * @return bool true if installed locally, false otherwise */ - public static function updateCurrentBootSetting() + public static function updateCurrentBootSetting(): bool { $default = Property::get(self::PROPERTY_DEFAULT_BOOT); if ($default === false) @@ -321,7 +343,7 @@ class MiniLinux } elseif ($slashes === 1) { // Latest from branch $ver = Database::queryFirst('SELECT versionid, installed FROM minilinux_version - WHERE branchid = :branchid AND installed = 1 ORDER BY dateline DESC', ['branchid' => $default]); + WHERE branchid = :branchid AND installed = :ok ORDER BY dateline DESC', ['branchid' => $default, 'ok' => self::INSTALL_OK]); } else { // Unknown return false; @@ -332,33 +354,218 @@ class MiniLinux return false; } Property::set(self::PROPERTY_DEFAULT_BOOT_EFFECTIVE, $ver['versionid']); - return $ver['installed'] != 0; + return $ver['installed'] != self::INSTALL_MISSING; } public static function linuxDownloadCallback($task, $versionid) { - self::setInstalledState($versionid, $task['statusCode'] === 'TASK_FINISHED'); + self::setInstalledState($versionid, $task['statusCode'] === 'TASK_FINISHED' ? self::INSTALL_OK : self::INSTALL_BROKEN); } - public static function setInstalledState($versionid, $installed) + public static function setInstalledState($versionid, int $installed): void { - settype($installed, 'int'); - error_log("Setting $versionid to $installed"); Database::exec('UPDATE minilinux_version SET installed = :installed WHERE versionid = :versionid', [ 'versionid' => $versionid, 'installed' => $installed, ]); + if ($installed === self::INSTALL_OK) { + $res = Database::queryFirst('SELECT Count(*) AS cnt FROM minilinux_version WHERE installed = :ok', + ['ok' => self::INSTALL_OK]); + if ($res['cnt'] == 1) { + self::setDefaultVersion($versionid); + } + } } - public static function queryAllVersionsByBranch() + public static function queryAllVersionsByBranch(): array { $list = []; - $res = Database::simpleQuery('SELECT branchid, versionid, title, dateline, orphan, taskid, installed + $res = Database::simpleQuery('SELECT branchid, versionid, title, Length(description) AS desclen, + dateline, orphan, taskid, installed FROM minilinux_version ORDER BY branchid, dateline, versionid'); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $list[$row['branchid']][$row['versionid']] = $row; } return $list; } -}
\ No newline at end of file + public static function setDefaultVersion($versionId) + { + Property::set(MiniLinux::PROPERTY_DEFAULT_BOOT, $versionId); + self::updateCurrentBootSetting(); + // Legacy PXELINUX boot menu (TODO: Remove this when we get rid of PXELINUX support) + $task = Taskmanager::submit('Symlink', [ + 'target' => $versionId, + 'linkname' => CONFIG_HTTP_DIR . '/default', + ]); + if ($task !== false) { + Taskmanager::release($task); + } + } + + /** + * Check whether an optionally required stage4 is available. + * Return true if there is no stage4, otherwise check filesystem, + * or try to request from local dnbd3-server. + * + * @param array $data decoded data column from minilinux_version + * @param string[] $errors in array of error messages if not available + * @return bool true if stage4 is available or none required + */ + public static function checkStage4(array $data, &$errors = false): bool + { + $errors = []; + $image = false; + $rid = 0; + foreach (['agnostic', 'efi', 'bios'] as $type) { + if (!isset($data[$type]) || !isset($data[$type]['commandLine'])) + continue; + if (!preg_match('/\bslx\.stage4\.path=(\S+)/', $data[$type]['commandLine'], $out)) + continue; + $image = $out[1]; + if (preg_match('/\bslx\.stage4\.rid=(\d+)/', $data[$type]['commandLine'], $out)) { + $rid = (int)$out[1]; + } + break; + } + if ($image === false) + return true; // No stage4 + if ($rid === 0) { + // Get latest local revision + foreach (glob(CONFIG_VMSTORE_DIR . '/' . $image . '.r*', GLOB_NOSORT) as $file) { + if (preg_match('/\.r(\d+)$/', $file, $out)) { + $cmp = (int)$out[1]; + if ($cmp > $rid) { + $rid = $cmp; + } + } + } + } + if ($rid > 0 && file_exists(CONFIG_VMSTORE_DIR . '/' . $image . '.r' . $rid) + && !file_exists(CONFIG_VMSTORE_DIR . '/' . $image . '.r' . $rid . '.map')) { + // Accept if image exists locally and no map file (map file would mean incomplete) + return true; + } + // Not found locally -- try to replicate + $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + if ($sock === false) { + $errors[] = 'Error creatring socket to connect to dnbd3-server'; + return false; + } + socket_set_option($sock, SOL_SOCKET, SO_SNDTIMEO, array('sec' => 1, 'usec' => 0)); + socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 5, 'usec' => 0)); + if (@socket_connect($sock, '127.0.0.1', 5003) === false) { + $errors[] = 'Could not connect to local dnbd3-server'; + socket_close($sock); + return false; + } + // proto-version(16), image name\0, rid(16), flags(8) + $payload = pack('vA*xvC', 3, $image, $rid, 0); + // magic(16), cmd(16), payload-len(32), offset(64), handle(64) XXX 32Bit compat + $packet = pack('A*vVVVVV', 'sr', 2, strlen($payload), 0, 0, 1234, 0) . $payload; + if (!socket_send($sock, $packet, strlen($packet), 0)) { + $errors[] = 'Cannot send request to dnbd3-server'; + socket_close($sock); + return false; + } + $len = socket_recv($sock, $reply, 16, MSG_WAITALL); + if ($len === 0) { + $errors[] = 'Local dnbd3-server cannot replicate required stage4 from master-server'; + socket_close($sock); + return false; + } + if ($len !== 16) { + $errors[] = 'Incomplete reply received from local dnbd3-server. Stage4 might not replicate!'; + socket_close($sock); + return false; + } + socket_close($sock); + // Try to decode header + $reply = unpack('A2magic/vcmd/Vsize/Vhandlelow/Vhandlehigh', $reply); + if ($reply['magic'] !== 'sr') { + $errors[] = 'Reply has wrong magic'; + } + if ($reply['cmd'] !== 2) { + $errors[] = 'Reply is not CMD_IMAGE_REPLY'; + } + return empty($errors); + } + + /** + * Determine by which menus/locations each MiniLinux version is being used. + */ + public static function getBootMenuUsage(): array + { + if (!Module::isAvailable('serversetup') || !class_exists('BootEntryHook')) + return []; + $res = Database::simpleQuery("SELECT be.entryid, be.data, + GROUP_CONCAT(DISTINCT me.menuid) AS menus, + GROUP_CONCAT(DISTINCT ml.locationid) AS locations + FROM serversetup_bootentry be + LEFT JOIN serversetup_menuentry me USING (entryid) + LEFT JOIN serversetup_menu_location ml USING (menuid) + WHERE module = 'minilinux' + GROUP BY be.data"); + $return = []; + $usedMenuIds = []; + foreach ($res as $row) { + $data = json_decode($row['data'], true); + if (!isset($data['id'])) + continue; + $id = self::resolveEntryId($data['id']); + $new = [ + 'entryids' => [$row['entryid']], + 'menus' => explode(',', $row['menus'] ?? ''), + 'locations' => explode(',', $row['locations'] ?? ''), + ]; + $usedMenuIds = array_merge($usedMenuIds, $new['menus']); + if (isset($return[$id])) { + $return[$id] = array_merge_recursive($return[$id], $new); + } else { + $return[$id] = $new; + } + } + // Build id => title map for menus + $res = Database::simpleQuery("SELECT menuid, title FROM serversetup_menu m + WHERE menuid IN (:menuid)", ['menuid' => array_unique($usedMenuIds)]); + $menus = []; + foreach ($res as $row) { + $menus[$row['menuid']] = $row['title']; + } + // Build output array + foreach ($return as &$item) { + $item['locations'] = array_map(function ($i) { + return ['locationid' => $i, 'locationname' => Location::getName($i)]; + }, array_unique(array_filter($item['locations'], 'is_numeric'))); + $item['menus'] = array_map(function ($i) use ($menus) { + return ['menuid' => $i, 'menuname' => $menus[$i]]; + }, array_unique(array_filter($item['menus'], 'is_numeric'))); + $item['locationCount'] = count($item['locations']); + $item['menuCount'] = count($item['menus']); + $item['entryCount'] = count($item['entryids']); + } + return $return; + } + + /** + * Take a configured versionid from a bootentry (serversetup module) and translate + * it, in case it's "default" or just a branch name. + */ + private static function resolveEntryId(string $id): string + { + if ($id === 'default') { // Special case + $id = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE); + } + if (substr_count($id, '/') < 2) { + // Maybe this is a branchid, which means latest from according branch (installed only) + $res = Database::queryFirst('SELECT versionid FROM minilinux_version WHERE branchid = :id AND installed = :ok + ORDER BY dateline DESC LIMIT 1', + ['id' => $id, 'ok' => self::INSTALL_OK]); + if ($res !== false) { + $id = $res['versionid']; + } + } + return $id; + } + +} diff --git a/modules-available/minilinux/install.inc.php b/modules-available/minilinux/install.inc.php index b859671a..7ef82d74 100644 --- a/modules-available/minilinux/install.inc.php +++ b/modules-available/minilinux/install.inc.php @@ -2,9 +2,9 @@ $result[] = tableCreate('minilinux_source', " `sourceid` varchar(8) CHARACTER SET ascii NOT NULL, - `title` varchar(100) NOT NULL, + `title` varchar(150) NOT NULL, `url` varchar(200) NOT NULL, - `lastupdate` int(10) UNSIGNED NOT NULL, + `lastupdate` int(10) UNSIGNED NOT NULL DEFAULT '0', `taskid` char(36) CHARACTER SET ascii DEFAULT NULL, `pubkey` blob NOT NULL, PRIMARY KEY (`sourceid`), @@ -13,7 +13,8 @@ $result[] = tableCreate('minilinux_source', " $result[] = tableCreate('minilinux_branch', " `sourceid` varchar(8) CHARACTER SET ascii DEFAULT NULL, `branchid` varchar(40) CHARACTER SET ascii NOT NULL, - `title` varchar(100) NOT NULL, + `title` varchar(150) NOT NULL, + `color` varchar(7) NOT NULL, `description` blob NOT NULL, PRIMARY KEY (`branchid`), KEY (`title`) @@ -21,7 +22,8 @@ $result[] = tableCreate('minilinux_branch', " $result[] = tableCreate('minilinux_version', " `branchid` varchar(40) CHARACTER SET ascii NOT NULL, `versionid` varchar(72) CHARACTER SET ascii NOT NULL, - `title` varchar(100) NOT NULL, + `title` varchar(150) NOT NULL, + `description` blob NOT NULL, `dateline` int(10) UNSIGNED NOT NULL, `data` blob NOT NULL, `orphan` tinyint(3) UNSIGNED NOT NULL, @@ -39,4 +41,28 @@ $result[] = tableAddConstraint('minilinux_version', 'branchid', 'minilinux_branc $result[] = tableAddConstraint('minilinux_branch', 'sourceid', 'minilinux_source', 'sourceid', 'ON UPDATE CASCADE ON DELETE SET NULL'); +// 2022-10-17: Add color to branch, description to version +if (!tableHasColumn('minilinux_branch', 'color')) { + if (Database::exec("ALTER TABLE `minilinux_branch` ADD COLUMN `color` varchar(7) NOT NULL DEFAULT '' AFTER `title`") !== false) { + $result[] = UPDATE_DONE; + } else { + finalResponse(UPDATE_FAILED, Database::lastError()); + } +} +if (!tableHasColumn('minilinux_version', 'description')) { + // BLOB/TEXT cannot have non-NULL default on older MariaDB + if (Database::exec("ALTER TABLE `minilinux_version` ADD COLUMN `description` blob NULL DEFAULT NULL AFTER `title`") !== false) { + $result[] = UPDATE_DONE; + } else { + finalResponse(UPDATE_FAILED, Database::lastError()); + } +} + +// 2023-07-17: Make title columns larger +foreach (['minilinux_source', 'minilinux_branch', 'minilinux_version'] as $table) { + if (stripos(tableColumnType($table, 'title'), 'varchar(150)') === false) { + Database::exec("ALTER TABLE `$table` MODIFY `title` varchar(150) NOT NULL"); + } +} + responseFromArray($result); diff --git a/modules-available/minilinux/lang/de/messages.json b/modules-available/minilinux/lang/de/messages.json index e957ee09..c32fa20d 100644 --- a/modules-available/minilinux/lang/de/messages.json +++ b/modules-available/minilinux/lang/de/messages.json @@ -1,10 +1,10 @@ { - "default-is-invalid": "Gew\u00e4hltes Linux-Standardsystem ist ung\u00fcltig", - "default-not-installed": "Gew\u00e4hltes Linux-Standardsystem {{0}} ist nicht (mehr) installiert", - "default-update-available": "F\u00fcr das Gew\u00e4hlte Linux-Standardsystem {{0}} ist die Aktualisierung {{1}} verf\u00fcgbar", + "default-is-invalid": "Gew\u00e4hltes Netboot-Grundsystem ist ung\u00fcltig", + "default-not-installed": "Gew\u00e4hltes Netboot-Grundsystem {{0}} ist nicht (mehr) installiert", + "default-update-available": "F\u00fcr das Gew\u00e4hlte Netboot-Grundsystem {{0}} ist die Aktualisierung {{1}} verf\u00fcgbar", "delete-error": "Fehler beim L\u00f6schen der Version {{0}}: {{1}}", - "no-default-set": "Kein Linux-Standardsystem festgelegt", + "no-default-set": "Kein Netboot-Grundsystem als Standard festgelegt", "no-such-version": "Ung\u00fcltige\/Unbekannte Version: {{0}}", - "please-download-minilinux": "Wichtige Dateien der MiniLinux-Installation fehlen", + "please-download-minilinux": "Wichtige Dateien der Netboot-Grundsysteminstallation fehlen", "version-deleted": "Version {{0}} wurde gel\u00f6scht" -}
\ No newline at end of file +} diff --git a/modules-available/minilinux/lang/de/module.json b/modules-available/minilinux/lang/de/module.json index 687b4a71..f47249d5 100644 --- a/modules-available/minilinux/lang/de/module.json +++ b/modules-available/minilinux/lang/de/module.json @@ -6,8 +6,13 @@ "file-ok": "OK", "file-size-mismatch": "Dateigr\u00f6\u00dfe stimmt nicht", "ipxe-debug": "Debug-Ausgaben statt Bootlogo", + "ipxe-force-init-dhcp": "Erzwinge erneuten DHCP-Request nach Laden des initramfs", + "ipxe-insecure-cpu": "Alle Mitigations f\u00fcr CPU-Sicherheitsl\u00fccken deaktivieren", "ipxe-kcl-extra": "Modifikation der Kernel-Command-Line", - "module_name": "Netboot Grundsystem", + "latest_of_branch": "(Neueste lokal vorhandene Version)", + "menu-sources": "Update-Quellen", + "menu-versions": "Verf\u00fcgbare Versionen", + "module_name": "Netboot-Grundsystem", "not_installed_hint": "(nicht installiert)", - "page_title": "Linuxvarianten f\u00fcr Netboot verwalten" -}
\ No newline at end of file + "page_title": "Netboot-Grundsystemverwaltung" +} diff --git a/modules-available/minilinux/lang/de/permissions.json b/modules-available/minilinux/lang/de/permissions.json index 29012620..4773611a 100644 --- a/modules-available/minilinux/lang/de/permissions.json +++ b/modules-available/minilinux/lang/de/permissions.json @@ -1,4 +1,5 @@ { - "view": "Zeige Komponenten des Minilinux. Wird nicht benötigt, wenn Nutzer eine der anderen Rechte hat.", - "update": "Aktualisieren von Komponenten des Minilinux." + "delete": "Ein heruntergeladenes Netboot-Grundsystem l\u00f6schen.", + "update": "Aktualisieren von Komponenten des Netboot-Grundsystems.", + "view": "Zeige Komponenten des Netboot-Grundsystems. Wird nicht ben\u00f6tigt, wenn Nutzer eine der anderen Rechte hat." }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/de/template-tags.json b/modules-available/minilinux/lang/de/template-tags.json index 2054896c..894b864b 100644 --- a/modules-available/minilinux/lang/de/template-tags.json +++ b/modules-available/minilinux/lang/de/template-tags.json @@ -2,21 +2,29 @@ "lang_branchesHeading": "Verf\u00fcgbare Varianten und Versionen", "lang_changelog": "Changelog", "lang_confirmDeleteVersion": "Diese Version wirklich l\u00f6schen?", + "lang_default": "Standard", "lang_download": "Herunterladen", "lang_id": "ID", "lang_installed": "Installiert", - "lang_introText": "Hier gibts MiniLinux.", + "lang_isGlobalDefault": "Ist globaler Standard", "lang_key": "GPG-Key", "lang_lastUpdate": "Zuletzt \u00fcberpr\u00fcft", - "lang_minilinuxHeading": "Netboot Linux verwalten", - "lang_orphanedVersion": "Verwaiste Version", + "lang_locations": "R\u00e4ume \/ Orte", + "lang_maybeMissingStage4": "Stage 4 m\u00f6glicherweise nicht verf\u00fcgbar", + "lang_menuEntries": "Men\u00fceintr\u00e4ge", + "lang_menus": "Men\u00fcs", + "lang_minilinuxHeading": "Netboot-Grundsystem verwalten", + "lang_orphanedVersion": "Verwaist", + "lang_orphanedVersionToolTip": "Diese Version wird vom Update-Server nicht mehr angeboten", "lang_releaseDate": "Ver\u00f6ffentlichungsdatum", "lang_selectedDefaultIs": "Gew\u00e4hltes Standardsystem ist", + "lang_setGlobalDefault": "Als globalen Standard festlegen", "lang_sources": "Quellen", "lang_sourcesIntro": "Liste der Quellen, aus denen Updates bezogen werden k\u00f6nnen.", "lang_title": "Titel", "lang_updateSourcesButton": "Nach neuen Updates suchen", "lang_url": "URL", + "lang_usedBy": "Verwendet", "lang_verify": "Integrit\u00e4t \u00fcberpr\u00fcfen", "lang_verifyToolTip": "Dateiintegrit\u00e4t anhand von Pr\u00fcfsummen verifizieren", "lang_version": "Version" diff --git a/modules-available/minilinux/lang/en/messages.json b/modules-available/minilinux/lang/en/messages.json index 6dc736a4..193b18fa 100644 --- a/modules-available/minilinux/lang/en/messages.json +++ b/modules-available/minilinux/lang/en/messages.json @@ -1,6 +1,10 @@ { + "default-is-invalid": "Currently selected default is invalid", + "default-not-installed": "Currently selected default of {{0}} is not locally installed (any more).", + "default-update-available": "You selected default system {{0}} can be updated to {{1}}", "delete-error": "Error deleting version {{0}}: {{1}}", + "no-default-set": "No default system selected", "no-such-version": "No such version: {{0}}", - "please-download-minilinux": "Important files from the mini Linux installation are missing.", + "please-download-minilinux": "Important files from the netboot Linux installation are missing.", "version-deleted": "Deleted version {{0}}" }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/en/module.json b/modules-available/minilinux/lang/en/module.json index b1526869..ff5c7a49 100644 --- a/modules-available/minilinux/lang/en/module.json +++ b/modules-available/minilinux/lang/en/module.json @@ -1,9 +1,18 @@ { + "default_boot_entry": "(Use global default)", "file-checksum-bad": "Bad checksum", "file-missing": "File missing", "file-not-readable": "File not readable", "file-ok": "OK", "file-size-mismatch": "File size mismatch", - "module_name": "Minilinux", + "ipxe-debug": "Print debug messages instead of showing splash screen", + "ipxe-force-init-dhcp": "Force another DHCP request after loading initramfs", + "ipxe-insecure-cpu": "Disable all mitigations for CPU security flaws", + "ipxe-kcl-extra": "Modifications to the kernel command line", + "latest_of_branch": "(Latest locally available version)", + "menu-sources": "Sources for updates", + "menu-versions": "Available versions", + "module_name": "Net-boot OS", + "not_installed_hint": "(not installed)", "page_title": "Manage Netboot Linux flavors" }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/en/permissions.json b/modules-available/minilinux/lang/en/permissions.json index b8389e62..9d97ad00 100644 --- a/modules-available/minilinux/lang/en/permissions.json +++ b/modules-available/minilinux/lang/en/permissions.json @@ -1,4 +1,5 @@ { - "view": "Show list of minilinux components. Not needed if User has any of the other permissions.", - "update": "Update minilinux components." + "delete": "Delete a downloaded netboot Linux version.", + "update": "Update netboot Linux components.", + "view": "Show list of netboot Linux components. Not needed if user has any of the other permissions." }
\ No newline at end of file diff --git a/modules-available/minilinux/lang/en/template-tags.json b/modules-available/minilinux/lang/en/template-tags.json index 48ba0c15..5b3c77e4 100644 --- a/modules-available/minilinux/lang/en/template-tags.json +++ b/modules-available/minilinux/lang/en/template-tags.json @@ -1,15 +1,31 @@ { - "lang_canUpdate1": "At least one component of", - "lang_canUpdate2": "Can be updated. For a smooth operation, it is recommended to keep all components up to date.", - "lang_configurationPackageNotFound": "Configuration package not found!", - "lang_desiredVersion": "Desired version", - "lang_errorGetting": "Error while downloading list!", - "lang_filesInVersion": "Files for version", - "lang_listObtained": "Downloading list...", - "lang_outdated": "Outdated", - "lang_redownload": "Download again", - "lang_systemUpdated": "The system is up to date.", - "lang_update": "Update", - "lang_updateAll": "Update all modules", - "lang_uptodate": "Up to date" + "lang_branchesHeading": "Available branches and versions", + "lang_changelog": "Change log", + "lang_confirmDeleteVersion": "Do you want to delete this version?", + "lang_default": "Default", + "lang_download": "Download", + "lang_id": "ID", + "lang_installed": "Installed", + "lang_isGlobalDefault": "Global default", + "lang_key": "GPG key", + "lang_lastUpdate": "Last updated", + "lang_locations": "Rooms \/ Locations", + "lang_maybeMissingStage4": "Stage 4 might be missing", + "lang_menuEntries": "Menu entries", + "lang_menus": "Menus", + "lang_minilinuxHeading": "Manage netboot base system", + "lang_orphanedVersion": "Orphaned", + "lang_orphanedVersionToolTip": "This version is not offered by the update server any more", + "lang_releaseDate": "Release date", + "lang_selectedDefaultIs": "Current default is", + "lang_setGlobalDefault": "Set as global default", + "lang_sources": "Sources", + "lang_sourcesIntro": "List of update sources that will be checked for available branches and versions.", + "lang_title": "Title", + "lang_updateSourcesButton": "Check for new updates", + "lang_url": "URL", + "lang_usedBy": "Used", + "lang_verify": "Check file integrity", + "lang_verifyToolTip": "Check all files against known checksums", + "lang_version": "Version" }
\ No newline at end of file diff --git a/modules-available/minilinux/page.inc.php b/modules-available/minilinux/page.inc.php index 7c7e3d36..8004f1ab 100644 --- a/modules-available/minilinux/page.inc.php +++ b/modules-available/minilinux/page.inc.php @@ -25,42 +25,75 @@ class Page_MiniLinux extends Page } User::assertPermission('view'); + Dashboard::addSubmenu('?do=minilinux', Dictionary::translate('menu-versions')); + Dashboard::addSubmenu('?do=minilinux&show=sources', Dictionary::translate('menu-sources')); } protected function doRender() { - Render::addTemplate('page-minilinux', ['default' => Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT)]); + $show = Request::get('show', 'list', 'string'); + if ($show === 'list') { + // List branches and versions + $branches = Database::queryAll('SELECT sourceid, branchid, title, color, description FROM minilinux_branch ORDER BY title ASC'); + $versions = MiniLinux::queryAllVersionsByBranch(); + $usage = MiniLinux::getBootMenuUsage(); + $sourceList = []; + // Group by branch for detailed listing, add usage info + foreach ($branches as &$branch) { + // Little hack: We abuse the title for ordering, so if the second char is a space, assume the first one + // is just for sort order and remove it. + if ($branch['title'][1] === ' ') { + $branch['title'] = substr($branch['title'], 2); + } + $bid = 'div-' . str_replace('/', '-', $branch['branchid']); + if (!isset($sourceList[$branch['sourceid']])) { + $sourceList[$branch['sourceid']] = ['sourceid' => $branch['sourceid'], 'list' => []]; + } + $sourceList[$branch['sourceid']]['list'][] = [ + 'title' => $branch['title'], + 'color' => $branch['color'], + 'bid' => $bid + ]; + $branch['bid'] = $bid; + if (isset($versions[$branch['branchid']])) { + $branch['versionlist'] = $this->renderVersionList($versions[$branch['branchid']], $usage); + } + } + unset($branch); + $sourceList = array_values($sourceList); + } elseif ($show === 'sources') { + // List sources + $res = Database::simpleQuery('SELECT sourceid, title, url, lastupdate, pubkey FROM minilinux_source ORDER BY title, sourceid'); + $sourceViewData = ['list' => [], 'show_refresh' => true]; + $tooOld = strtotime('-7 days'); + $showRefresh = strtotime('-5 minutes'); + foreach ($res as $row) { + $row['lastupdate_s'] = Util::prettyTime($row['lastupdate']); + if ($row['lastupdate'] != 0 && $row['lastupdate'] < $tooOld) { + $row['update_class'] = 'text-danger'; + } + if ($row['lastupdate'] > $showRefresh) { + $sourceViewData['show_refresh'] = false; + } + $sourceViewData['list'][] = $row; + } + } + // Output + Render::addTemplate('page-minilinux', [ + 'default' => Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT), + 'sources' => $sourceList ?? null, + ]); // Warning if (!MiniLinux::updateCurrentBootSetting()) { Message::addError('default-not-installed', Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT)); } - // List branches and versions - $branches = Database::queryAll('SELECT sourceid, branchid, title FROM minilinux_branch ORDER BY title ASC'); - $versions = MiniLinux::queryAllVersionsByBranch(); - // Group by branch for detailed listing - foreach ($branches as &$branch) { - if (isset($versions[$branch['branchid']])) { - $branch['versionlist'] = $this->renderVersionList($versions[$branch['branchid']]); - } - } - unset($branch); - Render::addTemplate('branches', ['branches' => $branches]); - // List sources - $res = Database::simpleQuery('SELECT sourceid, title, url, lastupdate, pubkey FROM minilinux_source ORDER BY title, sourceid'); - $data = ['list' => [], 'show_refresh' => true]; - $tooOld = strtotime('-7 days'); - $showRefresh = strtotime('-10 minutes'); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $row['lastupdate_s'] = Util::prettyTime($row['lastupdate']); - if ($row['lastupdate'] != 0 && $row['lastupdate'] < $tooOld) { - $row['update_class'] = 'text-danger'; - } - if ($row['lastupdate'] > $showRefresh) { - $data['show_refresh'] = false; - } - $data['list'][] = $row; + if (isset($branches)) { + Render::addTemplate('branches', ['branches' => $branches]); + } elseif (isset($sourceViewData)) { + Render::addTemplate('sources', $sourceViewData); + } else { + Message::addError('main.invalid-action', $show); } - Render::addTemplate('sources', $data); } protected function doAjax() @@ -74,23 +107,26 @@ class Page_MiniLinux extends Page } } - private function renderVersionList($versions) + private function renderVersionList(array $versions, array $usage): string { $def = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT); - $eff = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE); + //$eff = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE); foreach ($versions as &$version) { $version['dateline_s'] = Util::prettyTime($version['dateline']); - $version['orphan'] = ($version['orphan'] > 2); + $version['orphan'] = ($version['orphan'] > 0 && $version['installed'] == MiniLinux::INSTALL_MISSING) || ($version['orphan'] > 1); $version['downloading'] = $version['taskid'] && Taskmanager::isRunning(Taskmanager::status($version['taskid'])); - if ($version['installed'] && $version['versionid'] !== $def) { + if ($version['installed'] != MiniLinux::INSTALL_MISSING && $version['versionid'] !== $def) { $version['showsetdefault'] = true; } if ($version['versionid'] === $def) { $version['isdefault'] = true; - if (!$version['installed']) { + if (!$version['installed'] != MiniLinux::INSTALL_OK) { $version['default_class'] = 'bg-danger'; } } + if (isset($usage[$version['versionid']])) { + $version['usage'] = $usage[$version['versionid']]; + } } return Render::parse('versionlist', ['versions' => array_values($versions)]); } @@ -103,7 +139,8 @@ class Page_MiniLinux extends Page if ($versionid === false) { die('What!'); } - $ver = Database::queryFirst('SELECT versionid, taskid, data, installed FROM minilinux_version WHERE versionid = :versionid', + $ver = Database::queryFirst('SELECT versionid, description, taskid, data, installed + FROM minilinux_version WHERE versionid = :versionid', ['versionid' => $versionid]); if ($ver === false) { die('No such version'); @@ -115,7 +152,7 @@ class Page_MiniLinux extends Page } $data['versionid'] = $versionid; $data['dltask'] = MiniLinux::validateDownloadTask($versionid, $ver['taskid']); - $data['verify_button'] = !$verify && $data['dltask'] === false; + $data['verify_button'] = !$verify && $data['dltask'] === null; if (is_array($data['files'])) { $valid = true; $sort = []; @@ -139,7 +176,7 @@ class Page_MiniLinux extends Page if (isset($file['mtime'])) { $file['mtime_s'] = Util::prettyTime($file['mtime']); } - if ($data['dltask']) { + if ($data['dltask'] !== null) { $file['fileid'] = MiniLinux::fileToId($versionid, $file['name']); } } @@ -147,14 +184,17 @@ class Page_MiniLinux extends Page array_multisort($sort, SORT_ASC, $data['files']); if (!$valid) { $data['verify_button'] = false; - $data['download_button'] = !$data['dltask']; - if ($ver['installed']) { - MiniLinux::setInstalledState($versionid, false); + if ($ver['installed'] != MiniLinux::INSTALL_MISSING) { + MiniLinux::setInstalledState($versionid, MiniLinux::INSTALL_BROKEN); } - } elseif (!$ver['installed'] && $verify) { - MiniLinux::setInstalledState($versionid, true); + } elseif ($ver['installed'] != MiniLinux::INSTALL_OK && $verify) { + MiniLinux::setInstalledState($versionid, MiniLinux::INSTALL_OK); } } + if ($data['dltask'] !== null || $ver['installed'] != MiniLinux::INSTALL_MISSING) { + MiniLinux::checkStage4($data, $data['s4_errors']); + } + $data['changelog'] = Util::markup($ver['description'] ?? ''); echo Render::parse('filelist', $data); } @@ -164,7 +204,7 @@ class Page_MiniLinux extends Page const FILE_CHECKSUM_BAD = 3; const FILE_NOT_READABLE = 4; - private function getFileState($versionid, $file, $verify) + private function getFileState(string $versionid, array $file, bool $verify): int { $path = CONFIG_HTTP_DIR . '/' . $versionid . '/' . $file['name']; if (!is_file($path)) @@ -191,15 +231,15 @@ class Page_MiniLinux extends Page { switch ($state) { case self::FILE_CHECKSUM_BAD: - return Dictionary::translate('file-checksum-bad', true); + return Dictionary::translate('file-checksum-bad'); case self::FILE_SIZE_MISMATCH: - return Dictionary::translate('file-size-mismatch', true); + return Dictionary::translate('file-size-mismatch'); case self::FILE_MISSING: - return Dictionary::translate('file-missing', true); + return Dictionary::translate('file-missing'); case self::FILE_NOT_READABLE: - return Dictionary::translate('file-not-readable', true); + return Dictionary::translate('file-not-readable'); case self::FILE_OK: - return Dictionary::translate('file-ok', true); + return Dictionary::translate('file-ok'); } return '???'; } @@ -212,7 +252,7 @@ class Page_MiniLinux extends Page die('No version'); } $task = MiniLinux::downloadVersion($version); - if ($task === false) { + if ($task === null) { Message::addError('no-such-version', $version); Message::renderList(); } else { @@ -234,7 +274,6 @@ class Page_MiniLinux extends Page Message::addError('no-such-version'); return; } - MiniLinux::setInstalledState($version['versionid'], false); $path = CONFIG_HTTP_DIR . '/' . $version['versionid']; $task = Taskmanager::submit('DeleteDirectory', [ 'path' => $path, @@ -243,8 +282,10 @@ class Page_MiniLinux extends Page if ($task !== false) { $task = Taskmanager::waitComplete($task, 2500); if (Taskmanager::isFailed($task)) { + MiniLinux::setInstalledState($version['versionid'], MiniLinux::INSTALL_BROKEN); Message::addError('delete-error', $versionid, $task['data']['error']); } else { + MiniLinux::setInstalledState($version['versionid'], MiniLinux::INSTALL_MISSING); Message::addSuccess('version-deleted', $versionid); } } @@ -252,15 +293,20 @@ class Page_MiniLinux extends Page private function updateSources() { + User::assertPermission('view'); // As it doesn't really change anything, accept view permission $ret = MiniLinux::updateList(); if ($ret > 0) { - sleep(2); - Trigger::checkCallbacks(); + for ($i = 0; $i < 6; ++$i) { + sleep(1); + if (!Trigger::checkCallbacks()) + break; + } } } private function setDefault() { + User::assertPermission('update'); $versionid = Request::post('version', false, 'string'); if ($versionid === false) { Message::addError('main.parameter-missing', 'versionid'); @@ -272,7 +318,7 @@ class Page_MiniLinux extends Page Message::addError('no-such-version'); return; } - Property::set(MiniLinux::PROPERTY_DEFAULT_BOOT, $version['versionid']); + MiniLinux::setDefaultVersion($version['versionid']); } } diff --git a/modules-available/minilinux/templates/branches.html b/modules-available/minilinux/templates/branches.html index 5f3c4e50..372321e2 100644 --- a/modules-available/minilinux/templates/branches.html +++ b/modules-available/minilinux/templates/branches.html @@ -1,23 +1,48 @@ <h3>{{lang_branchesHeading}}</h3> +<div class="clearfix"></div> + <div id="ibm-mainframe"> {{#branches}} - <div class="panel panel-default"> + <a id="a-{{bid}}"></a> + <div class="panel panel-default" {{#color}}style="background:linear-gradient(90deg, {{color}} 0%, {{color}} 4px, rgba(255,255,255,0) 4px)"{{/color}}> <div class="panel-heading"> - <div class="pull-right"> - {{sourceid}} {{branchid}} + <div class="pull-right slx-pointer" data-toggle="collapse" data-target="#{{bid}}"> + {{sourceid}} {{branchid}} <b class="caret"></b> </div> <b>{{title}}</b> </div> + <div class="collapse in branch-item" id="{{bid}}"> <div class="panel-body"> {{description}} </div> {{{versionlist}}} + </div> </div> {{/branches}} </div> <script> document.addEventListener('DOMContentLoaded', function () { + // Remember collapsed state + var c = localStorage.getItem('ml-collapse'); + if (c) { + c = JSON.parse(c); + for (var e in c) { + if (c.hasOwnProperty(e)) { + $('#' + e).collapse('hide'); + } + } + } else { + c = {}; + } + $('.branch-item').on('hide.bs.collapse', function() { + c[this.id] = true; + localStorage.setItem('ml-collapse', JSON.stringify(c)); + }).on('show.bs.collapse', function() { + delete c[this.id]; + localStorage.setItem('ml-collapse', JSON.stringify(c)); + }); + // Button magic var addHandlers = function(parent) { parent.find('.btn-verify').click(function() { loadDetails($(this).data('version'), { show: "version", verify: 1 }); @@ -63,9 +88,16 @@ }); var taskDone = {}; var wasUnfinished = false; + var errors = {}; function dlTmCb(task) { if (!task.data || !task.data.files) return; + if (task.data.error) { + if (errors[task.id] !== task.data.error) { + errors[task.id] = task.data.error; + $('#error-' + task.id).text(errors[task.id]).show(); + } + } for (var i = 0; i < task.data.files.length; ++i) { var f = task.data.files[i]; var id = task.id + f.id; diff --git a/modules-available/minilinux/templates/filelist.html b/modules-available/minilinux/templates/filelist.html index 9aa175bd..241d1264 100644 --- a/modules-available/minilinux/templates/filelist.html +++ b/modules-available/minilinux/templates/filelist.html @@ -8,12 +8,6 @@ {{lang_verify}} </button> {{/verify_button}} - {{#download_button}} - <button type="button" class="btn btn-xs btn-success btn-download" data-version="{{versionid}}"> - <span class="glyphicon glyphicon-download"></span> - {{lang_download}} - </button> - {{/download_button}} {{#delete_button}} <button type="submit" name="show" value="delete" class="btn btn-xs btn-danger" data-confirm="{{lang_confirmDeleteVersion}}"> @@ -47,11 +41,18 @@ </tr> {{/files}} </table> +{{#s4_errors}} +<div class="alert alert-warning">{{lang_maybeMissingStage4}}: {{.}}</div> +{{/s4_errors}} {{#dltask}} <div class="hidden" data-tm-id="{{dltask}}" data-tm-callback="dlTmCb"></div> +<pre class="collapse" id="error-{{dltask}}"></pre> {{/dltask}} {{#changelog}} -<h4>{{lang_changelog}}</h4> -{{changelog}} + <div class="slx-space"></div> +<div style="border:1px solid #bbb;padding:4px;border-radius: 3px"> + <h4>{{lang_changelog}}</h4> + {{{changelog}}} +</div> {{/changelog}} <div class="slx-space"></div>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/page-minilinux.html b/modules-available/minilinux/templates/page-minilinux.html index 3059e827..c66de597 100644 --- a/modules-available/minilinux/templates/page-minilinux.html +++ b/modules-available/minilinux/templates/page-minilinux.html @@ -1,5 +1,18 @@ -<h1>{{lang_minilinuxHeading}}</h1> +{{#sources}} +<div class="panel panel-default pull-right" style="margin:2px"> + <table class="table table-condensed"> + <tr style="background:#eee"> + <th>{{sourceid}}</th> + </tr> + {{#list}} + <tr {{#color}}style="background:linear-gradient(90deg, {{color}} 0%, {{color}} 4px, rgba(255,255,255,0) 4px)"{{/color}}> + <td><a href="#a-{{bid}}">{{title}}</a></td> + </tr> + {{/list}} + </table> +</div> +{{/sources}} -<p>{{lang_introText}}</p> +<h1>{{lang_minilinuxHeading}}</h1> {{lang_selectedDefaultIs}}: <b>{{default}}</b>
\ No newline at end of file diff --git a/modules-available/minilinux/templates/sources.html b/modules-available/minilinux/templates/sources.html index dabc7f4d..50ad7c6f 100644 --- a/modules-available/minilinux/templates/sources.html +++ b/modules-available/minilinux/templates/sources.html @@ -20,10 +20,10 @@ <td class="small">{{url}}</td> <td class="{{update_class}}">{{lastupdate_s}}</td> <td class="text-center"> - <button type="button" class="btn btn-default btn-xs" data-confirm="#confirm-{{source}}" data-close="{{lang_close}}"> + <button type="button" class="btn btn-default btn-xs" data-confirm="#confirm-{{sourceid}}" data-close="{{lang_close}}"> <span class="glyphicon glyphicon-eye-open"></span> </button> - <pre id="confirm-{{source}}" class="hidden">{{pubkey}}</pre> + <pre id="confirm-{{sourceid}}" class="hidden">{{pubkey}}</pre> </td> </tr> {{/list}} diff --git a/modules-available/minilinux/templates/versionlist.html b/modules-available/minilinux/templates/versionlist.html index 4ef4e631..e66960b2 100644 --- a/modules-available/minilinux/templates/versionlist.html +++ b/modules-available/minilinux/templates/versionlist.html @@ -3,9 +3,10 @@ <th class="slx-smallcol">{{lang_version}}</th> <th class="slx-smallcol">{{lang_releaseDate}}</th> <th>{{lang_title}}</th> + <th class="slx-smallcol">{{lang_usedBy}}</th> <th class="slx-smallcol"></th> - <th class="slx-smallcol"></th> - <th class="slx-smallcol"></th> + <th class="slx-smallcol" style="width:100px">{{lang_default}}</th> + <th class="slx-smallcol" style="width:150px">{{lang_download}}</th> </tr> {{#versions}} <tr> @@ -15,11 +16,46 @@ <b class="caret"></b> </a> </td> - <td class="text-nowrap">{{dateline_s}}</td> + <td class="text-nowrap"> + {{#desclen}} + <div style="float:right;margin-right:-6px"> + <span class="glyphicon glyphicon-list-alt"></span> + </div> + {{/desclen}} + {{dateline_s}} + </td> <td>{{title}}</td> <td class="text-nowrap"> + {{#usage.entryids.0}} + <div class="dropdown"> + <button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown"> + {{usage.entryCount}} / {{usage.menuCount}} / {{usage.locationCount}} + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li role="separator" class="dropdown-header slx-bold">{{lang_menuEntries}}</li> + {{#usage.entryids}} + <li><a href="?do=serversetup&show=editbootentry&id={{.}}">{{.}}</a></li> + {{/usage.entryids}} + {{#usage.menus.0}} + <li role="separator" class="dropdown-header slx-bold">{{lang_menus}}</li> + {{/usage.menus.0}} + {{#usage.menus}} + <li><a href="?do=serversetup&show=editmenu&id={{menuid}}">{{menuname}}</a></li> + {{/usage.menus}} + {{#usage.locations.0}} + <li role="separator" class="dropdown-header slx-bold">{{lang_locations}}</li> + {{/usage.locations.0}} + {{#usage.locations}} + <li class="disabled"><a href="#">{{locationname}}</a></li> + {{/usage.locations}} + </ul> + </div> + {{/usage.entryids.0}} + </td> + <td class="text-nowrap"> {{#orphan}} - {{lang_orphanedVersion}} + <span class="label label-danger" title="{{lang_orphanedVersionToolTip}}">{{lang_orphanedVersion}}</span> {{/orphan}} </td> <td class="text-nowrap text-center {{default_class}}"> @@ -27,31 +63,33 @@ <form method="post" action="?do=minilinux" style="margin:0;padding:0;display:inline"> <input type="hidden" name="token" value="{{token}}"> <input type="hidden" name="version" value="{{versionid}}"> - <button type="submit" name="show" value="setdefault" class="btn btn-xs btn-info"> + <button type="submit" name="show" value="setdefault" class="btn btn-xs btn-info" title="{{lang_setGlobalDefault}}"> <span class="glyphicon glyphicon-flag"></span> </button> </form> {{/showsetdefault}} {{#isdefault}} - <span class="glyphicon glyphicon-ok"></span> + <span class="glyphicon glyphicon-ok" title="{{lang_isGlobalDefault}}"></span> {{/isdefault}} </td> - <td class="text-nowrap text-center"> + <td class="text-nowrap text-right"> {{#installed}} - <span class="btn btn-default btn-xs disabled">{{lang_installed}}</span> + <span class="label label-info">{{lang_installed}}</span> {{/installed}} {{^installed}} + {{^orphan}} {{^downloading}} <button type="button" class="btn btn-xs btn-success btn-download" data-version="{{versionid}}"> <span class="glyphicon glyphicon-download"></span> {{lang_download}} </button> {{/downloading}} + {{/orphan}} {{/installed}} </td> </tr> <tr> - <td colspan="6" class="version-container collapse" data-version="{{versionid}}"></td> + <td colspan="7" class="version-container collapse" data-version="{{versionid}}"></td> </tr> {{/versions}} </table>
\ No newline at end of file |