From d10a3a96e0538b8347472d6c2d350dc2bee86501 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 24 Sep 2019 15:25:32 +0200 Subject: [serversetup-bwlp-ipxe] --- inc/hook.inc.php | 37 +++ .../serversetup-bwlp-ipxe/inc/bootentry.inc.php | 254 +++++++++++++-------- .../inc/bootentryhook.inc.php | 91 ++++++++ .../serversetup-bwlp-ipxe/inc/execdata.inc.php | 162 +++++++++++++ .../serversetup-bwlp-ipxe/inc/ipxemenu.inc.php | 2 +- .../serversetup-bwlp-ipxe/inc/menuentry.inc.php | 2 +- .../serversetup-bwlp-ipxe/install.inc.php | 78 ++++--- .../serversetup-bwlp-ipxe/lang/de/messages.json | 4 +- .../lang/de/template-tags.json | 5 + .../serversetup-bwlp-ipxe/lang/en/messages.json | 4 +- .../lang/en/template-tags.json | 5 + .../serversetup-bwlp-ipxe/page.inc.php | 140 +++++++----- .../templates/bootentry-list.html | 16 +- .../templates/ipxe-new-boot-entry.html | 89 +++++++- .../serversetup-bwlp-ipxe/templates/menu-edit.html | 4 +- 15 files changed, 695 insertions(+), 198 deletions(-) create mode 100644 modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php create mode 100644 modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php diff --git a/inc/hook.inc.php b/inc/hook.inc.php index bed81aeb..05078f72 100644 --- a/inc/hook.inc.php +++ b/inc/hook.inc.php @@ -27,6 +27,26 @@ class Hook return $retval; } + /** + * Load given hook for a specific module only. + * + * @param string $moduleName Module + * @param string $hookName Hook + * @param bool $filterBroken return false if the module has missing deps + * @return Hook|false hook instance, false on error or if module doesn't have given hook + */ + public static function loadSingle($moduleName, $hookName, $filterBroken = true) + { + if (Module::get($moduleName) === false) // No such module + return false; + if ($filterBroken && !Module::isAvailable($moduleName)) // Broken + return false; + $file = 'modules/' . $moduleName . '/hooks/' . $hookName . '.inc.php'; + if (!file_exists($file)) // No hook + return false; + return new Hook($moduleName, $file); + } + /* * */ @@ -40,4 +60,21 @@ class Hook $this->file = $hookFile; } + /** + * Run the hook's code. The include is expected to return a + * value, which will in turn be the return value of this + * method. + * + * @return mixed The return value of the include file, or false on error + */ + public function run() + { + try { + return (include $this->file); + } catch (Exception $e) { + error_log($e); + return false; + } + } + } diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php index 130bb52b..e97d1389 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php @@ -3,17 +3,6 @@ abstract class BootEntry { - public function __construct($data = false) - { - if (is_array($data)) { - foreach ($data as $key => $value) { - if (property_exists($this, $key)) { - $this->{$key} = $value; - } - } - } - } - public abstract function supportsMode($mode); public abstract function toScript($failLabel, $mode); @@ -29,18 +18,31 @@ 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 */ - public static function fromJson($data) + public static function fromJson($module, $data) { + if ($module{0} !== '.') { + // Hook from other module + $hook = Hook::loadSingle($module, 'ipxe-bootentry'); + if ($hook === false) { + error_log('Module ' . $module . ' doesnt have an ipxe-bootentry hook'); + return null; + } + $ret = $hook->run(); + if (!($ret instanceof BootEntryHook)) + return null; + return $ret->getBootEntry($data); + } if (is_string($data)) { $data = json_decode($data, true); } - if (isset($data['script'])) { + if ($module === '.script') { return new CustomBootEntry($data); } - if (isset($data['executable'])) { + if ($module === '.exec') { return new StandardBootEntry($data); } return null; @@ -51,9 +53,9 @@ abstract class BootEntry return new MenuBootEntry($menuId); } - public static function newStandardBootEntry($initData) + public static function newStandardBootEntry($initData, $efi = false, $arch = false) { - $ret = new StandardBootEntry($initData); + $ret = new StandardBootEntry($initData, $efi, $arch); $list = []; if ($ret->arch() !== StandardBootEntry::EFI) { $list[] = StandardBootEntry::BIOS; @@ -61,8 +63,9 @@ abstract class BootEntry if ($ret->arch() === StandardBootEntry::EFI || $ret->arch() === StandardBootEntry::BOTH) { $list[] = StandardBootEntry::EFI; } + $data = $ret->toArray(); foreach ($list as $mode) { - if (empty($ret->toArray()['executable'][$mode])) { + if (empty($data[$mode]['executable'])) { error_log('Incomplete stdbot: ' . print_r($initData, true)); return null; } @@ -85,11 +88,11 @@ abstract class BootEntry */ public static function fromDatabaseId($id) { - $row = Database::queryFirst("SELECT data FROM serversetup_bootentry + $row = Database::queryFirst("SELECT module, data FROM serversetup_bootentry WHERE entryid = :id LIMIT 1", ['id' => $id]); if ($row === false) return false; - return self::fromJson($row['data']); + return self::fromJson($row['module'], $row['data']); } /** @@ -103,7 +106,7 @@ abstract class BootEntry $res = Database::simpleQuery("SELECT entryid, data FROM serversetup_bootentry"); $ret = []; while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $tmp = self::fromJson($row['data']); + $tmp = self::fromJson($row['module'], $row['data']); if ($tmp === null) continue; $ret[$row['entryid']] = $tmp; @@ -115,12 +118,15 @@ abstract class BootEntry class StandardBootEntry extends BootEntry { - protected $executable; - protected $initRd; - protected $commandLine; - protected $replace; - protected $autoUnload; - protected $resetConsole; + /** + * @var ExecData PCBIOS boot data + */ + protected $pcbios; + /** + * @var ExecData same for EFI + */ + protected $efi; + protected $arch; // Constants below const BIOS = 'PCBIOS'; // Only valid for legacy BIOS boot @@ -128,56 +134,104 @@ class StandardBootEntry extends BootEntry const BOTH = 'PCBIOS-EFI'; // Supports both via distinct entry const AGNOSTIC = 'agnostic'; // Supports both via same entry (PCBIOS entry) - public function __construct($data = false) + const KEYS = ['executable', 'initRd', 'commandLine', 'replace', 'imageFree', 'autoUnload', 'resetConsole', 'dhcpOptions']; + + public function __construct($data, $efi = false, $arch = false) { + $this->pcbios = new ExecData(); + $this->efi = new ExecData(); if ($data instanceof PxeSection) { - // Gets arrayfied below - $this->executable = $data->kernel; - $this->initRd = [self::BIOS => $data->initrd]; - $this->commandLine = ' ' . str_replace('vga=current', '', $data->append) . ' '; - $this->resetConsole = true; - $this->replace = true; - $this->autoUnload = true; - if (strpos($this->commandLine, ' quiet ') !== false) { - $this->commandLine .= ' loglevel=5 rd.systemd.show_status=auto'; + // Import from PXELINUX menu + $this->fromPxeMenu($data); + } elseif ($data instanceof ExecData && is_string($arch)) { + if (!($efi instanceof ExecData)) { + $efi = new ExecData(); } - if ($data->ipAppend & 1) { - $this->commandLine .= ' ${ipappend1}'; + $this->pcbios = $data; + $this->efi = $efi; + $this->arch = $arch; + } elseif (is_array($data)) { + // Serialized data + if (!isset($data['arch'])) { + error_log('Serialized data to StandardBootEntry doesnt contain arch: ' . json_encode($data)); + } else { + $this->arch = $data['arch']; } - if ($data->ipAppend & 2) { - $this->commandLine .= ' ${ipappend2}'; + if (isset($data[self::BIOS]) || isset($data[self::EFI])) { + // Current format + $this->fromCurrentFormat($data); + } else { + // Convert legacy DB format + $this->fromLegacyFormat($data); } - if ($data->ipAppend & 4) { - $this->commandLine .= ' SYSUUID=${uuid}'; - } - $this->commandLine = trim(preg_replace('/\s+/', ' ', $this->commandLine)); } else { - if (isset($data['initRd']) && is_array($data['initRd'])) { - foreach ($data['initRd'] as &$initrd) { - if (is_string($initrd)) { - $initrd = preg_split('/\s*,\s*/', $initrd); - } - } - unset($initrd); + error_log('Invalid StandardBootEntry constructor call'); + } + if (!in_array($this->arch, [self::BIOS, self::EFI, self::BOTH, self::AGNOSTIC])) { + $this->arch = self::AGNOSTIC; + } + } + + private function fromLegacyFormat($data) + { + $ok = false; + foreach (self::KEYS as $key) { + if (isset($data[$key][self::BIOS])) { + $this->pcbios->{$key} = $data[$key][self::BIOS]; + $ok = true; + } + if (isset($data[$key][self::EFI])) { + $this->efi->{$key} = $data[$key][self::EFI]; + $ok = true; } - parent::__construct($data); } - // Convert legacy DB format - foreach (['executable', 'initRd', 'commandLine', 'replace', 'autoUnload', 'resetConsole'] as $key) { - if (!is_array($this->{$key})) { - $this->{$key} = [ self::BIOS => $this->{$key}, self::EFI => '' ]; + if (!$ok) { + // Very old entry + foreach (self::KEYS as $key) { + if (isset($data[$key])) { + $this->pcbios->{$key} = $data[$key]; + } } } - foreach ($this->initRd as &$initrd) { - if (!is_array($initrd)) { - $initrd = [$initrd]; + } + + private function fromCurrentFormat($data) + { + foreach (self::KEYS as $key) { + if (isset($data[self::BIOS][$key])) { + $this->pcbios->{$key} = $data[self::BIOS][$key]; + } + if (isset($data[self::EFI][$key])) { + $this->efi->{$key} = $data[self::EFI][$key]; } - $initrd = array_filter($initrd, function($x) { return strlen(trim($x)) !== 0; }); } - unset($initrd); - if ($this->arch === null) { - $this->arch = self::AGNOSTIC; + } + + /** + * @param PxeSection $data + */ + private function fromPxeMenu($data) + { + $bios = $this->pcbios; + $bios->executable = $data->kernel; + $bios->initRd = $data->initrd; + $bios->commandLine = ' ' . str_replace('vga=current', '', $data->append) . ' '; + $bios->resetConsole = true; + $bios->replace = true; + $bios->autoUnload = true; + if (strpos($bios->commandLine, ' quiet ') !== false) { + $bios->commandLine .= ' loglevel=5 rd.systemd.show_status=auto'; + } + if ($data->ipAppend & 1) { + $bios->commandLine .= ' ${ipappend1}'; } + if ($data->ipAppend & 2) { + $bios->commandLine .= ' ${ipappend2}'; + } + if ($data->ipAppend & 4) { + $bios->commandLine .= ' SYSUUID=${uuid}'; + } + $bios->commandLine = trim(preg_replace('/\s+/', ' ', $bios->commandLine)); } public function arch() @@ -201,19 +255,37 @@ class StandardBootEntry extends BootEntry if (!$this->supportsMode($mode)) { return "prompt Entry doesn't have an executable for mode $mode\n"; } - if ($this->arch === self::AGNOSTIC) { - $mode = self::BIOS; + if ($this->arch === self::AGNOSTIC || $mode == self::BIOS) { + $entry = $this->pcbios; + } else { + $entry = $this->efi; } $script = ''; - if ($this->resetConsole[$mode]) { + if ($entry->resetConsole) { $script .= "console ||\n"; } - // TODO: Checkbox - $script .= "imgfree ||\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($this->initRd[$mode])) { - foreach (array_values($this->initRd[$mode]) as $i => $initrd) { + 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"; @@ -221,23 +293,23 @@ class StandardBootEntry extends BootEntry } } $script .= "boot "; - if ($this->autoUnload[$mode]) { + if ($entry->autoUnload) { $script .= "-a "; } - if ($this->replace[$mode]) { + if ($entry->replace) { $script .= "-r "; } - $script .= $this->executable[$mode]; + $script .= $entry->executable; if (empty($initrds)) { $rdBase = ''; } else { $rdBase = " initrd=" . implode(',', $initrds); } - if (!empty($this->commandLine[$mode])) { - $script .= "$rdBase {$this->commandLine[$mode]}"; + if (!empty($entry->commandLine)) { + $script .= "$rdBase {$entry->commandLine}"; } $script .= " || goto $failLabel\n"; - if ($this->resetConsole[$mode]) { + if ($entry->resetConsole) { $script .= "goto start ||\n"; } return $script; @@ -246,30 +318,16 @@ class StandardBootEntry extends BootEntry public function addFormFields(&$array) { $array[$this->arch . '_selected'] = 'selected'; - foreach ([self::BIOS, self::EFI] as $mode) { - $array['entries'][] = [ - 'is' . $mode => true, - 'mode' => $mode, - 'executable' => $this->executable[$mode], - 'initRd' => implode(',', $this->initRd[$mode]), - 'commandLine' => $this->commandLine[$mode], - 'replace_checked' => $this->replace[$mode] ? 'checked' : '', - 'autoUnload_checked' => $this->autoUnload[$mode] ? 'checked' : '', - 'resetConsole_checked' => $this->resetConsole[$mode] ? 'checked' : '', - ]; - } + $array['entries'][] = $this->pcbios->toFormFields(self::BIOS); + $array['entries'][] = $this->efi->toFormFields(self::EFI); $array['exec_checked'] = 'checked'; } public function toArray() { return [ - 'executable' => $this->executable, - 'initRd' => $this->initRd, - 'commandLine' => $this->commandLine, - 'replace' => $this->replace, - 'autoUnload' => $this->autoUnload, - 'resetConsole' => $this->resetConsole, + self::BIOS => $this->pcbios->toArray(), + self::EFI => $this->efi->toArray(), 'arch' => $this->arch, ]; } @@ -279,6 +337,13 @@ class CustomBootEntry extends BootEntry { protected $script; + public function __construct($data) + { + if (is_array($data) && isset($data['script'])) { + $this->script = $data['script']; + } + } + public function supportsMode($mode) { return true; @@ -309,7 +374,6 @@ class MenuBootEntry extends BootEntry public function __construct($menuId) { - parent::__construct(false); $this->menuId = $menuId; } diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php new file mode 100644 index 00000000..2e2e5009 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php @@ -0,0 +1,91 @@ +groupsInternal(); + foreach ($groups as $group) { + foreach ($group->entries as $entry) { + if ($entry->id === $this->selectedId) { + $entry->selected = 'selected'; + } + } + } + return $groups; + } + + public function setSelected($id) + { + $this->selectedId = $id; + } + +} + +class HookEntryGroup +{ + /** + * @var string + */ + public $groupName; + /** + * @var HookEntry[] + */ + public $entries; + + public function __construct($groupName, $entries) + { + $this->groupName = $groupName; + $this->entries = $entries; + } +} + +class HookEntry +{ + /** + * @var string + */ + public $id; + /** + * @var string + */ + public $name; + /** + * @var string internal - to be set by ipxe module + */ + public $selected; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } +} \ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php new file mode 100644 index 00000000..b82ce2e7 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php @@ -0,0 +1,162 @@ + Value + */ + public $dhcpOptions = []; + + /** + * Supported Options + */ + const DHCP_OPTIONS = [ + 17 => [ + 'name' => 'Root Path', + 'type' => 'string', + ], + 43 => [ + 'name' => 'Vendor Specific', + 'type' => 'string', + ], + 66 => [ + 'name' => 'Next Server', + 'type' => 'string', + ], + 67 => [ + 'name' => 'Boot File', + 'type' => 'string', + ], + 209 => [ + 'name' => 'Configuration File', + 'type' => 'string', + ], + 210 => [ + 'name' => 'Path Prefix', + 'type' => 'string', + ], + ]; + + private function sanitize() + { + settype($this->executable, 'string'); + settype($this->initRd, 'array'); + foreach ($this->initRd as &$entry) { + settype($entry, 'string'); + } + settype($this->commandLine, 'string'); + settype($this->imageFree, 'bool'); + settype($this->replace, 'bool'); + settype($this->autoUnload, 'bool'); + settype($this->resetConsole, 'bool'); + settype($this->dhcpOptions, 'array'); + foreach (array_keys($this->dhcpOptions) as $key) { + $val =& $this->dhcpOptions[$key]; + if (!empty($val['override'])) { + unset($val['override']); + $val['opt'] = $key; + if (isset($val['hex']) && isset($val['value'])) { + $val['value'] = preg_replace('/[^0-9a-f]/i', '', $val['value']); + $val['value'] = substr($val['value'], 0, floor(strlen($val['value']) / 2) * 2); + $val['value'] = strtolower($val['value']); + } + } + if (!isset($val['opt']) || !is_numeric($val['opt']) || $val['opt'] <= 0 || $val['opt'] >= 255) { + unset($this->dhcpOptions[$key]); + continue; + } + if (!array_key_exists($val['opt'], self::DHCP_OPTIONS)) + continue; // Not known... + settype($val['value'], self::DHCP_OPTIONS[$val['opt']]['type']); + } + $this->dhcpOptions = array_values($this->dhcpOptions); + } + + public function toArray() + { + $this->sanitize(); + return [ + 'executable' => $this->executable, + 'initRd' => $this->initRd, + 'commandLine' => $this->commandLine, + 'imageFree' => $this->imageFree, + 'replace' => $this->replace, + 'autoUnload' => $this->autoUnload, + 'resetConsole' => $this->resetConsole, + 'dhcpOptions' => $this->dhcpOptions, + ]; + } + + public function toFormFields($arch) + { + $this->sanitize(); + $opts = []; + foreach (self::DHCP_OPTIONS as $opt => $val) { + $opts[$opt] = [ + 'opt' => $opt, + 'name' => $val['name'], + ]; + } + foreach ($this->dhcpOptions as $val) { + if (!isset($opts[$val['opt']])) { + $opts[$val['opt']] = []; + } + $opts[$val['opt']] += [ + 'opt' => $val['opt'], + 'value' => $val['value'], + 'override_checked' => 'checked', + 'hex_checked' => empty($val['hex']) ? '' : 'checked', + ]; + } + ksort($opts); + return [ + 'is' . $arch => true, + 'mode' => $arch, + 'executable' => $this->executable, + 'initRd' => implode(',', $this->initRd), + 'commandLine' => $this->commandLine, + 'imageFree_checked' => $this->imageFree ? 'checked' : '', + 'replace_checked' => $this->replace ? 'checked' : '', + 'autoUnload_checked' => $this->autoUnload ? 'checked' : '', + 'resetConsole_checked' => $this->resetConsole ? 'checked' : '', + 'opts' => array_values($opts), + ]; + } + +} \ 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 0c20e839..f87d15c2 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php @@ -26,7 +26,7 @@ class IPxeMenu $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.data AS bootentry + b.module, b.data AS bootentry FROM serversetup_menuentry e LEFT JOIN serversetup_bootentry b USING (entryid) WHERE e.menuid = :menuid diff --git a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php index eff7f24e..a65e9f98 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php @@ -48,7 +48,7 @@ class MenuEntry } $this->hotkey = self::getKeyCode($row['hotkey']); if (!empty($row['bootentry'])) { - $this->bootEntry = BootEntry::fromJson($row['bootentry']); + $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']); } elseif ($row['refmenuid'] !== null) { $this->bootEntry = BootEntry::forMenu($row['refmenuid']); } diff --git a/modules-available/serversetup-bwlp-ipxe/install.inc.php b/modules-available/serversetup-bwlp-ipxe/install.inc.php index 35eeee37..37cfc085 100644 --- a/modules-available/serversetup-bwlp-ipxe/install.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/install.inc.php @@ -1,17 +1,18 @@ fetch(PDO::FETCH_ASSOC)) { + $json = json_decode($row['data'], true); + if (isset($json['script'])) { + Database::exec("UPDATE serversetup_bootentry SET module = '.script' WHERE entryid = :id", ['id' => $row['entryid']]); + } else { + Database::exec("UPDATE serversetup_bootentry SET module = '.exec' WHERE entryid = :id", ['id' => $row['entryid']]); + } + } + } else { + $result[] = UPDATE_FAILED; + } +} + if (Module::isAvailable('serversetup')) { IPxe::createDefaultEntries(); } -responseFromArray($res); +responseFromArray($result); diff --git a/modules-available/serversetup-bwlp-ipxe/lang/de/messages.json b/modules-available/serversetup-bwlp-ipxe/lang/de/messages.json index cc8af749..339296e7 100644 --- a/modules-available/serversetup-bwlp-ipxe/lang/de/messages.json +++ b/modules-available/serversetup-bwlp-ipxe/lang/de/messages.json @@ -7,6 +7,7 @@ "import-error": "Fehler beim Importieren", "import-no-entries": "Nichts importiert: Men\u00fc scheint leer zu sein", "invalid-boot-entry": "Ung\u00fcltiger Men\u00fceintrag: {{0}}", + "invalid-custom-entry-id": "Ung\u00fcltige Eintrags-ID {{1}} f\u00fcr Modul {{0}}", "invalid-ip": "Kein Interface ist auf die Adresse {{0}} konfiguriert", "invalid-menu-id": "Ung\u00fcltige Men\u00fc-ID: {{0}}", "localboot-invalid-method": "Ung\u00fcltige localboot-Methode: {{0}}", @@ -19,5 +20,6 @@ "missing-bootentry-data": "Fehlende Daten f\u00fcr den Men\u00fceintrag", "no-ip-addr-set": "Bitte w\u00e4hlen Sie die prim\u00e4re IP-Adresse des Servers", "no-ip-set": "Kann Import alter Konfiguration nicht ausf\u00fchren. Bitte zuerst die prim\u00e4re IP-Adresse des Servers festlegen.", - "unknown-bootentry-type": "Unbekannter Eintrags-Typ: {{0}}" + "unknown-bootentry-type": "Unbekannter Eintrags-Typ: {{0}}", + "unknown-hook-module": "Unbekanntes Modul: {{0}}" } \ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/lang/de/template-tags.json b/modules-available/serversetup-bwlp-ipxe/lang/de/template-tags.json index d2277546..07cf8fa3 100644 --- a/modules-available/serversetup-bwlp-ipxe/lang/de/template-tags.json +++ b/modules-available/serversetup-bwlp-ipxe/lang/de/template-tags.json @@ -15,11 +15,13 @@ "lang_bootentryHead": "Men\u00fceintr\u00e4ge", "lang_bootentryIntro": "Hier k\u00f6nnen Sie Men\u00fceintr\u00e4ge definieren, die sich sp\u00e4ter einem Men\u00fc zuweisen lassen. Ein Men\u00fceintrag besteht entweder aus einem zu ladenden Kernel\/Image plus optional initrd, oder aus einem iPXE-Skript.", "lang_bootentryTitle": "Men\u00fceintrag", + "lang_bootentryType": "Typ", "lang_chooseIP": "Bitte w\u00e4hlen Sie die IP-Adresse, \u00fcber die der Server von den Clients zum Booten angesprochen werden soll.", "lang_commandLine": "Command line", "lang_copy": "Kopieren", "lang_count": "Anzahl", "lang_createUsbImage": "Bootbaren USB-Stick erstellen", + "lang_dhcpOverrides": "DHCP-Optionen \u00fcberschreiben", "lang_downloadBootImage": "Boot-Image herunterladen", "lang_downloadRufus": "Rufus herunterladen", "lang_editBootEntryHead": "Men\u00fceintrag bearbeiten", @@ -30,15 +32,18 @@ "lang_entryId": "ID", "lang_entryTitle": "Bezeichnung", "lang_execAutoUnload": "Nach Ausf\u00fchrung entladen (--autofree)", + "lang_execImageFree": "Andere geladene Images vor dem Ausf\u00fchren entladen (imgfree)", "lang_execReplace": "Aktuellen iPXE-Stack erstzen (--replace)", "lang_execResetConsole": "Konsole vor Ausf\u00fchrung zur\u00fccksetzen", "lang_forceRecompile": "Jetzt neu kompilieren", "lang_generationFailed": "Erzeugen des Bootmen\u00fcs fehlgeschlagen. Der Netzwerkboot von bwLehrpool wird wahrscheinlich nicht funktionieren. Wenn Sie den Fehler nicht selbst beheben k\u00f6nnen, melden Sie bitte die Logausgabe an das bwLehrpool-Projekt.", + "lang_hex": "Hex", "lang_hotkey": "Hotkey", "lang_idFormatHint": "(Max. 16 Zeichen, nur a-z 0-9 - _)", "lang_imageToLoad": "Zu ladendes Image (z.B. Kernel)", "lang_import": "Importieren", "lang_initRd": "Zu ladendes initramfs", + "lang_ipxeSettings": "iPXE-spezifische Einstellungen", "lang_ipxeWikiUrl": "im iPXE Wiki", "lang_isDefault": "Standard", "lang_listOfMenus": "Men\u00fcliste", diff --git a/modules-available/serversetup-bwlp-ipxe/lang/en/messages.json b/modules-available/serversetup-bwlp-ipxe/lang/en/messages.json index 9dafe62b..9e1c0b3e 100644 --- a/modules-available/serversetup-bwlp-ipxe/lang/en/messages.json +++ b/modules-available/serversetup-bwlp-ipxe/lang/en/messages.json @@ -7,6 +7,7 @@ "import-error": "Error importing menu", "import-no-entries": "Nothing imported: Menu seems to be empty", "invalid-boot-entry": "Invalid menu item: {{0}}", + "invalid-custom-entry-id": "Invalid entry id {{1}} for module {{0}}", "invalid-ip": "No interface is configured with the address {{0}}", "invalid-menu-id": "Invalid menu id: {{0}}", "localboot-invalid-method": "Invalid localboot method: {{0}}", @@ -19,5 +20,6 @@ "missing-bootentry-data": "Missing data for menu item", "no-ip-addr-set": "Please set the server's primary IP address", "no-ip-set": "Cannot import old configuration. Please set the primary IP address first.", - "unknown-bootentry-type": "Unknown item type: {{0}}" + "unknown-bootentry-type": "Unknown item type: {{0}}", + "unknown-hook-module": "Unknown module: {{0}}" } \ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/lang/en/template-tags.json b/modules-available/serversetup-bwlp-ipxe/lang/en/template-tags.json index 84c43dd7..29d99bf6 100644 --- a/modules-available/serversetup-bwlp-ipxe/lang/en/template-tags.json +++ b/modules-available/serversetup-bwlp-ipxe/lang/en/template-tags.json @@ -15,11 +15,13 @@ "lang_bootentryHead": "Menu items", "lang_bootentryIntro": "This is where you can add, edit and remove menu items, which can be added to menus. A menu item is either a combination of a kernel\/image to load (and an optional initrd), or a custom iPXE-script.", "lang_bootentryTitle": "Menu item", + "lang_bootentryType": "Type", "lang_chooseIP": "Please select the IP address that the client server will use to boot.", "lang_commandLine": "Command line", "lang_copy": "Copy", "lang_count": "Count", "lang_createUsbImage": "Create bootable thumb drive", + "lang_dhcpOverrides": "Override DHCP options", "lang_downloadBootImage": "Download boot-image", "lang_downloadRufus": "Download Rufus", "lang_editBootEntryHead": "Edit menu item", @@ -30,15 +32,18 @@ "lang_entryId": "ID", "lang_entryTitle": "Title", "lang_execAutoUnload": "Unload after execution (--autofree)", + "lang_execImageFree": "Unload any other images before execution (imgfree)", "lang_execReplace": "Replace current iPXE stack (--replace)", "lang_execResetConsole": "Reset console before execution", "lang_forceRecompile": "Force recompile", "lang_generationFailed": "Could not generate boot menu. The bwLehrpool-System might not work properly. If you can't fix the problem, please report the error log below to the bwLehrpool project.", + "lang_hex": "Hex", "lang_hotkey": "Hotkey", "lang_idFormatHint": "(16 chars max, a-z 0-9 - _)", "lang_imageToLoad": "Image to load (e.g. kernel)", "lang_import": "Import", "lang_initRd": "Optional initrd\/initramfs to load", + "lang_ipxeSettings": "iPXE-specific settings", "lang_ipxeWikiUrl": "at the iPXE wiki", "lang_isDefault": "Default", "lang_listOfMenus": "Menulist", diff --git a/modules-available/serversetup-bwlp-ipxe/page.inc.php b/modules-available/serversetup-bwlp-ipxe/page.inc.php index 6a874775..40b75dd2 100644 --- a/modules-available/serversetup-bwlp-ipxe/page.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/page.inc.php @@ -228,7 +228,7 @@ class Page_ServerSetup extends Page if (is_file($file)) { $base = basename($file); $features = []; - foreach (preg_split('/[\-\.\/]+/', $base, -1, PREG_SPLIT_NO_EMPTY) as $p) { + foreach (preg_split('/[\-.\/]+/', $base, -1, PREG_SPLIT_NO_EMPTY) as $p) { if (array_key_exists($p, $strings)) { $features += $strings[$p]; } @@ -298,14 +298,11 @@ class Page_ServerSetup extends Page { $allowEdit = User::hasPermission('ipxe.bootentry.edit'); - $res = Database::simpleQuery("SELECT be.entryid, be.hotkey, be.title, be.builtin, Count(sm.menuid) AS refs FROM serversetup_bootentry be + $bootentryTable = Database::queryAll("SELECT be.entryid, be.hotkey, be.title, be.builtin, be.module, Count(sm.menuid) AS refs + FROM serversetup_bootentry be LEFT JOIN serversetup_menuentry sm USING (entryid) - GROUP BY be.entryid + GROUP BY be.entryid, be.title ORDER BY be.title ASC"); - $bootentryTable = []; - while ($row = $res->fetch(PDO::FETCH_ASSOC)) { - $bootentryTable[] = $row; - } if (empty($bootentryTable)) { if (Property::getServerIp() === false || Property::getServerIp() === 'invalid') { @@ -339,7 +336,7 @@ class Page_ServerSetup extends Page FROM serversetup_menu m LEFT JOIN serversetup_menu_location l USING (menuid) LEFT JOIN location ll USING (locationid) - GROUP BY menuid + GROUP BY menuid, title ORDER BY title"); $menuTable = []; while ($row = $res->fetch(PDO::FETCH_ASSOC)) { @@ -420,31 +417,22 @@ class Page_ServerSetup extends Page } $menu['keys'] = array_map(function ($item) { return ['key' => $item]; }, MenuEntry::getKeyList()); $menu['entrylist'] = array_merge( - Database::queryAll("SELECT entryid, title, hotkey, data FROM serversetup_bootentry ORDER BY title ASC"), + Database::queryAll("SELECT entryid, title, hotkey, module, data FROM serversetup_bootentry ORDER BY title ASC"), // Add all menus, so we can link Database::queryAll("SELECT Concat('menu:', menuid) AS entryid, title FROM serversetup_menu ORDER BY title ASC") ); - class_exists('BootEntry'); // Leave this here for StandardBootEntry foreach ($menu['entrylist'] as &$bootentry) { - if (!isset($bootentry['data'])) + if (!isset($bootentry['data']) || !isset($bootentry['module']) || $bootentry['module']{0} !== '.') + continue; + $entry = BootEntry::fromJson($bootentry['module'], $bootentry['data']); + if ($entry === null) { + error_log('WARNING: Ignoring NULL menu entry: ' . $bootentry['data']); continue; - $bootentry['data'] = json_decode($bootentry['data'], true); + } + $bootentry['data'] = $entry->toArray(); // Transform stuff suitable for mustache if (!array_key_exists('arch', $bootentry['data'])) continue; - // BIOS/EFI or both - if ($bootentry['data']['arch'] === StandardBootEntry::BIOS - || $bootentry['data']['arch'] === StandardBootEntry::BOTH) { - $bootentry['data']['PCBIOS'] = array('executable' => $bootentry['data']['executable']['PCBIOS'], - 'initRd' => $bootentry['data']['initRd']['PCBIOS'], - 'commandLine' => $bootentry['data']['commandLine']['PCBIOS']); - } - if ($bootentry['data']['arch'] === StandardBootEntry::EFI - || $bootentry['data']['arch'] === StandardBootEntry::BOTH) { - $bootentry['data']['EFI'] = array('executable' => $bootentry['data']['executable']['EFI'], - 'initRd' => $bootentry['data']['initRd']['EFI'], - 'commandLine' => $bootentry['data']['commandLine']['EFI']); - } // Naming and agnostic if ($bootentry['data']['arch'] === StandardBootEntry::BIOS) { $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_biosOnly', true); @@ -453,16 +441,13 @@ class Page_ServerSetup extends Page $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_efiOnly', true); unset($bootentry['data']['PCBIOS']); } elseif ($bootentry['data']['arch'] === StandardBootEntry::AGNOSTIC) { - $bootentry['data']['archAgnostic'] = array('executable' => $bootentry['data']['executable']['PCBIOS'], - 'initRd' => $bootentry['data']['initRd']['PCBIOS'], - 'commandLine' => $bootentry['data']['commandLine']['PCBIOS']); $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_archAgnostic', true); unset($bootentry['data']['EFI']); } else { $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_archBoth', true); } foreach ($bootentry['data'] as &$e) { - if (isset($e['initRd']) && is_array($e['initRd'])) { + if (isset($e['initRd'])) { $e['initRd'] = implode(',', $e['initRd']); } } @@ -478,29 +463,57 @@ class Page_ServerSetup extends Page private function showEditBootEntry() { - $params = []; + $params = ['hooks' => []]; + foreach (Hook::load('ipxe-bootentry') as $hook) { + $var = $hook->run(); + if ($var instanceof BootEntryHook) { + $var->moduleId = $hook->moduleId; + $params['hooks'][] = $var; + } + } $id = Request::get('id', false, 'string'); if ($id === false) { $params['exec_checked'] = 'checked'; $params['entryid'] = 'u-' . dechex(mt_rand(0x1000, 0xffff)) . '-' . dechex(time()); - $params['entries'] = [ - ['mode' => 'PCBIOS'], - ['mode' => 'EFI'], - ]; } else { // Query existing entry - $row = Database::queryFirst('SELECT entryid, title, builtin, data FROM serversetup_bootentry + $row = Database::queryFirst('SELECT entryid, title, builtin, module, data FROM serversetup_bootentry WHERE entryid = :id LIMIT 1', ['id' => $id]); if ($row === false) { Message::addError('invalid-boot-entry', $id); Util::redirect('?do=serversetup'); } - $entry = BootEntry::fromJson($row['data']); - if ($entry === null) { - Message::addError('unknown-bootentry-type', $id); - Util::redirect('?do=serversetup'); + if ($row['module']{0} === '.') { + // either script or exec entry + $json = json_decode($row['data'], true); + if (!is_array($json)) { + Message::addError('unknown-bootentry-type', $id); + Util::redirect('?do=serversetup&show=bootentry'); + } + $entry = BootEntry::fromJson($row['module'], $json); + if ($entry === null) { + Message::addError('unknown-bootentry-type', $id); + Util::redirect('?do=serversetup&show=bootentry'); + } + $entry->addFormFields($params); + } else { + // Hook from another module + if (Module::get($row['module']) === false) { + Message::addError('unknown-hook-module', $row['module']); + } else { + foreach ($params['hooks'] as $he) { + /** @var BootEntryHook $he */ + if ($he->moduleId === $row['module']) { + $he->setSelected($row['data']); + $he->checked = 'checked'; + if ($he->getBootEntry($row['data']) === null) { + Message::addError('invalid-custom-entry-id', $row['module'], $row['data']); + } + break; + } + } + } } - $entry->addFormFields($params); $params['title'] = $row['title']; if (!Request::get('copy')) { $params['oldentryid'] = $params['entryid'] = $row['entryid']; @@ -787,34 +800,53 @@ class Page_ServerSetup extends Page Message::addError('missing-bootentry-data'); return; } + $module = false; $type = Request::post('type', false, 'string'); - if ($type === 'exec') { - $entry = BootEntry::newStandardBootEntry($data); - } elseif ($type === 'script') { - $entry = BootEntry::newCustomBootEntry($data); + if ($type{0} === '.') { + // Exec or script + if ($type === '.exec') { + $entry = BootEntry::newStandardBootEntry($data); + } elseif ($type === '.script') { + $entry = BootEntry::newCustomBootEntry($data); + } + if ($entry === null) { + Message::addError('main.empty-field'); + Util::redirect('?do=serversetup&show=bootentry'); + } + $entryData = json_encode($entry->toArray()); } else { - Message::addError('unknown-bootentry-type', $type); - return; - } - if ($entry === null) { - Message::addError('main.empty-field'); - Util::redirect('?do=serversetup&show=bootentry'); + // Module hook + $hook = Hook::loadSingle($type, 'ipxe-bootentry'); + if ($hook === false) { + Message::addError('unknown-bootentry-type', $type); + return; + } + /** @var BootEntryHook $module */ + $module = $hook->run(); + $entryData = Request::post('selection-' . $type, false, 'string'); + $entry = $module->getBootEntry($entryData); + if ($entry === null) { + Message::addError('invalid-custom-entry-id', $type, $entryData); + return; + } } $params = [ 'entryid' => $newId, 'title' => Request::post('title', '', 'string'), - 'data' => json_encode($entry->toArray()), + 'module' => $type, + 'data' => $entryData, ]; // New or update? if (empty($oldEntryId)) { // New entry - Database::exec('INSERT INTO serversetup_bootentry (entryid, title, builtin, data) - VALUES (:entryid, :title, 0, :data)', $params); + Database::exec('INSERT INTO serversetup_bootentry (entryid, title, builtin, module, data) + VALUES (:entryid, :title, 0, :module, :data)', $params); Message::addSuccess('boot-entry-created', $newId); } else { // Edit existing entry $params['oldid'] = $oldEntryId; - Database::exec('UPDATE serversetup_bootentry SET entryid = If(builtin = 0, :entryid, entryid), title = :title, data = :data + Database::exec('UPDATE serversetup_bootentry SET + entryid = If(builtin = 0, :entryid, entryid), title = :title, module = :module, data = :data WHERE entryid = :oldid', $params); Message::addSuccess('boot-entry-updated', $newId); } diff --git a/modules-available/serversetup-bwlp-ipxe/templates/bootentry-list.html b/modules-available/serversetup-bwlp-ipxe/templates/bootentry-list.html index dfc4e6a8..9eecc6f5 100644 --- a/modules-available/serversetup-bwlp-ipxe/templates/bootentry-list.html +++ b/modules-available/serversetup-bwlp-ipxe/templates/bootentry-list.html @@ -11,20 +11,24 @@ {{lang_entryId}} + {{lang_bootentryType}} {{lang_bootentryTitle}} - {{lang_hotkey}} - {{lang_refCount}} - {{lang_edit}} - {{#allowEdit}}{{lang_copy}}{{/allowEdit}} - {{lang_delete}} + {{lang_hotkey}} + {{lang_refCount}} + {{lang_edit}} + {{#allowEdit}}{{lang_copy}}{{/allowEdit}} + {{lang_delete}} {{#bootentryTable}} - + {{entryid}} + + {{module}} + {{title}} diff --git a/modules-available/serversetup-bwlp-ipxe/templates/ipxe-new-boot-entry.html b/modules-available/serversetup-bwlp-ipxe/templates/ipxe-new-boot-entry.html index b195394d..fd76c1d5 100644 --- a/modules-available/serversetup-bwlp-ipxe/templates/ipxe-new-boot-entry.html +++ b/modules-available/serversetup-bwlp-ipxe/templates/ipxe-new-boot-entry.html @@ -23,13 +23,19 @@
- +
- +
+ {{#hooks}} +
+ + +
+ {{/hooks}}
@@ -68,42 +74,77 @@ - +
- +
-
+

{{lang_ipxeSettings}}

+
+
+ + +
+
+ name="entry[{{mode}}][replace]" {{replace_checked}} {{disabled}}>
+ name="entry[{{mode}}][autoUnload]" {{autoUnload_checked}} {{disabled}}>
+ name="entry[{{mode}}][resetConsole]" {{resetConsole_checked}} {{disabled}}>
+

{{lang_dhcpOverrides}}

+ {{#opts}} +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ {{/opts}} @@ -121,6 +162,23 @@ + {{#hooks}} +
+ +
+ +
+
+ {{/hooks}} + {{#builtin}}
{{lang_editBuiltinWarn}} @@ -148,7 +206,8 @@ document.addEventListener('DOMContentLoaded', function () { $('.type-radio').click(function () { $('.type-form').hide(); - $('#form-' + $(this).val()).show(); + var name = $(this).val().replace('.', ''); + $('#form-' + name).show(); }); $('.type-radio[checked]').click(); var $as = $('#arch-selector'); @@ -167,5 +226,17 @@ document.addEventListener('DOMContentLoaded', function () { $('#col-' + vs[i]).attr('class', 'mode-class col-md-' + cols).show(); } }).change(); + var colorize = function() { + var $t = $(this); + $t.css('color', ($t.data('hex') && !$t.val().match(/^[a-f0-9]*$/i)) ? 'red' : ''); + }; + var setHex = function() { + var n = '#' + this.id.replace(/-hex$/, '-value'); + var $obj = $(n); + $obj.data('hex', this.checked); + colorize.call($obj[0]); + }; + $('.hex-box').change(setHex).each(setHex); + $('.hex-value').change(colorize).keyup(colorize).each(colorize); }); // --> \ No newline at end of file diff --git a/modules-available/serversetup-bwlp-ipxe/templates/menu-edit.html b/modules-available/serversetup-bwlp-ipxe/templates/menu-edit.html index 358aa4d6..d2fa12be 100644 --- a/modules-available/serversetup-bwlp-ipxe/templates/menu-edit.html +++ b/modules-available/serversetup-bwlp-ipxe/templates/menu-edit.html @@ -150,10 +150,12 @@
{{/script}} {{^script}} + {{#arch}}
-
{{arch}}
+
{{.}}
+ {{/arch}} {{#archAgnostic}}
-- cgit v1.2.3-55-g7522