From c2a6dfb649107ce3ad64162e2bb2c3c7275650fb Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 12 Feb 2019 14:55:12 +0100 Subject: [serversetup*] PXELinux and iPXE side-by-side --- .../serversetup-bwlp-ipxe/inc/ipxe.inc.php | 453 +++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php (limited to 'modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php') diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php new file mode 100644 index 00000000..d34839f0 --- /dev/null +++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php @@ -0,0 +1,453 @@ += :start AND endaddr <= :end", compact('start', 'end')); + $locations = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($locations as &$loc) { + if ($row['startaddr'] <= $loc['startaddr'] && $row['endaddr'] >= $loc['endaddr']) { + $loc = false; + } elseif ($row['startaddr'] >= $loc['startaddr'] && $row['endaddr'] <= $loc['endaddr']) { + continue 2; + } + } + unset($loc); + $locations[] = $row; + } + $menu = PxeLinux::parsePxeLinux($content); + $key = $menu->hash(true); + if (isset($menus[$key])) { + $menuId = $menus[$key]; + $defId = null; + // Figure out the default label, get it's label name + foreach ($menu->sections as $section) { + if ($section->isDefault) { + $defId = $section; + } elseif ($defId === null && $section->label === $menu->timeoutLabel) { + $defId = $section; + } + } + if ($defId !== null) { + $defId = self::cleanLabelFixLocal($defId); + // Confirm it actually exists (it should since the menu seems identical) and get menuEntryId + $me = Database::queryFirst('SELECT m.defaultentryid, me.menuentryid FROM serversetup_bootentry be + INNER JOIN serversetup_menuentry me ON (be.entryid = me.entryid) + INNER JOIN serversetup_menu m ON (m.menuid = me.menuid) + WHERE be.entryid = :id AND me.menuid = :menuid', + ['id' => $defId, 'menuid' => $menuId]); + if ($me === false || $me['defaultentryid'] == $me['menuentryid']) { + $defId = null; // Not found, or is already default - don't override if it's the same + } else { + $defId = $me['menuentryid']; + } + } + } else { + $menuId = self::insertMenu($menu, 'Imported', false, 0, [], []); + $menus[$key] = $menuId; + $defId = null; + $importCount++; + } + if ($menuId === false) + continue; + foreach ($locations as $loc) { + if ($loc === false) + continue; + Database::exec('INSERT IGNORE INTO serversetup_menu_location (menuid, locationid, defaultentryid) + VALUES (:menuid, :locationid, :def)', [ + 'menuid' => $menuId, + 'locationid' => $loc['locationid'], + 'def' => $defId, + ]); + } + } + return $importCount; + } + + public static function importLegacyMenu($force = false) + { + if (!$force && false !== Database::queryFirst("SELECT entryid FROM serversetup_bootentry WHERE entryid = 'bwlp-default'")) + return false; // Already exists + // Now create the default entry + self::createDefaultEntries(); + $prepend = ['bwlp-default' => false, 'localboot' => false]; + $defaultLabel = 'bwlp-default'; + $menuTitle = 'bwLehrpool Bootauswahl'; + $pxeConfig = ''; + $timeoutSecs = 60; + // Try to import any customization + $oldMenu = Property::getBootMenu(); + if (is_array($oldMenu)) { + // + if (isset($oldMenu['timeout'])) { + $timeoutSecs = (int)$oldMenu['timeout']; + } + if (isset($oldMenu['defaultentry'])) { + if ($oldMenu['defaultentry'] === 'net') { + $defaultLabel = 'bwlp-default'; + } elseif ($oldMenu['defaultentry'] === 'hdd') { + $defaultLabel = 'localboot'; + } elseif ($oldMenu['defaultentry'] === 'custom') { + $defaultLabel = 'custom'; + } + } + if (!empty($oldMenu['custom'])) { + $pxeConfig = $oldMenu['custom']; + } + } + $append = [ + '', + 'bwlp-default-dbg' => false, + '', + 'poweroff' => false, + ]; + return self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append); + } + + /** + * @param PxeMenu $pxeMenu + * @param string $menuTitle + * @param string|false $defaultLabel + * @param $defaultTimeoutSeconds + * @param $prepend + * @param $append + * @return bool|int + */ + private static function insertMenu($pxeMenu, $menuTitle, $defaultLabel, $defaultTimeoutSeconds, $prepend, $append) + { + $timeoutMs = []; + $menuEntries = $prepend; + settype($menuEntries, 'array'); + if (!empty($pxeMenu)) { + $pxe =& $pxeMenu; + if (!empty($pxe->title)) { + $menuTitle = $pxe->title; + } + if ($pxe->timeoutLabel !== null) { + $defaultLabel = $pxe->timeoutLabel; + } + $timeoutMs[] = $pxe->timeoutMs; + $timeoutMs[] = $pxe->totalTimeoutMs; + foreach ($pxe->sections as $section) { + if ($section->localBoot || preg_match('/chain\.c32$/i', $section->kernel)) { + $menuEntries['localboot'] = 'localboot'; + continue; + } + if ($section->label === null) { + if (!$section->isHidden && !empty($section->title)) { + $menuEntries[] = $section->title; + } + continue; + } + if (empty($section->kernel)) { + if (!$section->isHidden && !empty($section->title)) { + $menuEntries[] = $section->title; + } + continue; + } + $entry = self::pxe2BootEntry($section); + if ($entry === null) + continue; + $label = self::cleanLabelFixLocal($section); + if ($defaultLabel === $section->label) { + $defaultLabel = $label; + } + $hotkey = MenuEntry::filterKeyName($section->hotkey); + // Create boot entry + $data = $entry->toArray(); + Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) + VALUES (:label, :hotkey, :title, 0, :data)', [ + 'label' => $label, + 'hotkey' => $hotkey, + 'title' => self::sanitizeIpxeString($section->title), + 'data' => json_encode($data), + ]); + $menuEntries[$label] = $section; + } + } + if (is_array($append)) { + $menuEntries += $append; + } + if (empty($menuEntries)) + return false; + // Make menu + $timeoutMs = array_filter($timeoutMs, 'is_int'); + if (empty($timeoutMs)) { + $timeoutMs = (int)($defaultTimeoutSeconds * 1000); + } else { + $timeoutMs = min($timeoutMs); + } + $isDefault = (int)(Database::queryFirst('SELECT menuid FROM serversetup_menu WHERE isdefault = 1') === false); + Database::exec("INSERT INTO serversetup_menu (timeoutms, title, defaultentryid, isdefault) + VALUES (:timeoutms, :title, NULL, :isdefault)", [ + 'title' => self::sanitizeIpxeString($menuTitle), + 'timeoutms' => $timeoutMs, + 'isdefault' => $isDefault, + ]); + $menuId = Database::lastInsertId(); + if (!array_key_exists($defaultLabel, $menuEntries) && $timeoutMs > 0) { + $defaultLabel = array_keys($menuEntries)[0]; + } + // Link boot entries to menu + $defaultEntryId = null; + $order = 1000; + foreach ($menuEntries as $label => $entry) { + if (is_string($entry)) { + // Gap entry + Database::exec("INSERT INTO serversetup_menuentry + (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) + VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, '', '')", [ + 'menuid' => $menuId, + 'entryid' => null, + 'hotkey' => '', + 'title' => self::sanitizeIpxeString($entry), + 'hidden' => 0, + 'sortval' => $order += 100, + ]); + continue; + } + $data = Database::queryFirst("SELECT entryid, hotkey, title FROM serversetup_bootentry WHERE entryid = :entryid", ['entryid' => $label]); + if ($data === false) + continue; + $data['pass'] = ''; + $data['hidden'] = 0; + if ($entry instanceof PxeSection) { + $data['hidden'] = (int)$entry->isHidden; + // Prefer explicit data from this imported menu over the defaults + $data['title'] = self::sanitizeIpxeString($entry->title); + if (MenuEntry::getKeyCode($entry->hotkey) !== false) { + $data['hotkey'] = $entry->hotkey; + } + if (!empty($entry->passwd)) { + // Most likely it's a hash so we cannot recover; ask people to reset + $data['pass'] ='please_reset'; + } + } + $data['menuid'] = $menuId; + $data['sortval'] = $order += 100; + $res = Database::exec("INSERT INTO serversetup_menuentry + (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) + VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, :pass, :pass)", $data); + if ($res !== false && $label === $defaultLabel) { + $defaultEntryId = Database::lastInsertId(); + } + } + // Now we can set default entry + if (!empty($defaultEntryId)) { + Database::exec("UPDATE serversetup_menu SET defaultentryid = :entryid WHERE menuid = :menuid", + ['menuid' => $menuId, 'entryid' => $defaultEntryId]); + } + // TODO: masterpw? rather pointless.... + //$oldMenu['masterpasswordclear']; + return $menuId; + } + + private static function createDefaultEntries() + { + $query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) + VALUES (:entryid, :hotkey, :title, 1, :data)'; + Database::exec($query, + [ + 'entryid' => 'bwlp-default', + 'hotkey' => 'B', + 'title' => 'bwLehrpool-Umgebung starten', + 'data' => json_encode([ + 'executable' => '/boot/default/kernel', + 'initRd' => '/boot/default/initramfs-stage31', + 'commandLine' => 'slxbase=boot/default quiet splash loglevel=5 rd.systemd.show_status=auto intel_iommu=igfx_off ${ipappend1} ${ipappend2}', + 'replace' => true, + 'autoUnload' => true, + 'resetConsole' => true, + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'bwlp-default-dbg', + 'hotkey' => '', + 'title' => 'bwLehrpool-Umgebung starten (nosplash, debug)', + 'data' => json_encode([ + 'executable' => '/boot/default/kernel', + 'initRd' => '/boot/default/initramfs-stage31', + 'commandLine' => 'slxbase=boot/default loglevel=7 intel_iommu=igfx_off ${ipappend1} ${ipappend2}', + 'replace' => true, + 'autoUnload' => true, + 'resetConsole' => true, + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'localboot', + 'hotkey' => 'L', + 'title' => 'Lokales System starten', + 'data' => json_encode([ + 'script' => 'goto slx_localboot || goto %fail% ||', + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'poweroff', + 'hotkey' => 'P', + 'title' => 'Power off', + 'data' => json_encode([ + 'script' => 'poweroff || goto %fail% ||', + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'reboot', + 'hotkey' => 'R', + 'title' => 'Reboot', + 'data' => json_encode([ + 'script' => 'reboot || goto %fail% ||', + ]), + ]); + } + + /** + * Create unique label for a boot entry. It will try to figure out whether + * this is one of our default entries and if not, create a unique label + * representing the menu entry contents. + * Also it patches the entry if it's referencing the local bwlp install + * because side effects. + * + * @param PxeSection $section + * @return string + */ + private static function cleanLabelFixLocal($section) + { + $myip = Property::getServerIp(); + // Detect our "old" entry types + if (count($section->initrd) === 1 && preg_match(",$myip/boot/default/kernel\$,", $section->kernel) + && preg_match(",$myip/boot/default/initramfs-stage31\$,", $section->initrd[0])) { + // Kernel and initrd match, examine KCL + if ($section->append === 'slxbase=boot/default vga=current quiet splash') { + // Normal + return 'bwlp-default'; + } elseif ($section->append === 'slxbase=boot/default') { + // Debug output + return 'bwlp-default-dbg'; + } else { + // Transform to relative URL, leave KCL, fall through to generic label gen + $section->kernel = '/boot/default/kernel'; + $section->initrd = ['/boot/default/initramfs-stage31']; + } + } + // Generic -- "smart" hash of kernel, initrd and command line + $str = $section->kernel . ' ' . implode(',', $section->initrd); + $array = preg_split('/\s+/', $section->append, -1, PREG_SPLIT_NO_EMPTY); + sort($array); + $str .= ' ' . implode(' ', $array); + + return 'i-' . substr(md5($str), 0, 12); + } + + /** + * @param PxeSection $section + * @return BootEntry|null The according boot entry, null if it's unparsable + */ + private static function pxe2BootEntry($section) + { + if (preg_match('/(pxechain\.com|pxechn\.c32)$/i', $section->kernel)) { + // Chaining -- create script + $args = preg_split('/\s+/', $section->append); + $script = ''; + $file = false; + for ($i = 0; $i < count($args); ++$i) { + $arg = $args[$i]; + if ($arg === '-c') { // PXELINUX config file option + ++$i; + $script .= "set 209:string {$args[$i]} || goto %fail%\n"; + } elseif ($arg === '-p') { // PXELINUX prefix path option + ++$i; + $script .= "set 210:string {$args[$i]} || goto %fail%\n"; + } elseif ($arg === '-t') { // PXELINUX timeout option + ++$i; + $script .= "set 211:int32 {$args[$i]} || goto %fail%\n"; + } elseif ($arg === '-o') { // Overriding various DHCP options + ++$i; + if (preg_match('/^((?:0x)?[a-f0-9]{1,4})\.([bwlsh])=(.*)$/i', $args[$i], $out)) { + // TODO: 'q' (8byte) unsupported for now + $opt = intval($out[1], 0); + if ($opt > 0 && $opt < 255) { + static $optType = ['b' => 'uint8', 'w' => 'uint16', 'l' => 'int32', 's' => 'string', 'h' => 'hex']; + $type = $optType[$out[2]]; + $script .= "set {$opt}:{$type} {$args[$i]} || goto %fail%\n"; + } + } + } elseif ($arg{0} === '-') { + continue; + } elseif ($file === false) { + $file = self::parseFile($arg); + } + } + if ($file !== false) { + $url = parse_url($file); + if (isset($url['host'])) { + $script .= "set next-server {$url['host']} || goto %fail%\n"; + } + if (isset($url['path'])) { + $script .= "set filename {$url['path']} || goto %fail%\n"; + } + $script .= "chain -ar {$file} || goto %fail%\n"; + return new CustomBootEntry(['script' => $script]); + } + return null; + } + // "Normal" entry that should be convertible into a StandardBootEntry + $section->kernel = self::parseFile($section->kernel); + foreach ($section->initrd as &$initrd) { + $initrd = self::parseFile($initrd); + } + return BootEntry::newStandardBootEntry($section); + } + + /** + * Parse PXELINUX file notion. Basically, turn + * server::file into tftp://server/file. + * + * @param string $file + * @return string + */ + private static function parseFile($file) + { + if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) { + return 'tftp://' . $out[1] . '/' . $out[2]; + } + return $file; + } + + public static function sanitizeIpxeString($string) + { + return str_replace(['&', '|', ';', '$', "\r", "\n"], ['+', '/', ':', 'S', ' ', ' '], $string); + } + + public static function makeMd5Pass($plainpass, $salt) + { + if (empty($plainpass)) + return ''; + return md5(md5($plainpass) . '-' . $salt); + } + +} -- cgit v1.2.3-55-g7522