diff options
Diffstat (limited to 'modules-available/serversetup-bwlp-ipxe/inc')
15 files changed, 1732 insertions, 581 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php index dec70528..5812c0cd 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php @@ -12,13 +12,28 @@ abstract class BootEntry /** Supports both via distinct entry */ const BOTH = 'PCBIOS-EFI'; - public abstract function supportsMode($mode); + /** + * @var string Internal ID - set to your liking, e.g. the MiniLinux version identifier + */ + protected $internalId; + + public function __construct(string $internalId) + { + $this->internalId = $internalId; + } + + public abstract function supportsMode(string $mode): bool; - public abstract function toScript($failLabel, $mode); + public abstract function toScript(ScriptBuilderBase $builder): string; - public abstract function toArray(); + public abstract function toArray(): array; - public abstract function addFormFields(&$array); + public abstract function addFormFields(array &$array): void; + + public function internalId(): string + { + return $this->internalId; + } /* * @@ -28,15 +43,15 @@ abstract class BootEntry * Return a BootEntry instance from the serialized data. * * @param string $module module this entry belongs to, or special values .script/.exec - * @param string $jsonString serialized entry data - * @return BootEntry|null instance representing boot entry, null on error + * @param string $data serialized entry data + * @return ?BootEntry instance representing boot entry, null on error */ - public static function fromJson($module, $data) + public static function fromJson(string $module, string $data): ?BootEntry { - if ($module{0} !== '.') { + if ($module[0] !== '.') { // Hook from other module $hook = Hook::loadSingle($module, 'ipxe-bootentry'); - if ($hook === false) { + if ($hook === null) { error_log('Module ' . $module . ' doesnt have an ipxe-bootentry hook'); return null; } @@ -45,26 +60,29 @@ abstract class BootEntry return null; return $ret->getBootEntry($data); } - if (is_string($data)) { - $data = json_decode($data, true); - } + $data = json_decode($data, true); + if (!is_array($data)) + return null; if ($module === '.script') { return new CustomBootEntry($data); } if ($module === '.exec') { return new StandardBootEntry($data); } + if ($module === '.special') { + return new SpecialBootEntry($data); + } return null; } - public static function forMenu($menuId) + public static function forMenu(int $menuId): MenuBootEntry { return new MenuBootEntry($menuId); } - public static function newStandardBootEntry($initData, $efi = false, $arch = false) + public static function newStandardBootEntry($initData, $efi = false, $arch = false, string $internalId = ''): ?StandardBootEntry { - $ret = new StandardBootEntry($initData, $efi, $arch); + $ret = new StandardBootEntry($initData, $efi, $arch, $internalId); $list = []; if ($ret->arch() !== self::EFI) { $list[] = self::BIOS; @@ -82,9 +100,9 @@ abstract class BootEntry return $ret; } - public static function newCustomBootEntry($initData) + public static function newCustomBootEntry($initData): ?CustomBootEntry { - if (empty($initData['script'])) + if (!is_array($initData) || empty($initData)) return null; return new CustomBootEntry($initData); } @@ -92,15 +110,14 @@ abstract class BootEntry /** * Return a BootEntry instance from database with the given id. * - * @param string $id - * @return BootEntry|null|false false == unknown id, null = unknown entry type, BootEntry instance on success + * @return ?BootEntry null = unknown entry type, BootEntry instance on success */ - public static function fromDatabaseId($id) + public static function fromDatabaseId(string $id): ?BootEntry { $row = Database::queryFirst("SELECT module, data FROM serversetup_bootentry WHERE entryid = :id LIMIT 1", ['id' => $id]); if ($row === false) - return false; + return null; return self::fromJson($row['module'], $row['data']); } @@ -110,11 +127,11 @@ abstract class BootEntry * * @return BootEntry[] all existing BootEntries */ - public static function getAll() + public static function getAll(): array { - $res = Database::simpleQuery("SELECT entryid, data FROM serversetup_bootentry"); + $res = Database::simpleQuery("SELECT entryid, module, data FROM serversetup_bootentry"); $ret = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $tmp = self::fromJson($row['module'], $row['data']); if ($tmp === null) continue; @@ -135,13 +152,16 @@ class StandardBootEntry extends BootEntry * @var ExecData same for EFI */ protected $efi; - - protected $arch; // Constants below + /** + * @var ?string BootEntry Constants above + */ + protected $arch; const KEYS = ['executable', 'initRd', 'commandLine', 'replace', 'imageFree', 'autoUnload', 'resetConsole', 'dhcpOptions']; - public function __construct($data, $efi = false, $arch = false) + public function __construct($data, $efi = false, ?string $arch = null, string $internalId = '') { + parent::__construct($internalId); $this->pcbios = new ExecData(); $this->efi = new ExecData(); if ($data instanceof PxeSection) { @@ -214,10 +234,7 @@ class StandardBootEntry extends BootEntry } } - /** - * @param PxeSection $data - */ - private function fromPxeMenu($data) + private function fromPxeMenu(PxeSection $data): void { $bios = $this->pcbios; $bios->executable = $data->kernel; @@ -241,12 +258,12 @@ class StandardBootEntry extends BootEntry $bios->commandLine = trim(preg_replace('/\s+/', ' ', $bios->commandLine)); } - public function arch() + public function arch(): ?string { return $this->arch; } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { if ($mode === $this->arch || $this->arch === BootEntry::AGNOSTIC) return true; @@ -257,73 +274,16 @@ class StandardBootEntry extends BootEntry return false; } - public function toScript($failLabel, $mode) + public function toScript(ScriptBuilderBase $builder): string { - if (!$this->supportsMode($mode)) { - return "prompt Entry doesn't have an executable for mode $mode\n"; - } - if ($this->arch === BootEntry::AGNOSTIC || $mode == BootEntry::BIOS) { - $entry = $this->pcbios; - } else { - $entry = $this->efi; - } - $entry->sanitize(); - - $script = ''; - if ($entry->resetConsole) { - $script .= "console ||\n"; - } - if ($entry->imageFree) { - $script .= "imgfree ||\n"; - } - foreach ($entry->dhcpOptions as $opt) { - if (empty($opt['value'])) { - $val = '${}'; - } else { - if (empty($opt['hex'])) { - $val = bin2hex($opt['value']); - } else { - $val = $opt['value']; - } - preg_match_all('/[0-9a-f]{2}/', $val, $out); - $val = implode(':', $out[0]); - } - $script .= 'set net${idx}/' . $opt['opt'] . ':hex ' . $val - . ' || prompt Cannot override DHCP server option ' . $opt['opt'] . ". Press any key to continue anyways.\n"; - } - $initrds = []; - if (!empty($entry->initRd)) { - foreach (array_values($entry->initRd) as $i => $initrd) { - if (empty($initrd)) - continue; - $script .= "initrd --name initrd$i $initrd || goto $failLabel\n"; - $initrds[] = "initrd$i"; - } - } - $script .= "boot "; - if ($entry->autoUnload) { - $script .= "-a "; - } - if ($entry->replace) { - $script .= "-r "; - } - $script .= $entry->executable; - if (empty($initrds)) { - $rdBase = ''; - } else { - $rdBase = " initrd=" . implode(',', $initrds); - } - if (!empty($entry->commandLine)) { - $script .= "$rdBase {$entry->commandLine}"; - } - $script .= " || goto $failLabel\n"; - if ($entry->resetConsole) { - $script .= "goto start ||\n"; - } - return $script; + if ($this->arch === BootEntry::AGNOSTIC) // Same as below, could construct fall-through but this is more clear + return $builder->execDataToScript($this->pcbios, null, null); + return $builder->execDataToScript(null, + $this->supportsMode(BootEntry::BIOS) ? $this->pcbios : null, + $this->supportsMode(BootEntry::EFI) ? $this->efi : null); } - public function addFormFields(&$array) + public function addFormFields(array &$array): void { $array[$this->arch . '_selected'] = 'selected'; $array['entries'][] = $this->pcbios->toFormFields(BootEntry::BIOS); @@ -331,7 +291,10 @@ class StandardBootEntry extends BootEntry $array['exec_checked'] = 'checked'; } - public function toArray() + /** + * @return array{PCBIOS: array, EFI: array, arch: string} + */ + public function toArray(): array { return [ BootEntry::BIOS => $this->pcbios->toArray(), @@ -343,65 +306,118 @@ class StandardBootEntry extends BootEntry class CustomBootEntry extends BootEntry { - protected $script; + /** + * @var string iPXE + */ + protected $ipxe = ''; + + protected $bash; + + protected $grub; public function __construct($data) { - if (is_array($data) && isset($data['script'])) { - $this->script = $data['script']; + parent::__construct('custom'); + if (is_array($data)) { + $this->ipxe = $data['script'] ?? ''; // LEGACY + foreach (['bash', 'grub'] as $key) { + $this->{$key} = $data[$key] ?? ''; + } } } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { return true; } - public function toScript($failLabel, $mode) + public function toScript(ScriptBuilderBase $builder): string { - return str_replace('%fail%', $failLabel, $this->script) . "\n"; + // TODO: A (very) simple translator for oneliners like "poweroff || goto fail" maybe? + if ($builder instanceof ScriptBuilderIpxe) + return $this->ipxe; + if ($builder instanceof ScriptBuilderBash) + return $this->bash; + if ($builder instanceof ScriptBuilderGrub) + return $this->grub; + return ''; } - public function addFormFields(&$array) + public function addFormFields(array &$array): void { $array['entry'] = [ - 'script' => $this->script, + 'script' => $this->ipxe, ]; $array['script_checked'] = 'checked'; } - public function toArray() + /** + * @return array{script: string} + */ + public function toArray(): array { - return ['script' => $this->script]; + return ['script' => $this->ipxe]; } } class MenuBootEntry extends BootEntry { + /** @var int */ protected $menuId; - public function __construct($menuId) + public function __construct(int $menuId) { + parent::__construct('menu-' . $menuId); $this->menuId = $menuId; } - public function supportsMode($mode) + public function supportsMode(string $mode): bool { return true; } - public function toScript($failLabel, $mode) + public function toScript(ScriptBuilderBase $builder): string { - return 'chain -ar ${self}&menuid=' . $this->menuId . ' || goto ' . $failLabel . "\n"; + $menu = IPxeMenu::get($this->menuId, true); + return $builder->menuToScript($menu); } - public function toArray() + public function toArray(): array { return []; } - public function addFormFields(&$array) + public function addFormFields(array &$array): void { } } +class SpecialBootEntry extends BootEntry +{ + + private $type; + + public function __construct($type) + { + $this->type = $type['type'] ?? $type; + parent::__construct('special-' . $this->type); + } + + public function supportsMode(string $mode): bool + { + return true; + } + + public function toScript(ScriptBuilderBase $builder): string + { + return $builder->getSpecial($this->type); + } + + public function toArray(): array + { + return []; + } + + public function addFormFields(array &$array): void { } + +} diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php index cf180006..ab55c888 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php @@ -6,37 +6,37 @@ abstract class BootEntryHook /** * @var string -- set by ipxe, not module implementing hook */ - public $moduleId; + public $moduleId = ''; /** * @var string -- set by ipxe, not module implementing hook */ - public $checked; + public $checked = ''; - private $selectedId; + private $selectedId = ''; private $data = []; /** * @return string */ - public abstract function name(); + public abstract function name(): string; /** * @return HookExtraField[] */ - public abstract function extraFields(); + public abstract function extraFields(): array; - public abstract function isValidId($id); + public abstract function isValidId(string $id): bool; /** * @return HookEntryGroup[] */ - protected abstract function groupsInternal(); + protected abstract function groupsInternal(): array; /** * @return HookEntryGroup[] */ - public final function groups() + public final function groups(): array { $groups = $this->groupsInternal(); foreach ($groups as $group) { @@ -50,34 +50,48 @@ abstract class BootEntryHook } /** - * @param $id * @return BootEntry|null the actual boot entry instance for given entry, null if invalid id */ - public abstract function getBootEntryInternal($localData); + public abstract function getBootEntryInternal(array $localData): ?BootEntry; - public final function getBootEntry($data) + public final function getBootEntry(string $jsonString): ?BootEntry { - if (!is_array($data)) { - $data = json_decode($data, true); - } + $data = json_decode($jsonString, true); return $this->getBootEntryInternal($data); } - public function setSelected($id) + /** + * @param string $mixed either the plain ID if the entry to be marked as selected, or the JSON string representing + * the entire entry, which must have a key called 'id' that will be used as the ID then. + */ + public function setSelected(string $mixed): void { - $json = @json_decode($id, true); + $json = @json_decode($mixed, true); if (is_array($json)) { $id = $json['id']; $this->data = $json; + } else { + $id = $mixed; } $this->selectedId = $id; } - public function renderExtraFields() + /** + * @return string ID of entry that was marked as selected by setSelected() + */ + public function getSelected(): string + { + return $this->selectedId; + } + + /** + * @return HookExtraField[] + */ + public function renderExtraFields(): array { $list = $this->extraFields(); - foreach ($list as &$entry) { - $entry->currentValue = isset($this->data[$entry->name]) ? $this->data[$entry->name] : $entry->default; + foreach ($list as $entry) { + $entry->currentValue = $this->data[$entry->name] ?? $entry->default; $entry->hook = $this; } return $list; @@ -126,14 +140,7 @@ class HookEntry */ public $selected; - /** - * HookEntry constructor. - * - * @param string $id - * @param string $name - * @param bool $valid - */ - public function __construct($id, $name, $valid) + public function __construct(string $id, string $name, bool $valid) { $this->id = $id; $this->name = $name; @@ -164,7 +171,7 @@ class HookExtraField */ public $hook; - public function __construct($name, $type, $default) + public function __construct(string $name, string $type, $default) { $this->name = $name; $this->type = $type; @@ -185,10 +192,10 @@ class HookExtraField return $val; } - public function html() + public function html(): string { $fieldId = 'extra-' . $this->hook->moduleId . '-' . $this->name; - $fieldText = htmlspecialchars(Dictionary::translateFileModule($this->hook->moduleId, 'module', 'ipxe-' . $this->name, true)); + $fieldText = htmlspecialchars(Dictionary::translateFileModule($this->hook->moduleId, 'module', 'ipxe-' . $this->name)); if (is_array($this->type)) { $out = '<label for="' . $fieldId . '">' . $fieldText . '</label><select class="form-control" name="' . $fieldId . '" id="' . $fieldId . '">'; foreach ($this->type as $entry) { diff --git a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php index 97f98b94..e4f7a1d7 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php @@ -108,7 +108,10 @@ class ExecData $this->dhcpOptions = array_values($this->dhcpOptions); } - public function toArray() + /** + * @return array{executable: string, initRd: string[], commandLine: string, imageFree: bool, replace: bool, autoUnload: bool, resetConsole: bool, dhcpOptions: array} + */ + public function toArray(): array { $this->sanitize(); return [ @@ -123,7 +126,7 @@ class ExecData ]; } - public function toFormFields($arch) + public function toFormFields(string $arch): array { $this->sanitize(); $opts = []; @@ -159,4 +162,4 @@ class ExecData ]; } -}
\ No newline at end of file +} diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php index 4c2a7678..5e0531ab 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php @@ -12,13 +12,13 @@ class IPxe * Import all IP-Range based pxe menus from the given directory. * * @param string $configPath The pxelinux.cfg path where to look for menu files in hexadecimal IP format. - * @return Number of menus imported + * @return int Number of menus imported */ - public static function importSubnetPxeMenus($configPath) + public static function importSubnetPxeMenus(string $configPath): int { $res = Database::simpleQuery('SELECT menuid, entryid FROM serversetup_menuentry ORDER BY sortval ASC'); $menus = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { if (!isset($menus[$row['menuid']])) { $menus[(int)$row['menuid']] = []; } @@ -40,7 +40,7 @@ class IPxe WHERE startaddr >= :start AND endaddr <= :end", compact('start', 'end')); $locations = []; // Iterate over result, eliminate those that are dominated by others - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { foreach ($locations as &$loc) { if ($row['startaddr'] <= $loc['startaddr'] && $row['endaddr'] >= $loc['endaddr']) { $loc = false; @@ -52,6 +52,10 @@ class IPxe $locations[] = $row; } $menu = PxeLinux::parsePxeLinux($content, true); + if ($menu === null) { + error_log("Skipping empty pxelinux menu file $file"); + continue; + } // Insert all entries first, so we can get the list of entry IDs $entries = []; self::importPxeMenuEntries($menu, $entries); @@ -83,10 +87,10 @@ class IPxe } else { error_log('Imported menu ' . $menu->title . ' is NEW, using for ' . count($locations) . ' locations.'); // Insert new menu - $menuId = self::insertMenu($menu, 'Auto Imported', false, 0, [], []); - if ($menuId === false) + $menuId = self::insertMenu($menu, 'Auto Imported', null, 0, [], []); + if ($menuId === null) continue; - $menus[(int)$menuId] = $entries; + $menus[$menuId] = $entries; $importCount++; } foreach ($locations as $loc) { @@ -103,20 +107,20 @@ class IPxe return $importCount; } - public static function importLegacyMenu($force = false) + public static function importLegacyMenu(bool $force = false): bool { // See if anything is there if (!$force && false !== Database::queryFirst("SELECT menuentryid FROM serversetup_menuentry LIMIT 1")) return false; // Already exists // Now create the default entry self::createDefaultEntries(); - $prepend = ['bwlp-default' => false, 'localboot' => false]; + $prepend = ['bwlp-default' => null, 'localboot' => null]; $defaultLabel = 'bwlp-default'; $menuTitle = 'bwLehrpool Bootauswahl'; $pxeConfig = ''; $timeoutSecs = 60; - // Try to import any customization - $oldMenu = Property::getBootMenu(); + // Try to import any customization of the legacy PXELinux menu (despite the property name hinting at iPXE) + $oldMenu = json_decode(Property::get('ipxe-menu'), true); if (is_array($oldMenu)) { // if (isset($oldMenu['timeout'])) { @@ -136,47 +140,45 @@ class IPxe } } $append = [ - '', - 'bwlp-default-dbg' => false, - '', - 'poweroff' => false, + new PxeSection(null), + 'bwlp-default-dbg' => null, + new PxeSection(null), + 'poweroff' => null, ]; - return self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append); + self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append); + return !empty($pxeConfig); } /** - * @param PxeMenu $pxeMenu - * @param string $menuTitle - * @param string|false $defaultLabel Fallback for the default label, if PxeMenu doesn't set one + * @param ?string $defaultLabel Fallback for the default label, if PxeMenu doesn't set one * @param int $defaultTimeoutSeconds Default timeout, if PxeMenu doesn't set one - * @param array $prepend - * @param array $append - * @return int|false + * @param (?PxeSection)[] $prepend + * @param (?PxeSection)[] $append + * @return ?int ID of newly created menu, or null on error, e.g. if the menu is empty */ - public static function insertMenu($pxeMenu, $menuTitle, $defaultLabel, $defaultTimeoutSeconds, $prepend, $append) + public static function insertMenu(?PxeMenu $pxeMenu, string $menuTitle, ?string $defaultLabel, int $defaultTimeoutSeconds, + array $prepend, array $append): ?int { $timeoutMs = []; $menuEntries = $prepend; - settype($menuEntries, 'array'); - if (!empty($pxeMenu)) { - $pxe =& $pxeMenu; - if (!empty($pxe->title)) { - $menuTitle = $pxe->title; + if ($pxeMenu !== null) { + if (!empty($pxeMenu->title)) { + $menuTitle = $pxeMenu->title; } - if ($pxe->timeoutLabel !== null && $pxe->hasLabel($pxe->timeoutLabel)) { - $defaultLabel = $pxe->timeoutLabel; - } elseif ($pxe->hasLabel($pxe->default)) { - $defaultLabel = $pxe->default; + if ($pxeMenu->timeoutLabel !== null && $pxeMenu->hasLabel($pxeMenu->timeoutLabel)) { + $defaultLabel = $pxeMenu->timeoutLabel; + } elseif ($pxeMenu->hasLabel($pxeMenu->default)) { + $defaultLabel = $pxeMenu->default; } - $timeoutMs[] = $pxe->timeoutMs; - $timeoutMs[] = $pxe->totalTimeoutMs; - self::importPxeMenuEntries($pxe, $menuEntries); + $timeoutMs[] = $pxeMenu->timeoutMs; + $timeoutMs[] = $pxeMenu->totalTimeoutMs; + self::importPxeMenuEntries($pxeMenu, $menuEntries); } - if (is_array($append)) { + if (!empty($append)) { $menuEntries += $append; } if (empty($menuEntries)) - return false; + return null; // Make menu $timeoutMs = array_filter($timeoutMs, function($x) { return is_int($x) && $x > 0; }); if (empty($timeoutMs)) { @@ -195,25 +197,29 @@ class IPxe // Figure out entryid for default label // Fiddly diddly way of getting the mangled entryid for the wanted pxe menu label $defaultEntryId = false; + $fallbackDefault = false; foreach ($menuEntries as $entryId => $section) { - if ($section instanceof PxeSection) { - if ($section->isDefault) { - $defaultEntryId = $entryId; - break; - } - if ($section->label === $defaultLabel) { - $defaultEntryId = $entryId; - } + if ($section === null) + continue; + if ($section->isDefault) { + $defaultEntryId = $entryId; + break; + } + if ($section->label === $defaultLabel) { + $defaultEntryId = $entryId; + } + if ($fallbackDefault === false && !empty($entryId)) { + $fallbackDefault = $entryId; } } if ($defaultEntryId === false) { - $defaultEntryId = array_keys($menuEntries)[0]; + $defaultEntryId = $fallbackDefault; } // Link boot entries to menu $defaultMenuEntryId = null; $order = 1000; foreach ($menuEntries as $entryId => $entry) { - if (is_string($entry)) { + if ($entry !== null && $entry->isTextOnly()) { // Gap entry Database::exec("INSERT INTO serversetup_menuentry (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) @@ -221,7 +227,7 @@ class IPxe 'menuid' => $menuId, 'entryid' => null, 'hotkey' => '', - 'title' => self::sanitizeIpxeString($entry), + 'title' => self::sanitizeIpxeString($entry->title), 'hidden' => 0, 'sortval' => $order += 100, ]); @@ -232,7 +238,7 @@ class IPxe continue; $data['pass'] = ''; $data['hidden'] = 0; - if ($entry instanceof PxeSection) { + if ($entry !== null) { $data['hidden'] = (int)$entry->isHidden; // Prefer explicit data from this imported menu over the defaults $title = self::sanitizeIpxeString($entry->title); @@ -243,7 +249,7 @@ class IPxe $data['hotkey'] = $entry->hotkey; } if (!empty($entry->passwd)) { - // Most likely it's a hash so we cannot recover; ask people to reset + // Most likely it's a hash, so we cannot recover; ask people to reset $data['pass'] ='please_reset'; } } @@ -268,36 +274,28 @@ class IPxe /** * Import only the bootentries from the given PXELinux menu - * @param PxeMenu $pxe - * @param array $menuEntries Where to append the generated menu items to + * + * @param PxeSection[] $menuEntries Where to append the generated menu items to */ - public static function importPxeMenuEntries($pxe, &$menuEntries) + public static function importPxeMenuEntries(PxeMenu $pxe, array &$menuEntries): void { if (self::$allEntries === false) { self::$allEntries = BootEntry::getAll(); } foreach ($pxe->sections as $section) { - if ($section->localBoot !== false || preg_match('/chain\.c32$/i', $section->kernel)) { + if ($section->isLocalboot()) { $menuEntries['localboot'] = $section; continue; } - if ($section->label === null) { - if (!$section->isHidden && !empty($section->title)) { - $menuEntries[] = $section->title; - } - continue; - } - if (empty($section->kernel)) { - if (!$section->isHidden && !empty($section->title)) { - $menuEntries[] = $section->title; - } + if ($section->isTextOnly()) { + $menuEntries[] = $section; continue; } $label = self::cleanLabelFixLocal($section); $entry = self::pxe2BootEntry($section); if ($entry === null) continue; // Error? Ignore - if ($label !== false || ($label = array_search($entry, self::$allEntries))) { + if ($label !== false || ($label = array_search($entry, self::$allEntries)) !== false) { // Exact Duplicate, Do Nothing error_log('Ignoring duplicate boot entry ' . $section->label . ' (' . $section->kernel . ')'); } else { @@ -318,9 +316,10 @@ class IPxe if (empty($title)) { $title = $label; } - Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) - VALUES (:label, :hotkey, :title, 0, :data)', [ + Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, module, hotkey, title, builtin, data) + VALUES (:label, :module, :hotkey, :title, 0, :data)', [ 'label' => $label, + 'module' => ($entry instanceof StandardBootEntry) ? '.exec' : '.script', 'hotkey' => $hotkey, 'title' => $title, 'data' => json_encode($data), @@ -332,52 +331,43 @@ class IPxe public static function createDefaultEntries() { - Database::exec( 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) - VALUES (:entryid, :hotkey, :title, 1, :data) ON DUPLICATE KEY UPDATE data = VALUES(data)', + $query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, module, data) + VALUES (:entryid, :hotkey, :title, 1, :module, :data) + ON DUPLICATE KEY UPDATE builtin = 1, module = VALUES(module), data = VALUES(data)'; + Database::exec($query, [ 'entryid' => 'bwlp-default', 'hotkey' => 'B', 'title' => 'bwLehrpool-Umgebung starten', + 'module' => 'minilinux', 'data' => json_encode([ - 'script' => ' -imgfree || -set slxextra initrd=logo || -initrd /boot/default/initramfs-stage31 || goto fail -initrd --name logo /tftp/bwlp.cpio || clear slxextra -boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boot/default quiet splash loglevel=5 rd.systemd.show_status=auto intel_iommu=igfx_off ${ipappend1} ${ipappend2} || goto fail -', + 'id' => 'default', + 'kcl-extra' => '', + 'debug' => false, ]), ]); - $query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) - VALUES (:entryid, :hotkey, :title, 1, :data) ON DUPLICATE KEY UPDATE data = VALUES(data)'; Database::exec($query, [ 'entryid' => 'bwlp-default-dbg', 'hotkey' => 'D', 'title' => 'bwLehrpool-Umgebung starten (nosplash, debug output)', + 'module' => 'minilinux', 'data' => json_encode([ - 'executable' => ['PCBIOS' => '/boot/default/kernel'], - 'initRd' => ['PCBIOS' => ['/boot/default/initramfs-stage31']], - 'commandLine' => ['PCBIOS' => 'slxbase=boot/default loglevel=7 intel_iommu=igfx_off ${ipappend1} ${ipappend2}'], - 'replace' => true, - 'autoUnload' => true, - 'resetConsole' => true, - 'arch' => 'agnostic', + 'id' => 'default', + 'kcl-extra' => '', + 'debug' => true, ]), ]); Database::exec($query, [ 'entryid' => 'bwlp-default-sh', - 'hotkey' => 'D', + 'hotkey' => 'S', 'title' => 'bwLehrpool-Umgebung starten (nosplash, !!! debug shell !!!)', + 'module' => 'minilinux', 'data' => json_encode([ - 'executable' => ['PCBIOS' => '/boot/default/kernel'], - 'initRd' => ['PCBIOS' => ['/boot/default/initramfs-stage31']], - 'commandLine' => ['PCBIOS' => 'slxbase=boot/default loglevel=7 debug=1 intel_iommu=igfx_off ${ipappend1} ${ipappend2}'], - 'replace' => true, - 'autoUnload' => true, - 'resetConsole' => true, - 'arch' => 'agnostic', + 'id' => 'default', + 'kcl-extra' => 'debug=1', + 'debug' => true, ]), ]); Database::exec($query, @@ -385,17 +375,17 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo 'entryid' => 'localboot', 'hotkey' => 'L', 'title' => 'Lokales System starten', - 'data' => json_encode([ - 'script' => 'goto slx_localboot || goto %fail% ||', - ]), + 'module' => '.special', + 'data' => json_encode(['type' => 'localboot']), ]); Database::exec($query, [ 'entryid' => 'poweroff', 'hotkey' => 'P', 'title' => 'Power off', + 'module' => '.script', 'data' => json_encode([ - 'script' => 'poweroff || goto %fail% ||', + 'script' => 'poweroff || goto fail ||', ]), ]); Database::exec($query, @@ -403,8 +393,9 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo 'entryid' => 'reboot', 'hotkey' => 'R', 'title' => 'Reboot', + 'module' => '.script', 'data' => json_encode([ - 'script' => 'reboot || goto %fail% ||', + 'script' => 'reboot || goto fail ||', ]), ]); } @@ -415,10 +406,9 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo * Also it patches the entry if it's referencing the local bwlp install * but with different options. * - * @param PxeSection $section * @return string|false existing label if match, false otherwise */ - private static function cleanLabelFixLocal($section) + private static function cleanLabelFixLocal(PxeSection $section) { $myip = Property::getServerIp(); // Detect our "old" entry types @@ -441,10 +431,9 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo } /** - * @param PxeSection $section * @return BootEntry|null The according boot entry, null if it's unparsable */ - private static function pxe2BootEntry($section) + private static function pxe2BootEntry(PxeSection $section): ?BootEntry { if (preg_match('/(pxechain\.com|pxechn\.c32)$/i', $section->kernel)) { // Chaining -- create script @@ -473,7 +462,7 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo $script .= "set netX/{$opt}:{$type} {$args[$i]} || goto %fail%\n"; } } - } elseif ($arg{0} === '-') { + } elseif ($arg[0] === '-') { continue; } elseif ($file === false) { $file = self::parseFile($arg); @@ -503,11 +492,8 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo /** * Parse PXELINUX file notion. Basically, turn * server::file into tftp://server/file. - * - * @param string $file - * @return string */ - private static function parseFile($file) + private static function parseFile(string $file): string { if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) { return 'tftp://' . $out[1] . '/' . $out[2]; @@ -515,12 +501,12 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo return $file; } - public static function sanitizeIpxeString($string) + public static function sanitizeIpxeString(string $string): string { return str_replace(['&', '|', ';', '$', "\r", "\n"], ['+', '/', ':', 'S', ' ', ' '], $string); } - public static function makeMd5Pass($plainpass, $salt) + public static function makeMd5Pass(string $plainpass, string $salt): string { if (empty($plainpass)) return ''; @@ -535,15 +521,16 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo * remove any occurrence of either "option" or "option=something". If the argument starts with a * '+', it will be added to the command line after removing the '+'. If the argument starts with any * other character, it will also be added to the command line. + * * @param string $cmdLine command line to modify - * @param string $modifier modification string of space separated arguments + * @param string $modifier modification string of space separated arguments * @return string the modified command line */ - public static function modifyCommandLine($cmdLine, $modifier) + public static function modifyCommandLine(string $cmdLine, string $modifier): string { $items = preg_split('/\s+/', $modifier, -1, PREG_SPLIT_NO_EMPTY); foreach ($items as $item) { - if ($item{0} === '-') { + if ($item[0] === '-') { $item = preg_quote(substr($item, 1), '/'); $cmdLine = preg_replace('/(^|\s)' . $item . '(=\S*)?($|\s)/', ' ', $cmdLine); } else { diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php new file mode 100644 index 00000000..a2b25f55 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php @@ -0,0 +1,80 @@ +<?php + +class IPxeBuilder +{ + + const PROP_IPXE_BUILDSTRING = 'ipxe.compile-time'; + const PROP_IPXE_HASH = 'ipxe.commit-hash'; + const PROP_IPXE_COMPILE_TASKID = 'ipxe-task-id'; + const VERSION_LIST_TASK = 'ipxe-version-list-id'; + const PROP_VERSION_SELECT_TASKID = 'ipxe-version-select-id'; + + /** + * Checkout given commit/ref of ipxe repo. Returns the according task-id, or null on error + * + * @param string $version version ref (commit, tag, ...) + * @param ?string $parent parent task id, if any + */ + public static function setIpxeVersion(string $version, ?string $parent = null): ?string + { + $task = Taskmanager::submit('IpxeVersion', [ + 'action' => 'CHECKOUT', + 'ref' => $version, + 'parentTask' => $parent, + ]); + if (!Taskmanager::isTask($task)) + return null; + TaskmanagerCallback::addCallback($task, 'ipxeVersionSet'); + Property::set(IPxeBuilder::PROP_VERSION_SELECT_TASKID, $task['id'], 2); + return $task['id']; + } + + public static function getVersionTaskResult(): ?array + { + $task = Taskmanager::status(IPxeBuilder::VERSION_LIST_TASK); + if (!Taskmanager::isTask($task) || Taskmanager::isFailed($task)) { + $task = Taskmanager::submit('IpxeVersion', + ['id' => IPxeBuilder::VERSION_LIST_TASK, 'action' => 'LIST']); + } + $task = Taskmanager::waitComplete($task); + if (Taskmanager::isFinished($task) && !Taskmanager::isFailed($task)) { + return $task['data']; + } + return null; + } + + /** + * Callback when compile Taskmanager job finished + */ + public static function compileCompleteCallback(array $task): void + { + if (!Taskmanager::isFinished($task) || Taskmanager::isFailed($task)) + return; + $version = 'Unknown'; + if (isset($task['data']['hash'])) { + $hash = $task['data']['hash']; + Property::set(IPxeBuilder::PROP_IPXE_HASH, $hash); + $version = $hash; + $list = IPxeBuilder::getVersionTaskResult(); + if (isset($list['versions'])) { + foreach ($list['versions'] as $v) { + if ($v['hash'] === $version) { + // Do NOT change (see below) + $version = date('Y-m-d H:i', $v['date']) . ' (' . substr($version, 0, 7) . ')'; + break; + } + } + } + } + // Do NOT change the format of this string -- we depend on it in ScriptBuilderIpxe::output() + $buildString = date('d.m.Y H:i') . ', Version: ' . $version; + Property::set(IPxeBuilder::PROP_IPXE_BUILDSTRING, $buildString); + } + + public static function setIPxeVersionCallback(array $task): void + { + if (!Taskmanager::isFinished($task) || Taskmanager::isFailed($task) || empty($task['data']['ref'])) + return; + Property::set(IPxeBuilder::PROP_IPXE_HASH, $task['data']['ref']); + } +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php index f87d15c2..3ffecba1 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php @@ -3,84 +3,71 @@ class IPxeMenu { + /** + * @var int ID of this menu, from DB + */ protected $menuid; - protected $timeoutMs; - protected $title; - protected $defaultEntryId; + /** + * @var int 0 = disabled, otherwise, launch default option after timeout + */ + public $timeoutMs; + /** + * @var string title to display above menu + */ + public $title; + /** + * @var int menu entry id from DB + */ + public $defaultEntryId; /** * @var MenuEntry[] */ - protected $items = []; + public $items = []; - public function __construct($menu) + public static function get(int $menuId, bool $emptyFallback = false): ?IPxeMenu + { + $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu + WHERE menuid = :menuid LIMIT 1", ['menuid' => $menuId]); + if ($menu !== false) + return new IPxeMenu($menu); + if (!$emptyFallback) + return null; + return new EmptyIPxeMenu(); + } + + /** + * IPxeMenu constructor. + * + * @param array $menu array for according menu row + */ + public function __construct(array $menu) { - if (!is_array($menu)) { - $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu - WHERE menuid = :menuid LIMIT 1", ['menuid' => $menu]); - if (!is_array($menu)) { - $menu = ['menuid' => 'foo', 'title' => 'Invalid Menu ID: ' . (int)$menu]; - } - } $this->menuid = (int)$menu['menuid']; $this->timeoutMs = (int)$menu['timeoutms']; - $this->title = $menu['title']; - $this->defaultEntryId = $menu['defaultentryid']; - $res = Database::simpleQuery("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title, e.hidden, e.sortval, e.md5pass, - b.module, b.data AS bootentry + $this->title = (string)$menu['title']; + $defaultEntryId = $menu['defaultentryid']; + $res = Database::simpleQuery("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title, + e.hidden, e.sortval, e.md5pass, b.module, b.data AS bootentry, b.title AS betitle FROM serversetup_menuentry e LEFT JOIN serversetup_bootentry b USING (entryid) WHERE e.menuid = :menuid ORDER BY e.sortval ASC, e.title ASC", ['menuid' => $menu['menuid']]); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { $this->items[] = new MenuEntry($row); } // Make sure we have a default entry if the menu isn't empty - if ($this->defaultEntryId === null && !empty($this->items)) { - $this->defaultEntryId = $this->items[0]->menuEntryId(); - } - } - - public function getMenuDefinition($targetVar, $mode, $slxExtensions) - { - $str = "menu -- {$this->title}\n"; - foreach ($this->items as $item) { - $str .= $item->getMenuItemScript("m_{$this->menuid}", $this->defaultEntryId, $mode, $slxExtensions); - } - if ($this->defaultEntryId === null) { - $defaultLabel = "mx_{$this->menuid}_poweroff"; - } else { - $defaultLabel = "m_{$this->menuid}_{$this->defaultEntryId}"; - } - $str .= "choose"; - if ($this->timeoutMs > 0) { - $str .= " --timeout {$this->timeoutMs}"; + if ($defaultEntryId === null && !empty($this->items)) { + $defaultEntryId = $this->items[0]->menuEntryId(); } - $str .= " $targetVar || goto $defaultLabel || goto fail\n"; - if ($this->defaultEntryId === null) { - $str .= "goto skip_{$defaultLabel}\n" - . ":{$defaultLabel}\n" - . "poweroff || goto fail\n" - . ":skip_{$defaultLabel}\n"; - } - return $str; - } - - public function getItemsCode($mode) - { - $str = ''; - foreach ($this->items as $item) { - $str .= $item->getBootEntryScript("m_{$this->menuid}", 'fail', $mode); - $str .= "goto slx_menu\n"; - } - return $str; + $this->defaultEntryId = (int)$defaultEntryId; } - public function title() + public function title(): string { return $this->title; } - public function timeoutMs() + public function timeoutMs(): int { return $this->timeoutMs; } @@ -88,52 +75,89 @@ class IPxeMenu /** * @return int Number of items in this menu */ - public function itemCount() + public function itemCount(): int { return count($this->items); } /** - * @return string|false Return script label of default entry, false if not set + * @return MenuEntry|null Return preselected menu entry */ - public function getDefaultScriptLabel() + public function defaultEntry(): ?MenuEntry { - if ($this->defaultEntryId !== null) - return "m_{$this->menuid}_{$this->defaultEntryId}"; - return false; + foreach ($this->items as $item) { + if ($item->menuEntryId() === $this->defaultEntryId) + return $item; + } + return null; + } + + private function maybeOverrideDefault(string $uuid) + { + $e = $this->defaultEntry(); + // Shortcut - is already bwlp and timeout is reasonable (1-15s), do nothing + $defIsMl = $e !== null && substr($e->internalId(), 0, 3) === 'ml-'; + $timeoutOk = $this->timeoutMs > 0 && $this->timeoutMs <= 15000; + if ($timeoutOk && $defIsMl) + return; + // No runmode module anyways + if (!Module::isAvailable('runmode')) + return; + $rm = RunMode::getRunMode($uuid); + // No runmode for this client, cannot be PVSmgr + if ($rm === false) + return; + // Is not pvsmgr + if ($rm['module'] !== 'roomplanner') + return; + // See if it's a dedicated station, if so make sure it boots into bwLehrpool + $data = json_decode($rm['modedata'], true); + if ($data['dedicatedmgr'] ?? false) { + if (!$defIsMl) { + $this->overrideDefaultToMinilinux(); + } + if (!$timeoutOk) { + $this->timeoutMs = 5000; + } + } } /** - * @return MenuEntry|null Return preselected menu entry + * Patch the menu to make sure bwLehrpool/"MiniLinux" is the default + * boot option, and set timeout to something reasonable. This is used + * for dedicated PVS managers, as they might not have a keyboard + * connected. */ - public function defaultEntry() + private function overrideDefaultToMinilinux() { foreach ($this->items as $item) { - if ($item->menuEntryId() == $this->defaultEntryId) - return $item; + if (substr($item->internalId(), 0, 3) === 'ml-') { + $this->defaultEntryId = $item->menuEntryId(); + return; + } } - return null; } /* * */ - public static function forLocation($locationId) + public static function forLocation(int $locationId): IPxeMenu { $chain = null; if (Module::isAvailable('locations')) { $chain = Location::getLocationRootChain($locationId); } if (!empty($chain)) { - $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid - FROM serversetup_menu m - INNER JOIN serversetup_menu_location ml USING (menuid) - WHERE ml.locationid IN (:chain)", ['chain' => $chain]); + $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, + IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid + FROM serversetup_menu m + INNER JOIN serversetup_menu_location ml USING (menuid) + WHERE ml.locationid IN (:chain)", ['chain' => $chain]); if ($res->rowCount() > 0) { // Make the location id key, preserving order (closest location is first) $chain = array_flip($chain); - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($res as $row) { // Overwrite the value (numeric ascending values, useless) with menu array of according location $chain[(int)$row['locationid']] = $row; } @@ -156,13 +180,19 @@ class IPxeMenu return new IPxeMenu($menu); } - public static function forClient($ip, $uuid) + public static function forClient(string $ip, ?string $uuid): IPxeMenu { $locationId = 0; if (Module::isAvailable('locations')) { $locationId = Location::getFromIpAndUuid($ip, $uuid); } - return self::forLocation($locationId); + $menu = self::forLocation($locationId); + if ($uuid !== null) { + // Super specialcase hackery: If this is a dedicated PVS, force the default to + // be bwlp/"minilinux" + $menu->maybeOverrideDefault($uuid); + } + return $menu; } } @@ -170,11 +200,14 @@ class IPxeMenu class EmptyIPxeMenu extends IPxeMenu { - /** @noinspection PhpMissingParentConstructorInspection */ public function __construct() { - $this->title = 'No menu defined'; - $this->menuid = -1; + parent::__construct([ + 'menuid' => -1, + 'timeoutms' => 120, + 'defaultentryid' => null, + 'title' => 'No menu defined', + ]); $this->items[] = new MenuEntry([ 'title' => 'Please create a menu in Server-Setup first' ]); diff --git a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php index 4203f931..4d1a56c7 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php @@ -7,38 +7,49 @@ class Localboot const BOOT_METHODS = [ 'PCBIOS' => [ - 'EXIT' => 'exit 1', + 'EXIT' => 'set slx_exit 1 || +exit 1', 'COMBOOT' => 'set netX/209:string localboot.cfg || set netX/210:string http://${serverip}/tftp/sl-bios/ || chain -ar /tftp/sl-bios/lpxelinux.0', 'SANBOOT' => 'sanboot --no-describe', ], 'EFI' => [ - 'EXIT' => 'exit 1', - 'COMBOOT' => 'set netX/209:string localboot.cfg || -set netX/210:string http://${serverip}/tftp/sl-efi64/ || -chain -ar /tftp/sl-efi64/syslinux.efi', + 'EXIT' => 'set slx_exit 1 || +exit 1', + 'SANBOOT' => 'imgfree || +console || +set filename \EFI\Boot\bootx64.efi || +set i:int32 0 || +:blubber +sanboot --no-describe --drive ${i} --filename ${filename} || +inc i +iseq ${i} 10 || goto blubber', + 'GRUB' => 'chain /tftp/grub-boot.img', ], ]; - public static function getDefault() + /** + * @return array{PCBIOS: string, EFI: string} + */ + public static function getDefault(): array { - $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,EXIT')); + $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,GRUB')); if (empty($ret)) { - $ret = ['SANBOOT', 'EXIT']; + $ret = ['SANBOOT', 'GRUB']; } elseif (count($ret) < 2) { - $ret[] = 'EXIT'; + $ret[] = 'SANBOOT'; } - if (null === self::BOOT_METHODS['PCBIOS'][$ret[0]]) { + if (!isset(self::BOOT_METHODS['PCBIOS'][$ret[0]])) { $ret[0] = 'SANBOOT'; } - if (null === self::BOOT_METHODS['EFI'][$ret[1]]) { - $ret[1] = 'EXIT'; + if (!isset(self::BOOT_METHODS['EFI'][$ret[1]])) { + $ret[1] = 'GRUB'; } return ['PCBIOS' => $ret[0], 'EFI' => $ret[1]]; } - public static function setDefault($pcbios, $efi) + public static function setDefault(string $pcbios, string $efi) { Property::set(self::PROPERTY_KEY, "$pcbios,$efi"); } diff --git a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php index a65e9f98..da94a16b 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php @@ -5,114 +5,98 @@ class MenuEntry /** * @var int id of entry, used for pw */ - private $menuentryid; + public $menuentryid; /** * @var false|string key code as expected by iPXE */ - private $hotkey; + public $hotkey; /** * @var string */ - private $title; + public $title; /** * @var bool */ - private $hidden; + public $hidden; /** * @var bool */ - private $gap; + public $gap; /** * @var int */ - private $sortval; + public $sortval; /** - * @var BootEntry + * @var ?BootEntry */ - private $bootEntry = null; + public $bootEntry = null; - private $md5pass = null; + public $plainpass = null; + + public $md5pass = null; + + public static function get(int $menuEntryId): ?MenuEntry + { + $row = Database::queryFirst("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title, + e.hidden, e.sortval, e.plainpass, e.md5pass, b.module, b.data AS bootentry, b.title AS betitle + FROM serversetup_menuentry e + LEFT JOIN serversetup_bootentry b USING (entryid) + WHERE e.menuentryid = :id", ['id' => $menuEntryId]); + if ($row === false) + return null; + return new MenuEntry($row); + } /** * MenuEntry constructor. * * @param array $row row from database */ - public function __construct($row) + public function __construct(array $row) { - if (is_array($row)) { - foreach ($row as $key => $value) { - if (property_exists($this, $key)) { - $this->{$key} = $value; - } - } - $this->hotkey = self::getKeyCode($row['hotkey']); - if (!empty($row['bootentry'])) { - $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']); - } elseif ($row['refmenuid'] !== null) { - $this->bootEntry = BootEntry::forMenu($row['refmenuid']); + if (empty($row['title']) && !empty($row['betitle'])) { + $row['title'] = $row['betitle']; + } + foreach ($row as $key => $value) { + if (property_exists($this, $key)) { + $this->{$key} = $value; } - $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null); } + $this->hotkey = self::getKeyCode($row['hotkey'] ?? ''); + if (!empty($row['bootentry'])) { + $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']); + } elseif (isset($row['refmenuid'])) { + $this->bootEntry = BootEntry::forMenu($row['refmenuid']); + } + $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null); settype($this->hidden, 'bool'); settype($this->gap, 'bool'); settype($this->sortval, 'int'); settype($this->menuentryid, 'int'); } - public function getMenuItemScript($lblPrefix, $requestedDefaultId, $mode, $slxExtensions) + public function getBootEntryScript(ScriptBuilderBase $builder): string { - if ($this->bootEntry !== null && !$this->bootEntry->supportsMode($mode)) + if ($this->bootEntry === null) return ''; - $str = 'item '; - if ($this->gap) { - $str .= '--gap -- '; - } else { - if ($this->hidden && $slxExtensions) { - if ($this->hotkey === false) - return ''; // Hidden entries without hotkey are illegal - $str .= '--hidden '; - } - if ($this->hotkey !== false) { - $str .= '--key ' . $this->hotkey . ' '; - } - if ($this->menuentryid == $requestedDefaultId) { - $str .= '--default '; - } - $str .= "-- {$lblPrefix}_{$this->menuentryid} "; - } - if (empty($this->title)) { - $str .= '${}'; - } else { - $str .= $this->title; - } - return $str . " || prompt Could not create menu item for {$lblPrefix}_{$this->menuentryid}\n"; + return $this->bootEntry->toScript($builder); } - public function getBootEntryScript($lblPrefix, $failLabel, $mode) + public function menuEntryId(): int { - if ($this->bootEntry === null || !$this->bootEntry->supportsMode($mode)) - return ''; - $str = ":{$lblPrefix}_{$this->menuentryid}\n"; - if (!empty($this->md5pass)) { - $str .= "set slx_hash {$this->md5pass} || goto $failLabel\n" - . "set slx_salt {$this->menuentryid} || goto $failLabel\n" - . "set slx_pw_ok {$lblPrefix}_ok || goto $failLabel\n" - . "set slx_pw_fail slx_menu || goto $failLabel\n" - . "goto slx_pass_check || goto $failLabel\n" - . ":{$lblPrefix}_ok\n"; - } - return $str . $this->bootEntry->toScript($failLabel, $mode); + return $this->menuentryid; } - public function menuEntryId() + public function title(): string { - return $this->menuentryid; + return $this->title; } - public function title() + public function internalId(): string { - return $this->title; + if ($this->bootEntry === null) + return ''; + return $this->bootEntry->internalId(); } /* @@ -154,7 +138,7 @@ class MenuEntry * * @return string[] list of known key names */ - public static function getKeyList() + public static function getKeyList(): array { return array_keys(self::getKeyArray()); } @@ -163,10 +147,9 @@ class MenuEntry * Get the key code ipxe expects for the given named * key. Returns false if the key name is unknown. * - * @param string $keyName * @return false|string Key code as hex string, or false if not found */ - public static function getKeyCode($keyName) + public static function getKeyCode(string $keyName) { $data = self::getKeyArray(); if (isset($data[$keyName])) @@ -178,7 +161,7 @@ class MenuEntry * @param string $keyName desired key name * @return string $keyName if it's known, empty string otherwise */ - public static function filterKeyName($keyName) + public static function filterKeyName(string $keyName): string { $data = self::getKeyArray(); if (isset($data[$keyName])) diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php index ff548c4c..24b099dc 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php @@ -7,11 +7,14 @@ class PxeLinux /** * Takes a (partial) pxelinux menu and parses it into * a PxeMenu object. + * * @param string $input The pxelinux menu to parse - * @return PxeMenu the parsed menu + * @return ?PxeMenu the parsed menu, or null if input is not a PXELinux menu */ - public static function parsePxeLinux($input, $isCp437) + public static function parsePxeLinux(string $input, bool $isCp437): ?PxeMenu { + if (empty($input)) + return null; if ($isCp437) { $input = iconv('IBM437', 'UTF8//TRANSLIT//IGNORE', $input); } @@ -76,12 +79,14 @@ class PxeLinux } $section->helpText = $text; } elseif (self::handleKeyword($key, $val, $sectionPropMap, $section)) { - continue; + //continue; } } if ($section !== null) { $menu->sections[] = $section; } + if (empty($menu->sections)) + return null; // Probably not a PXE menu but random text? foreach ($menu->sections as $section) { $section->mangle(); } @@ -93,13 +98,14 @@ class PxeLinux * to the given object. The map to look up the keyword has to be passed * as well as the object to set the value in. Map and object should * obviously match. + * * @param string $key keyword of parsed line * @param string $val raw value of currently parsed line (empty if not present) * @param array $map Map in which $key is looked up as key - * @param PxeMenu|PxeSection The object to set the parsed and sanitized value in + * @param PxeMenu|PxeSection $object The object to set the parsed and sanitized value in * @return bool true if the value was found in the map (and set in the object), false otherwise */ - private static function handleKeyword($key, $val, $map, $object) + private static function handleKeyword(string $key, string $val, array $map, $object): bool { if (!isset($map[$key])) return false; @@ -122,161 +128,3 @@ class PxeLinux } -/** - * Class representing a parsed pxelinux menu. Members - * will be set to their annotated type if present or - * be null otherwise, except for present-only boolean - * options, which will default to false. - */ -class PxeMenu -{ - - /** - * @var string menu title, shown at the top of the menu - */ - public $title; - /** - * @var int initial timeout after which $timeoutLabel would be executed - */ - public $timeoutMs; - /** - * @var int if the user canceled the timeout by pressing a key, this timeout would still eventually - * trigger and launch the $timeoutLabel section - */ - public $totalTimeoutMs; - /** - * @var string label of section which will execute if the timeout expires - */ - public $timeoutLabel; - /** - * @var bool hide menu and just show background after triggering an entry - */ - public $menuClear = false; - /** - * @var bool boot the associated entry directly if its corresponding hotkey is pressed instead of just highlighting - */ - public $immediateHotkeys = false; - /** - * @var PxeSection[] list of sections the menu contains - */ - public $sections = []; - /** - * @var string The DEFAULT entry of the menu. Usually refers either to a - * LABEL, or a loadable module (like vesamenu.c32) - */ - public $default; - - /** - * Check if any of the sections has the given label. - */ - public function hasLabel($label) - { - foreach ($this->sections as $section) { - if ($section->label === $label) - return true; - } - return false; - } - -} - -/** - * Class representing a parsed pxelinux menu entry. Members - * will be set to their annotated type if present or - * be null otherwise, except for present-only boolean - * options, which will default to false. - */ -class PxeSection -{ - - /** - * @var string label used internally in PXEMENU definition to address this entry - */ - public $label; - /** - * @var string MENU LABEL of PXEMENU - title of entry displayed to the user - */ - public $title; - /** - * @var int Number of spaces to prefix the title with - */ - public $indent; - /** - * @var string help text to display when the entry is highlighted - */ - public $helpText; - /** - * @var string Kernel to load - */ - public $kernel; - /** - * @var string|string[] initrd to load for the kernel. - * If mangle() has been called this will be an array, - * otherwise it's a comma separated list. - */ - public $initrd; - /** - * @var string command line options to pass to the kernel - */ - public $append; - /** - * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2. - */ - public $ipAppend; - /** - * @var string Password protecting the entry. This is most likely in crypted form. - */ - public $passwd; - /** - * @var bool whether this section is marked as default (booted after timeout) - */ - public $isDefault = false; - /** - * @var bool Menu entry is not visible (can only be triggered by timeout) - */ - public $isHidden = false; - /** - * @var bool Disable this entry, making it unselectable - */ - public $isDisabled = false; - /** - * @var int|false Value of the LOCALBOOT field, false if not set - */ - public $localBoot = false; - /** - * @var string hotkey to trigger item. Only valid after calling mangle() - */ - public $hotkey; - - public function __construct($label) { $this->label = $label; } - - public function mangle() - { - if (($i = strpos($this->title, '^')) !== false) { - $this->hotkey = strtoupper($this->title{$i+1}); - $this->title = substr($this->title, 0, $i) . substr($this->title, $i + 1); - } - if (strpos($this->append, 'initrd=') !== false) { - $parts = preg_split('/\s+/', $this->append); - $this->append = ''; - for ($i = 0; $i < count($parts); ++$i) { - if (preg_match('/^initrd=(.*)$/', $parts[$i], $out)) { - if (!empty($this->initrd)) { - $this->initrd .= ','; - } - $this->initrd .= $out[1]; - } else { - $this->append .= ' ' . $parts[$i]; - } - } - $this->append = trim($this->append); - } - if (is_string($this->initrd)) { - $this->initrd = explode(',', $this->initrd); - } elseif (!is_array($this->initrd)) { - $this->initrd = []; - } - } - -} - diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php new file mode 100644 index 00000000..7be57ef1 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php @@ -0,0 +1,59 @@ +<?php + +/** + * Class representing a parsed pxelinux menu. Members + * will be set to their annotated type if present or + * be null otherwise, except for present-only boolean + * options, which will default to false. + */ +class PxeMenu +{ + + /** + * @var string menu title, shown at the top of the menu + */ + public $title; + /** + * @var int initial timeout after which $timeoutLabel would be executed + */ + public $timeoutMs; + /** + * @var int if the user canceled the timeout by pressing a key, this timeout would still eventually + * trigger and launch the $timeoutLabel section + */ + public $totalTimeoutMs; + /** + * @var string label of section which will execute if the timeout expires + */ + public $timeoutLabel; + /** + * @var bool hide menu and just show background after triggering an entry + */ + public $menuClear = false; + /** + * @var bool boot the associated entry directly if its corresponding hotkey is pressed instead of just highlighting + */ + public $immediateHotkeys = false; + /** + * @var PxeSection[] list of sections the menu contains + */ + public $sections = []; + /** + * @var string The DEFAULT entry of the menu. Usually refers either to a + * LABEL, or a loadable module (like vesamenu.c32) + */ + public $default; + + /** + * Check if any of the sections has the given label. + */ + public function hasLabel(string $label): bool + { + foreach ($this->sections as $section) { + if ($section->label === $label) + return true; + } + return false; + } + +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php new file mode 100644 index 00000000..2d9cd6ab --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php @@ -0,0 +1,117 @@ +<?php + +/** + * Class representing a parsed pxelinux menu entry. Members + * will be set to their annotated type if present or + * be null otherwise, except for present-only boolean + * options, which will default to false. + */ +class PxeSection +{ + + /** + * @var ?string label used internally in PXEMENU definition to address this entry + */ + public $label; + /** + * @var string MENU LABEL of PXEMENU - title of entry displayed to the user + */ + public $title; + /** + * @var int Number of spaces to prefix the title with + */ + public $indent; + /** + * @var string help text to display when the entry is highlighted + */ + public $helpText; + /** + * @var string Kernel to load + */ + public $kernel; + /** + * @var string|string[] initrd to load for the kernel. + * If mangle() has been called this will be an array, + * otherwise it's a comma separated list. + */ + public $initrd; + /** + * @var string command line options to pass to the kernel + */ + public $append; + /** + * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2. + */ + public $ipAppend; + /** + * @var string Password protecting the entry. This is most likely in encrypted form. + */ + public $passwd; + /** + * @var bool whether this section is marked as default (booted after timeout) + */ + public $isDefault = false; + /** + * @var bool Menu entry is not visible (can only be triggered by timeout) + */ + public $isHidden = false; + /** + * @var bool Disable this entry, making it unselectable + */ + public $isDisabled = false; + /** + * @var int|false Value of the LOCALBOOT field, false if not set + */ + public $localBoot = false; + /** + * @var string hotkey to trigger item. Only valid after calling mangle() + */ + public $hotkey; + + public function __construct(?string $label) { $this->label = $label; } + + public function mangle() + { + if (($i = strpos($this->title, '^')) !== false) { + $this->hotkey = strtoupper($this->title[$i + 1]); + $this->title = substr($this->title, 0, $i) . substr($this->title, $i + 1); + } + if (strpos($this->append, 'initrd=') !== false) { + $parts = preg_split('/\s+/', $this->append); + $this->append = ''; + for ($i = 0; $i < count($parts); ++$i) { + if (preg_match('/^initrd=(.*)$/', $parts[$i], $out)) { + if (!empty($this->initrd)) { + $this->initrd .= ','; + } + $this->initrd .= $out[1]; + } else { + $this->append .= ' ' . $parts[$i]; + } + } + $this->append = trim($this->append); + } + if (is_string($this->initrd)) { + $this->initrd = explode(',', $this->initrd); + } elseif (!is_array($this->initrd)) { + $this->initrd = []; + } + } + + /** + * Does this appear to be an entry that triggers localboot? + */ + public function isLocalboot(): bool + { + return $this->localBoot !== false || preg_match('/chain\.c32$/i', $this->kernel); + } + + /** + * Is this (most likely) a separating entry only that cannot be selected? + */ + public function isTextOnly(): bool + { + return ($this->label === null || empty($this->kernel)) && !$this->isHidden && !empty($this->title); + } + +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php new file mode 100644 index 00000000..9cd07388 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php @@ -0,0 +1,102 @@ +<?php + +abstract class ScriptBuilderBase +{ + + private $lblId = 0; + + protected $serverIp; + + protected $platform = ''; + + /** @var string */ + protected $clientIp; + + /** @var ?string */ + protected $uuid; + + /** + * @var bool Running iPXE has slx-extensions + */ + protected $hasExtension = false; + + public function hasExtensions(): bool + { + return $this->hasExtension; + } + + public function platform(): string + { + return $this->platform; + } + + public function uuid(): ?string + { + return $this->uuid; + } + + public function clientIp(): string + { + return $this->clientIp; + } + + public function getLabel(): string + { + return 'b' . mt_rand(100, 999) . 'x' . (++$this->lblId); + } + + public function __construct(?string $platform = null, ?string $serverIp = null, ?bool $slxExtensions = null) + { + $this->clientIp = (string)$_SERVER['REMOTE_ADDR']; + if (substr($this->clientIp, 0, 7) === '::ffff:') { + $this->clientIp = substr($this->clientIp, 7); + } + $this->serverIp = $serverIp ?? $_SERVER['SERVER_ADDR'] ?? Property::getServerIp(); + $this->platform = $platform ?? Request::any('platform', null, 'string'); + if ($this->platform !== null) { + $this->platform = strtoupper($this->platform); + } + if ($this->platform !== 'EFI' && $this->platform !== 'PCBIOS') { + $this->platform = ''; + } + $this->hasExtension = $slxExtensions ?? (bool)Request::any('slx-extensions', false, 'int'); + $uuid = Request::any('uuid', null, 'string'); + if ($uuid !== null + && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $uuid)) { + $this->uuid = (string)$uuid; + } + } + + /** + * Output given string (script) to client, in a suitable encoding, headers, etc. + */ + public abstract function output(string $string): void; + + public abstract function bootstrapLive(); + + public abstract function getMenu(IPxeMenu $menu, bool $bootstrap); + + /** + * @param MenuEntry|null $menuEntry The according menu entry, or null if invalid. + * @param bool $honorPassword Whether we should generate a password dialog if protected, or skip + * @return string generated script/code/... + */ + public abstract function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string; + + /** + * @param BootEntry|null|false $bootEntry + */ + public abstract function getBootEntry(?BootEntry $entry): string; + + public abstract function getSpecial(string $special); + + public abstract function menuToScript(IPxeMenu $menu): string; + + /** + * Pass EITHER only $agnostic, OR $bios and/or $efi + * If $agnostic is given, it should be used unconditionally, + * and $bios/$efi should be ignored. + */ + public abstract function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string; + +}
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php new file mode 100644 index 00000000..d6b542ec --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php @@ -0,0 +1,97 @@ +<?php + +class ScriptBuilderBash extends ScriptBuilderBase +{ + + public function output(string $string): void + { + echo $string; + } + + public function bootstrapLive(): bool { return false; } + + public function getMenu(IPxeMenu $menu, bool $bootstrap): string + { + return $this->menuToScript($menu); + } + + public function getBootEntry(?BootEntry $entry): string + { + if ($entry === null) { + return "echo 'Invalid boot entry id'\nread -n1 -r _\n"; + } + return $entry->toScript($this); + } + + public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string + { + if ($entry === null) + return "echo 'Invalid menu entry id - press any key to continue'\nread -n1 -r _\n"; + return $entry->getBootEntryScript($this); + } + + public function getSpecial(string $special): string + { + return ''; // We can't really do localboot here I guess + } + + public function menuToScript(IPxeMenu $menu): string + { + $output = "declare -A items_name items_gap hotkey_item\ndeclare menu_default menu_timeout menu_title\n"; + foreach ($menu->items as $entry) { + $id = $entry->menuentryid; + if ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) + continue; + if (!$entry->hidden) { + $output .= 'items_name[' . $id . ']=' . $this->bashString($entry->title) . "\n"; + if ($entry->gap) { + $output .= 'items_gap[' . $id . "]=1\n"; + } + } + if ($entry->hotkey !== false) { + $output .= 'hotkey_item[' . $entry->hotkey . ']=' . $id . "\n"; + } + if ($id == $menu->defaultEntryId) { + $output .= "menu_default={$id}\n"; + } + } + return $output . "menu_timeout=" . $menu->timeoutMs + . "\nmenu_title=" . $this->bashString($menu->title) . "\n"; + } + + public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string + { + if ($agnostic !== null) + return $this->execDataToScriptInternal($agnostic); + if ($bios !== null && $this->platform === BootEntry::BIOS) + return $this->execDataToScriptInternal($bios); + if ($efi !== null && $this->platform === BootEntry::EFI) + return $this->execDataToScriptInternal($efi); + return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData()); + } + + private function execDataToScriptInternal(ExecData $entry) : string + { + $entry->sanitize(); + $script = "declare -a initrd\ndeclare kernel kcl\n"; + if (!empty($entry->initRd)) { + foreach ($entry->initRd as $initrd) { + if (empty($initrd)) + continue; + $script .= 'initrd+=( ' . $this->bashString($initrd) . " )\n"; + } + } + $script .= 'kernel=' . $this->bashString($entry->executable) . "\n"; + $script .= 'kcl="' . str_replace('"', '"\\""', $entry->commandLine) . "\"\n"; // Allow expansion + return $script; + } + + private function bashString(string $string): string + { + if (strpos($string, "'") === false) { + return "'$string'"; + } + return "'" . str_replace("'", "'\\''", $string) . "'"; + } + +} diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php new file mode 100644 index 00000000..9dce5214 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php @@ -0,0 +1,330 @@ +<?php + +class ScriptBuilderGrub extends ScriptBuilderBase +{ + + /** @var bool */ + private $confCodeEmited = false; + + public function __construct(?string $platform = null, ?string $serverIp = null) + { + if (empty($platform)) { + $platform = Request::any('platform', null, 'string'); + if ($platform === 'pc' || stripos($platform, 'bios') !== false) { + $platform = 'PCBIOS'; + } + } + parent::__construct($platform, $serverIp, false); + } + + private function getConfCode(): string + { + if ($this->confCodeEmited) + return ''; + $this->confCodeEmited = true; + $str = ' +if ! [ "$uuid" ] ; then + smbios --type 1 --get-uuid 8 --set uuid +fi +set serverip="' . $this->getLocalIp() . '" +'; + foreach (['mac', 'ip', 'domain', 'hostname'] as $var) { + $str .= <<<EOF +if ! [ "\$$var" ] ; then + set $var="\$net_default_$var" +fi +if ! [ "\$$var" ] ; then + set $var="\$net_efinet0_dhcp_$var" +fi + +EOF; + + } + return $str; + } + + private function getLocalIp(): string + { + if (isset($_SERVER['SCRIPT_URI']) && preg_match('#^\w+://([^/]+)#', $_SERVER['SCRIPT_URI'], $out)) { + $host = $out[1]; + } elseif (isset($_SERVER['SERVER_NAME'])) { + $host = $_SERVER['SERVER_NAME']; + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $host = $_SERVER['SERVER_ADDR']; + } else { + $host = $this->serverIp; + } + return $host; + } + + private function getGrubBase(): string + { + return '(http,' . $this->getLocalIp() . ')'; + } + + private function getUrlBase(): string + { + $host = $this->getGrubBase(); + if (isset($_SERVER['REQUEST_URI'])) { + $url = parse_url($_SERVER['REQUEST_URI']); + $path = $url['path']; + } else { + // Static fallback + $path = '/boot/ipxe'; + } + return $host . $path; + + } + + private function getUrlFull(?string $key = null, ?string $value = null): string + { + $url = parse_url($_SERVER['REQUEST_URI']); + $urlbase = $this->getUrlBase(); + if (empty($url['query'])) { + $fromQuery = []; + } else { + parse_str($url['query'], $fromQuery); + foreach ($fromQuery as &$v) { + $v = urlencode($v); + } + unset($v); + } + unset($fromQuery['entryid'], $fromQuery['special'], $fromQuery['redir']); + if ($key !== null) { + $fromQuery[$key] = $value; + } + $required = [ + 'type' => 'grub', + 'uuid' => '$uuid', + 'mac' => '$mac', + 'platform' => '$grub_platform', + ]; + $fullQuery = '?'; + foreach ($required + $fromQuery as $k => $v) { // Loop instead of http_build_query since we don't want escaping for the varnames! + $fullQuery .= $k . '=' . $v . '&'; + } + return $urlbase . $fullQuery; + } + + /** + * Redirect to same URL, but add our extended params + */ + private function redirect(string $key = null, string $value = null): string + { + // Redirect to self with added parameters + $urlfull = $this->getUrlFull($key, $value); + return $this->getConfCode() . <<<HERE + +set self="${urlfull}" +echo "Chaining to \$self..." +configfile \${self}redir=1 + +HERE; + } + + /** + * Called when we handle a real client request, and don't just generate static data + * for whatever use-case that might have. In the latter case, it wouldn't make much sense + * to generate a redirect code snippet. + * + * @return string + */ + public function bootstrapLive() + { + // Check if required arguments are given; if not, spit out according script and chain to self + if ($this->uuid === null || $this->platform === '') { + // REQUIRED so we can hide incompatible entries + // but avoid redirect cycle + if (Request::any('redir', '', 'string') === '') { + return $this->redirect(); + } + } + return false; + } + + public function getBootEntry(?BootEntry $entry): string + { + if ($entry === null) { + return "echo Invalid boot entry id\nsleep --interruptible --verbose 10\n"; + } + return $entry->toScript($this); + } + + public function getMenu(IPxeMenu $menu, bool $bootstrap): string + { + $base = $this->getUrlFull(); + return $this->getConfCode() + . "set self=\"{$base}\"\n" + . $this->menuToScript($menu); + } + + public function menuToScript(IPxeMenu $menu): string + { + if ($menu->defaultEntryId === null) { + $output = <<<EOF +set timeout=0 + +EOF; + + } else { + $secs = (int)($menu->timeoutMs / 1000); + $output = <<<EOF +set timeout={$secs} +set default="id-{$menu->defaultEntryId}" + +EOF; + } + $output .= $this->getConfCode(); + foreach ($menu->items as $item) { + $output .= $this->getMenuItemScript($item); + } + return $output; + } + + private function getMenuItemScript(MenuEntry $entry): string + { + $str = "menuentry '" . str_replace("'", '', $entry->title) . "' --id 'id-" . $entry->menuentryid . "' {\n"; + if ($entry->gap) { + $str .= "true\n"; // AFAICT, not possible in GRUB + } elseif ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) { + $str .= "echo Type mismatch\n"; + } elseif ($entry->hidden && $entry->hotkey === false) { + $str .= "echo Hidden entries without hotkey are illegal\n"; // Hidden entries without hotkey are illegal + } else { + if ($entry->hotkey !== false) { + // Not supported by grub... + } + if ($entry->bootEntry instanceof MenuBootEntry) { + // Link + $str .= "configfile \${self}entryid={$entry->menuentryid}\n"; + } else { + // Embed directly + // TODO: Password. Use read etc.; might need hashsum.mod, in that case, don't embed entry directly but use configfile... + $str .= $this->getMenuEntry($entry, true); + } + } + return $str . "}\n"; + } + + public function getSpecial(string $special): string + { + if ($special === 'localboot') { + // Sync this with setup-scripts/grub_localboot occasionally... + $output = <<<'EOF' +insmod chain +if [ "$grub_platform" = "pc" ] ; then + chainloader (hd0)+1 + chainloader (hd1)+1 + chainloader (hd2)+1 +fi +insmod fat +insmod part_gpt +echo "Scanning, first pass..." +for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \ + (*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do + regexp --set=1:efi_device '^\((.*)\)/' "${efi}" +done + +echo "Scanning, second pass..." +for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \ + (*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do + if [ -e "${efi}" ]; then + #regexp --set=1:efi_device '^\((.*)\)/' "${efi}" + regexp --set=1:root '^(\(.*\))/' "${efi}" + regexp --set=1:efi_path '^\(.*\)(/.*)$' "${efi}" + echo " >> Found operating system! <<" + echo " Path: '${efi}' on '${root}'" + echo " Fallback '${efi_path}'" + chainloader "${efi}" + boot + echo " That failed..." + fi +done + +echo "No EFI known OS found. Exiting." +exit +EOF; + + } else { + $output = <<<EOF +echo "Unknown special command: $special" +sleep --interruptible --verbose 10 +EOF; + } + return $output; + } + + public function output(string $string): void + { + Header('Content-Type: text/plain; charset=UTF-8'); + echo $string; + } + + public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string + { + if ($entry === null) + return "echo Invalid menu entry id\nsleep --interruptible --verbose 10\n"; + // TODO: Check for password + if ($honorPassword && !empty($entry->md5pass)) { + return "echo TODO: Implement password check...\nsleep --interruptible --verbose 10\n"; + } + $meid = $entry->menuEntryId(); + $output = $this->getConfCode() . "set menuentryid=$meid\n"; + // Output actual entry + $output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this)); + return $output; + } + + public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string + { + if ($agnostic !== null) + return $this->execDataToScriptInternal($agnostic); + + if ($efi !== null && $this->platform === BootEntry::EFI) + return $this->execDataToScriptInternal($efi); + // Unknown or not EFI, should be BIOS at this point + return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData()); + } + + private function execDataToScriptInternal(ExecData $entry): string + { + $entry->sanitize(); + $base = $this->getGrubBase(); + $script = ''; + // Overriding dhcpOpts probably not possible/necessary + $initrds = []; + if (!empty($entry->initRd)) { + foreach ($entry->initRd as $initrd) { + if (empty($initrd)) + continue; + $initrds[] = $this->combineUrl($base, $initrd); + } + } + $file = $this->combineUrl($base, $entry->executable); + $script .= "linux $file {$entry->commandLine} slx.ipxe.id=\${menuentryid}\n"; + if (!empty($initrds)) { + $script .= "initrd " . implode(' ', $initrds) . "\n"; + } + return $script; + } + + private function combineUrl(string $base, string $path): string + { + $url = parse_url($path); + if (isset($url['host'])) { + $scheme = $url['scheme'] ?? 'http'; + $host = $url['host']; + $base = "($scheme,$host)"; + $path = $url['path'] ?? '/'; + if (isset($url['query'])) { + $path .= '?' . $url['query']; + } + } else { + if ($path[0] !== '/') { + $path = '/' . $path; + } + } + return $base . $path; + } + +} diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php new file mode 100644 index 00000000..9421684f --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php @@ -0,0 +1,478 @@ +<?php + +class ScriptBuilderIpxe extends ScriptBuilderBase +{ + + private function getUrlBase(): string + { + if (isset($_SERVER['REQUEST_URI'])) { + $url = parse_url($_SERVER['REQUEST_URI']); + if (isset($_SERVER['SCRIPT_URI']) && preg_match('#^(\w+://[^/]+)#', $_SERVER['SCRIPT_URI'], $out)) { + $urlbase = $out[1]; + } elseif (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['SERVER_NAME'])) { + $urlbase = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME']; + } elseif (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['SERVER_ADDR'])) { + $urlbase = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_ADDR']; + } else { + $urlbase = 'http://' . $this->serverIp; + } + return $urlbase . $url['path']; + } + // Static fallback + return 'http://' . $this->serverIp . '/boot/ipxe'; + + } + + private function getUrlFull(?bool &$hasExt = null, ?string $key = null, ?string $value = null): string + { + $url = parse_url($_SERVER['REQUEST_URI']); + $urlbase = $this->getUrlBase(); + if (empty($url['query'])) { + $fromQuery = []; + } else { + parse_str($url['query'], $fromQuery); + foreach ($fromQuery as &$v) { + $v = urlencode($v); + } + unset($v); + } + unset($fromQuery['entryid'], $fromQuery['special']); + if ($key !== null) { + $fromQuery[$key] = $value; + } + $hasExt = isset($fromQuery['slx-extensions']); + $required = [ + 'uuid' => '${uuid}', + 'mac' => '${mac}', + 'manuf' => '${manufacturer:uristring}', + 'product' => '${product:uristring}', + 'platform' => '${platform:uristring}', + ]; + $fullQuery = '?'; + foreach ($required + $fromQuery as $k => $v) { // Loop instead of http_build_query since we don't want escaping for the varnames! + $fullQuery .= $k . '=' . $v . '&'; + } + return $urlbase . $fullQuery; + } + + /** + * Redirect to same URL, but add our extended params + */ + private function redirect(string $key = null, string $value = null): string + { + // Redirect to self with added parameters + $urlfull = $this->getUrlFull($hasExt, $key, $value); + if ($hasExt) { + $output = "#!ipxe\nset self {$urlfull} ||\n"; + } else { + $output = <<<HERE +#!ipxe +set slxtest:string something || +iseq \${slxtest:md5} \${} && set slxext 0 || set slxext 1 || +clear slxtest || +set self {$urlfull}slx-extensions=\${slxext} || + +HERE; + } + $output .= <<<HERE +:retry +echo Chaining to \${self} +chain -ar \${self} || +echo Chaining to self failed with \${errno}, retrying in a bit... +sleep 5 +goto retry + +HERE; + return $output; + } + + /** + * Called when we handle a real client request, and don't just generate static data + * for whatever use-case that might have. In the latter case, it wouldn't make much sense + * to generate a redirect code snippet. + * @return string + */ + public function bootstrapLive() + { + // Check if required arguments are given; if not, spit out according script and chain to self + if ($this->uuid === false || $this->platform === '') { + // REQUIRED so we can hide incompatible entries + return $this->redirect(); + } + return false; + } + + public function getBootEntry(?BootEntry $entry): string + { + if ($entry === null) { + return "#!ipxe\nprompt --timeout 5000 Invalid boot entry id\n"; + } + return $entry->toScript($this); + } + + public function getMenu(IPxeMenu $menu, bool $bootstrap): string + { + if ($bootstrap) { + return "#!ipxe\nimgfree ||\n" . $this->menuToScript($menu); + } + $base = $this->getUrlFull(); + return "#!ipxe\nset self {$base} ||\n" . $this->menuToScript($menu); + } + + public function menuToScript(IPxeMenu $menu): string + { + if ($this->hasExtension) { + $slxConsoleUpdate = '--update'; + } else { + $slxConsoleUpdate = ''; + } + + $output = <<<HERE +:start + +imgstat bg-menu || imgfetch --name bg-menu /tftp/pxe-menu.png || +console --left 55 --top 88 --right 63 --bottom 64 --keep --picture bg-menu || + +colour --rgb 0xffffff 7 || +colour --rgb 0xcccccc 5 || +colour --rgb 0x000000 0 || +colour --rgb 0xdddddd 6 || +cpair --foreground 0 --background 4 1 || +cpair --foreground 0 --background 5 2 || +cpair --foreground 7 --background 9 0 || + +:slx_menu + +console --left 55 --top 88 --right 63 --bottom 64 $slxConsoleUpdate --keep --picture bg-menu || + +menu -- {$menu->title} || prompt --timeout 5000 Error creating menu || + +HERE; + foreach ($menu->items as $item) { + $output .= $this->getMenuItemScript($menu->defaultEntryId, $item); + } + if ($menu->defaultEntryId === null) { + $default = "poweroff || exit 1 ||"; + } else { + $default = "chain -a \${self}&entryid={$menu->defaultEntryId} ||"; + } + $output .= "choose"; + if ($menu->timeoutMs > 0) { + $output .= " --timeout {$menu->timeoutMs}"; + } + $output .= " selection || goto default || goto fail\n"; + $output .= <<<HERE +console --left 60 --top 130 --right 67 --bottom 86 $slxConsoleUpdate || +set slx_exit \${} || +chain -a \${self}&entryid=\${selection} || +iseq \${slx_exit} \${} || console || +iseq \${slx_exit} \${} || echo Exiting with code \${slx_exit} || +iseq \${slx_exit} \${} || exit \${slx_exit} +goto fail || goto start +goto \${target} || +echo Could not find menu entry in script. +prompt Press any key to continue. +goto start +:default +$default +:fail +prompt Boot failed. Press any key to start. +goto start + +HERE; + return $output; + } + + private function getMenuItemScript(int $requestedDefaultId, MenuEntry $entry): string + { + $str = 'item '; + if ($entry->gap) { + $str .= '--gap -- '; + } else { + if ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) + return ''; + if ($entry->hidden && $this->hasExtension) { + if ($entry->hotkey === false) + return ''; // Hidden entries without hotkey are illegal + $str .= '--hidden '; + } + if ($entry->hotkey !== false) { + $str .= '--key ' . $entry->hotkey . ' '; + } + if ($entry->menuentryid == $requestedDefaultId) { + $str .= '--default '; + } + $str .= "-- {$entry->menuentryid} "; + } + if (empty($entry->title)) { + $str .= '${}'; + } else { + $str .= $entry->title; + } + return $str . " || prompt Could not create menu item for {$entry->menuentryid}\n"; + } + + public function getSpecial(string $special): string + { + if ($special === 'localboot') { + // Get preferred localboot method, depending on system model + // Check if required arguments are given; if not, spit out according script and chain to self + // Get platform - EFI or PCBIOS + $manuf = Request::any('manuf', false, 'string'); + $product = Request::any('product', false, 'string'); + if ($this->uuid === false && $manuf === false && $product === false) { + return $this->redirect('special', 'localboot'); + } + $BOOT_METHODS = Localboot::BOOT_METHODS[$this->platform]; + $localboot = false; + $model = false; + if ($this->uuid !== false && Module::get('statistics') !== false) { + // If we have the machine table, we rather try to look up the system model from there, using the UUID + $row = Database::queryFirst('SELECT systemmodel FROM machine WHERE machineuuid = :uuid', ['uuid' => $this->uuid]); + if ($row !== false && !empty($row['systemmodel'])) { + $model = $row['systemmodel']; + } + } + if ($model === false) { + // Otherwise use what iPXE sent us + $manuf = $this->modfilt($manuf); + $product = $this->modfilt($product); + if (!empty($product)) { + $model = $product; + if (!empty($manuf)) { + $model .= " ($manuf)"; + } + $model = Util::ansiToUtf8($model); + } + } + // Query + if ($model !== false) { + $e = strtolower($this->platform); // We made sure $this->platform is either PCBIOS or EFI, so no injection possible + $row = Database::queryFirst("SELECT $e AS bootmethod FROM serversetup_localboot WHERE systemmodel = :model LIMIT 1", + ['model' => $model]); + if ($row !== false) { + $localboot = $row['bootmethod']; + } + } + if ($localboot === false || !isset($BOOT_METHODS[$localboot])) { + $localboot = Localboot::getDefault()[$this->platform]; + if (!isset($BOOT_METHODS[$localboot])) { + $localboot = array_keys($BOOT_METHODS)[0]; + } + } + // Convert to actual ipxe code + $localboot = $BOOT_METHODS[$localboot] ?? 'prompt Localboot not possible'; + $output = <<<BLA +imgfree || +console || +$localboot || goto fail + +BLA; + // + } else { + $output = "prompt --timeout 5000 Unknown special command '$special' ||\nchain -ar \${self}\n"; + } + return $output; + } + + public function output(string $string): void + { + // iPXE introduced UTF-8 support at some point in 2022, and now expects all text/script files to be + // encoded as such. Since we still offer to use older versions, we need to detect that here and handle + // all non-ASCII chars differently. + // Use 'ipxe.compile-time' instead of const from IpxeBuilder to avoid pulling in another include + if (!preg_match('/Version: (\d{4})-\d{2}-\d{2}\b/', Property::get('ipxe.compile-time'), $out) + || (int)$out[1] >= 2022) { + Header('Content-Type: text/plain; charset=UTF-8'); + echo $string; + } else { + if ($this->platform === 'EFI') { + $cs = 'ASCII'; + } else { + $cs = 'IBM437'; + } + Header('Content-Type: text/plain; charset=' . $cs); + + setlocale(LC_ALL, 'de_DE.UTF-8', 'de_DE.utf-8', 'de_DE.utf8', 'de_DE', 'de', 'German', 'ge', 'en_US.UTF-8', 'en_US.utf-8'); + echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $string); + } + } + + public function modfilt($str) + { + if (empty($str) || preg_match('/product\s+name|be\s+filled|unknown|default\s+string|system\s+model|manufacturer/i', $str)) + return false; + return trim(preg_replace('/\s+/', ' ', $str)); + } + + const PROP_PW_SALT = 'ipxe.salt.'; + + private function passwordDialog(MenuEntry $entry): string + { + if ($this->hasExtension) { + $salt = dechex(mt_rand(0x100000, 0xFFFFFF)); + Property::addToList(self::PROP_PW_SALT . $this->clientIp, $salt, 5); + return <<<HERE +set password \${} || +login --nouser || +set password \${password:md5}-{$entry->menuentryid} +set password \${password:md5}$salt +params +param pwhash \${password:md5} +chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail || + +HERE; + } + return <<<HERE +set username PASSWORD ONLY || +login || +params +param pwplain \${password} +chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail || + +HERE; + } + + public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string + { + if ($entry === null) + return "#!ipxe\nprompt --timeout 10000 Invalid menu entry id\n"; + $base = $this->getUrlBase(); + $meid = $entry->menuEntryId(); + // Make sure legacy variables are set; they might get used + $output = <<<HERE +#!ipxe +set ipappend1 ip=\${ip}:{$this->serverIp}:\${gateway}:\${netmask} +set ipappend2 BOOTIF=01-\${mac:hexhyp} +set serverip {$this->serverIp} || +iseq \${idx} \${} && set idx:string X || +iseq \${self} \${} && set self {$base}? || +set menuentryid $meid || + +HERE; + // Check for password + if ($honorPassword && !empty($entry->md5pass)) { + $pwh = Request::post('pwhash', false, 'string'); + $pwp = Request::post('pwplain', false, 'string'); + if ($pwh === false && $pwp === false) { + return $output . $this->passwordDialog($entry); + } + $ok = false; + if ($pwh !== false) { + $list = Property::getList(self::PROP_PW_SALT . $this->clientIp); + foreach ($list as $salt) { + if ($pwh === md5($entry->md5pass . $salt)) { + $ok = true; + break; + } + } + } + if (!$ok && $pwp !== false && !empty($entry->plainpass)) { + $ok = ($pwp === $entry->plainpass); + } + if (!$ok) { + return $output . "prompt --timeout 10000 Wrong password ||\n"; + } + } + // Output actual entry + $output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this)); + $output .= <<<HERE + +goto end +:fail +prompt --timeout 5000 Error launching selected boot entry || +:end + +HERE; + return $output; + } + + public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi) : string + { + if ($agnostic !== null) + return $this->execDataToScriptInternal($agnostic) . "\ngoto fail\n"; + + if (empty($this->platform)) { + // output dynamic code that decides client-side + $biosLabel = $this->getLabel(); + $output = 'iseq ${platform} efi || goto ' . $biosLabel . "\n"; + // EFI + if ($efi !== null) { + $output .= $this->execDataToScriptInternal($efi) . "\n"; + } else { + $output .= "echo EFI not supported\n"; + } + $output .= "goto fail\n" + . ':' . $biosLabel . "\n"; + if ($bios !== null) { + $output .= $this->execDataToScriptInternal($bios) . "\n"; + } else { + $output .= "echo BIOS not supported\n"; + } + return $output . "goto fail\n"; + } + // static, we know in advance + if ($efi !== null && $this->platform === BootEntry::EFI) + return $this->execDataToScriptInternal($efi) . "\ngoto fail\n"; + // Should be BIOS at this point + return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData()) . "\ngoto fail\n"; + } + + private function execDataToScriptInternal(ExecData $entry) : string + { + $entry->sanitize(); + $script = ''; + if ($entry->resetConsole) { + $script .= "console ||\n"; + } + if ($entry->imageFree) { + $script .= "imgfree ||\n"; + } + foreach ($entry->dhcpOptions as $opt) { + if (empty($opt['value'])) { + $val = '${}'; + } else { + if (empty($opt['hex'])) { + $val = bin2hex($opt['value']); + } else { + $val = $opt['value']; + } + preg_match_all('/[0-9a-f]{2}/', $val, $out); + $val = implode(':', $out[0]); + } + $script .= 'set net${idx}/' . $opt['opt'] . ':hex ' . $val + . ' || prompt Cannot override DHCP server option ' . $opt['opt'] . ". Press any key to continue anyways.\n"; + } + $initrds = []; + if (!empty($entry->initRd)) { + foreach (array_values($entry->initRd) as $i => $initrd) { + if (empty($initrd)) + continue; + $script .= "initrd --name initrd$i $initrd || goto fail\n"; + $initrds[] = "initrd$i"; + } + } + $script .= "boot "; + if ($entry->autoUnload) { + $script .= "-a "; + } + if ($entry->replace) { + $script .= "-r "; + } + $script .= $entry->executable; + if (!empty($initrds)) { + foreach ($initrds as $initrd) { + $script .= " initrd=$initrd"; + } + } + if (!empty($entry->commandLine)) { + $script .= ' ' . $entry->commandLine . ' slx.ipxe.id=${menuentryid}'; + } + $script .= " || goto fail\n"; + if ($entry->resetConsole) { + $script .= "goto start ||\n"; + } + return $script; + } + +} |