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] --- .../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 +- 5 files changed, 414 insertions(+), 97 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 (limited to 'modules-available/serversetup-bwlp-ipxe/inc') 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']); } -- cgit v1.2.3-55-g7522