diff options
author | Simon Rettberg | 2024-02-23 17:02:48 +0100 |
---|---|---|
committer | Simon Rettberg | 2024-02-23 17:02:48 +0100 |
commit | 4d2b5a5b84fd8f1ff666633e4a730133bf4f3e9e (patch) | |
tree | fba49f8906d17fa008e47c71a79af6333a6aa18e | |
parent | [serversetup-bwlp-ipxe] Add initial support for GRUB menus (diff) | |
download | slx-admin-4d2b5a5b84fd8f1ff666633e4a730133bf4f3e9e.tar.gz slx-admin-4d2b5a5b84fd8f1ff666633e4a730133bf4f3e9e.tar.xz slx-admin-4d2b5a5b84fd8f1ff666633e4a730133bf4f3e9e.zip |
[serversetup-bwlp-ipxe] Add implementation of GRUB menu builder
-rw-r--r-- | modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php | 330 |
1 files changed, 330 insertions, 0 deletions
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; + } + +} |