From 4d2b5a5b84fd8f1ff666633e4a730133bf4f3e9e Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Fri, 23 Feb 2024 17:02:48 +0100 Subject: [serversetup-bwlp-ipxe] Add implementation of GRUB menu builder --- .../inc/scriptbuildergrub.inc.php | 330 +++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php 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 @@ +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 .= <<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() . <<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 = <<timeoutMs / 1000); + $output = <<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 = <<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; + } + +} -- cgit v1.2.3-55-g7522