summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2024-02-23 17:02:48 +0100
committerSimon Rettberg2024-02-23 17:02:48 +0100
commit4d2b5a5b84fd8f1ff666633e4a730133bf4f3e9e (patch)
treefba49f8906d17fa008e47c71a79af6333a6aa18e
parent[serversetup-bwlp-ipxe] Add initial support for GRUB menus (diff)
downloadslx-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.php330
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;
+ }
+
+}