summaryrefslogtreecommitdiffstats
path: root/modules-available/serversetup-bwlp-ipxe/inc
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/serversetup-bwlp-ipxe/inc')
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php258
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php453
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php142
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php15
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php177
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php302
6 files changed, 1347 insertions, 0 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
new file mode 100644
index 00000000..69adffd3
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
@@ -0,0 +1,258 @@
+<?php
+
+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);
+
+ public abstract function toArray();
+
+ public abstract function addFormFields(&$array);
+
+ /*
+ *
+ */
+
+ /**
+ * Return a BootEntry instance from the serialized data.
+ *
+ * @param string $jsonString serialized entry data
+ * @return BootEntry|null instance representing boot entry, null on error
+ */
+ public static function fromJson($data)
+ {
+ if (is_string($data)) {
+ $data = json_decode($data, true);
+ }
+ if (isset($data['script'])) {
+ return new CustomBootEntry($data);
+ }
+ if (isset($data['executable'])) {
+ return new StandardBootEntry($data);
+ }
+ return null;
+ }
+
+ public static function newStandardBootEntry($initData)
+ {
+ $ret = new StandardBootEntry($initData);
+ $list = [];
+ if ($ret->arch() !== StandardBootEntry::EFI) {
+ $list[] = StandardBootEntry::BIOS;
+ }
+ if ($ret->arch() === StandardBootEntry::EFI || $ret->arch() === StandardBootEntry::BOTH) {
+ $list[] = StandardBootEntry::EFI;
+ }
+ foreach ($list as $mode) {
+ if (empty($initData['executable'][$mode]))
+ return null;
+ }
+ return $ret;
+ }
+
+ public static function newCustomBootEntry($initData)
+ {
+ if (empty($initData['script']))
+ return null;
+ return new CustomBootEntry($initData);
+ }
+
+ /**
+ * Return a BootEntry instance from database with the given id.
+ *
+ * @param string $id
+ * @return BootEntry|null|false false == unknown id, null = unknown entry type, BootEntry instance on success
+ */
+ public static function fromDatabaseId($id)
+ {
+ $row = Database::queryFirst("SELECT data FROM serversetup_bootentry
+ WHERE entryid = :id LIMIT 1", ['id' => $id]);
+ if ($row === false)
+ return false;
+ return self::fromJson($row['data']);
+ }
+
+}
+
+class StandardBootEntry extends BootEntry
+{
+ protected $executable;
+ protected $initRd;
+ protected $commandLine;
+ protected $replace;
+ protected $autoUnload;
+ protected $resetConsole;
+ protected $arch; // Constants below
+
+ const BIOS = 'PCBIOS'; // Only valid for legacy BIOS boot
+ const EFI = 'EFI'; // Only valid for EFI boot
+ const BOTH = 'PCBIOS-EFI'; // Supports both via distinct entry
+ const AGNOSTIC = 'agnostic'; // Supports both via same entry (PCBIOS entry)
+
+ public function __construct($data = false)
+ {
+ if ($data instanceof PxeSection) {
+ // Gets arrayfied below
+ $this->executable = $data->kernel;
+ $this->initRd = $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';
+ }
+ if ($data->ipAppend & 1) {
+ $this->commandLine .= ' ${ipappend1}';
+ }
+ if ($data->ipAppend & 2) {
+ $this->commandLine .= ' ${ipappend2}';
+ }
+ if ($data->ipAppend & 4) {
+ $this->commandLine .= ' SYSUUID=${uuid}';
+ }
+ $this->commandLine = trim(preg_replace('/\s+/', ' ', $this->commandLine));
+ } else {
+ parent::__construct($data);
+ }
+ // Convert legacy DB format
+ foreach (['executable', 'initRd', 'commandLine', 'replace', 'autoUnload', 'resetConsole'] as $key) {
+ if (!is_array($this->{$key})) {
+ $this->{$key} = [ 'PCBIOS' => $this->{$key}, 'EFI' => '' ];
+ }
+ }
+ if ($this->arch === null) {
+ $this->arch = self::AGNOSTIC;
+ }
+ }
+
+ public function arch()
+ {
+ return $this->arch;
+ }
+
+ public function supportsMode($mode)
+ {
+ if ($mode === $this->arch || $this->arch === self::AGNOSTIC)
+ return true;
+ if ($mode === self::BIOS || $mode === self::EFI) {
+ return $this->arch === self::BOTH;
+ }
+ error_log('Unknown iPXE platform: ' . $mode);
+ return false;
+ }
+
+ public function toScript($failLabel, $mode)
+ {
+ if (!$this->supportsMode($mode)) {
+ return "prompt Entry doesn't have an executable for mode $mode\n";
+ }
+ if ($this->arch === self::AGNOSTIC) {
+ $mode = self::BIOS;
+ }
+
+ $script = '';
+ if ($this->resetConsole[$mode]) {
+ $script .= "console ||\n";
+ }
+ if (!empty($this->initRd[$mode])) {
+ $script .= "imgfree ||\n";
+ if (!is_array($this->initRd[$mode])) {
+ $script .= "initrd {$this->initRd[$mode]} || goto $failLabel\n";
+ } else {
+ foreach ($this->initRd[$mode] as $initrd) {
+ $script .= "initrd $initrd || goto $failLabel\n";
+ }
+ }
+ }
+ $script .= "boot ";
+ if ($this->autoUnload[$mode]) {
+ $script .= "-a ";
+ }
+ if ($this->replace[$mode]) {
+ $script .= "-r ";
+ }
+ $script .= $this->executable[$mode];
+ $rdBase = basename($this->initRd[$mode]);
+ if (!empty($this->commandLine[$mode])) {
+ $script .= " initrd=$rdBase {$this->commandLine[$mode]}";
+ }
+ $script .= " || goto $failLabel\n";
+ if ($this->resetConsole[$mode]) {
+ $script .= "goto start ||\n";
+ }
+ return $script;
+ }
+
+ 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' => $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['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,
+ 'arch' => $this->arch,
+ ];
+ }
+}
+
+class CustomBootEntry extends BootEntry
+{
+ protected $script;
+
+ public function supportsMode($mode)
+ {
+ return true;
+ }
+
+ public function toScript($failLabel, $mode)
+ {
+ return str_replace('%fail%', $failLabel, $this->script) . "\n";
+ }
+
+ public function addFormFields(&$array)
+ {
+ $array['entry'] = [
+ 'script' => $this->script,
+ ];
+ $array['script_checked'] = 'checked';
+ }
+
+ public function toArray()
+ {
+ return ['script' => $this->script];
+ }
+}
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 @@
+<?php
+
+class IPxe
+{
+
+ /**
+ * Import all IP-Range based pxe menus from the given directory.
+ *
+ * @param string $configPath The pxelinux.cfg path where to look for menu files in hexadecimal IP format.
+ * @return Number of menus imported
+ */
+ public static function importPxeMenus($configPath)
+ {
+ $importCount = 0;
+ $menus = [];
+ foreach (glob($configPath . '/*', GLOB_NOSORT) as $file) {
+ if (!is_file($file) || !preg_match('~/[A-F0-9]{1,8}$~', $file))
+ continue;
+ $content = file_get_contents($file);
+ if ($content === false)
+ continue;
+ $file = basename($file);
+ $start = hexdec(str_pad($file,8, '0'));
+ $end = hexdec(str_pad($file,8, 'f')); // TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^PREFIX
+ error_log('From ' . long2ip($start) . ' to ' . long2ip($end));
+ $res = Database::simpleQuery("SELECT locationid, startaddr, endaddr FROM subnet
+ WHERE startaddr >= :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);
+ }
+
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
new file mode 100644
index 00000000..5c1a87d5
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
@@ -0,0 +1,142 @@
+<?php
+
+class IPxeMenu
+{
+
+ protected $menuid;
+ protected $timeoutMs;
+ protected $title;
+ protected $defaultEntryId;
+ /**
+ * @var MenuEntry[]
+ */
+ protected $items = [];
+
+ public function __construct($menu)
+ {
+ if (!is_array($menu)) {
+ $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu
+ WHERE menuid = :menuid LIMIT 1", ['menuid' => $menu]);
+ if (!is_array($menu)) {
+ $menu = ['menuid' => 'foo', 'title' => 'Invalid Menu ID: ' . (int)$menu];
+ }
+ }
+ $this->menuid = (int)$menu['menuid'];
+ $this->timeoutMs = (int)$menu['timeoutms'];
+ $this->title = $menu['title'];
+ $this->defaultEntryId = $menu['defaultentryid'];
+ $res = Database::simpleQuery("SELECT e.menuentryid, e.entryid, e.hotkey, e.title, e.hidden, e.sortval, e.md5pass,
+ b.data AS bootentry
+ FROM serversetup_menuentry e
+ LEFT JOIN serversetup_bootentry b USING (entryid)
+ WHERE e.menuid = :menuid
+ ORDER BY e.sortval ASC, e.title ASC", ['menuid' => $menu['menuid']]);
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $this->items[] = new MenuEntry($row);
+ }
+ }
+
+ public function getMenuDefinition($targetVar, $mode, $slxExtensions)
+ {
+ $str = "menu -- {$this->title}\n";
+ foreach ($this->items as $item) {
+ $str .= $item->getMenuItemScript("m_{$this->menuid}", $this->defaultEntryId, $mode, $slxExtensions);
+ }
+ if ($this->defaultEntryId === null) {
+ $defaultLabel = "mx_{$this->menuid}_poweroff";
+ } else {
+ $defaultLabel = "m_{$this->menuid}_{$this->defaultEntryId}";
+ }
+ $str .= "choose";
+ if ($this->timeoutMs > 0) {
+ $str .= " --timeout {$this->timeoutMs}";
+ }
+ $str .= " $targetVar || goto $defaultLabel || goto fail\n";
+ if ($this->defaultEntryId === null) {
+ $str .= "goto skip_{$defaultLabel}\n"
+ . ":{$defaultLabel}\n"
+ . "poweroff || goto fail\n"
+ . ":skip_{$defaultLabel}\n";
+ }
+ return $str;
+ }
+
+ public function getItemsCode($mode)
+ {
+ $str = '';
+ foreach ($this->items as $item) {
+ $str .= $item->getBootEntryScript("m_{$this->menuid}", 'fail', $mode);
+ $str .= "goto slx_menu\n";
+ }
+ return $str;
+ }
+
+ /*
+ *
+ */
+
+ public static function forLocation($locationId)
+ {
+ $chain = null;
+ if (Module::isAvailable('location')) {
+ $chain = Location::getLocationRootChain($locationId);
+ }
+ if (!empty($chain)) {
+ $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid
+ FROM serversetup_menu m
+ INNER JOIN serversetup_menu_location ml USING (menuid)
+ WHERE ml.locationid IN (:chain)", ['chain' => $chain]);
+ if ($res->rowCount() > 0) {
+ // Make the location id key, preserving order (closest location is first)
+ $chain = array_flip($chain);
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ // Overwrite the value (numeric ascending values, useless) with menu array of according location
+ $chain[(int)$row['locationid']] = $row;
+ }
+ // Use first one that was found
+ foreach ($chain as $menu) {
+ if (is_array($menu)) {
+ return new IPxeMenu($menu);
+ }
+ }
+ // Should never end up here, but we'd just fall through and use the default
+ }
+ }
+ // We're here, no specific menu, use default
+ $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid
+ FROM serversetup_menu
+ ORDER BY isdefault DESC LIMIT 1");
+ if ($menu === false) {
+ return new EmptyIPxeMenu;
+ }
+ return new IPxeMenu($menu);
+ }
+
+ public static function forClient($ip, $uuid)
+ {
+ $locationId = 0;
+ if (Module::isAvailable('location')) {
+ $locationId = Location::getFromIpAndUuid($ip, $uuid);
+ }
+ return self::forLocation($locationId);
+ }
+
+}
+
+class EmptyIPxeMenu extends IPxeMenu
+{
+
+ /** @noinspection PhpMissingParentConstructorInspection */
+ public function __construct()
+ {
+ $this->title = 'No menu defined';
+ $this->menuid = -1;
+ $this->items[] = new MenuEntry([
+ 'title' => 'Please create a menu in Server-Setup first'
+ ]);
+ $this->items[] = new MenuEntry([
+ 'title' => 'Bitte erstellen Sie zunächst ein Menü'
+ ]);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
new file mode 100644
index 00000000..3ab81862
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
@@ -0,0 +1,15 @@
+<?php
+
+class Localboot
+{
+
+ const PROPERTY_KEY = 'serversetup.localboot';
+
+ const BOOT_METHODS = [
+ 'AUTO' => 'iseq efi ${platform} && exit 1 || sanboot --no-describe',
+ 'EXIT' => 'exit 1',
+ 'COMBOOT' => 'chain /tftp/chain.c32 hd0',
+ 'SANBOOT' => 'sanboot --no-describe',
+ ];
+
+} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
new file mode 100644
index 00000000..d243fd23
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
@@ -0,0 +1,177 @@
+<?php
+
+class MenuEntry
+{
+ /**
+ * @var int id of entry, used for pw
+ */
+ private $menuentryid;
+ /**
+ * @var false|string key code as expected by iPXE
+ */
+ private $hotkey;
+ /**
+ * @var string
+ */
+ private $title;
+ /**
+ * @var bool
+ */
+ private $hidden;
+ /**
+ * @var bool
+ */
+ private $gap;
+ /**
+ * @var int
+ */
+ private $sortval;
+ /**
+ * @var BootEntry
+ */
+ private $bootEntry = null;
+
+ private $md5pass = null;
+
+ /**
+ * MenuEntry constructor.
+ *
+ * @param array $row row from database
+ */
+ public function __construct($row)
+ {
+ if (is_array($row)) {
+ foreach ($row as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->{$key} = $value;
+ }
+ }
+ $this->hotkey = self::getKeyCode($row['hotkey']);
+ if (!empty($row['bootentry'])) {
+ $this->bootEntry = BootEntry::fromJson($row['bootentry']);
+ }
+ $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null);
+ }
+ settype($this->hidden, 'bool');
+ settype($this->gap, 'bool');
+ settype($this->sortval, 'int');
+ settype($this->menuentryid, 'int');
+ }
+
+ public function getMenuItemScript($lblPrefix, $requestedDefaultId, $mode, $slxExtensions)
+ {
+ if ($this->bootEntry !== null && !$this->bootEntry->supportsMode($mode))
+ return '';
+ $str = 'item ';
+ if ($this->gap) {
+ $str .= '--gap ';
+ } else {
+ if ($this->hidden && $slxExtensions) {
+ if ($this->hotkey === false)
+ return ''; // Hidden entries without hotkey are illegal
+ $str .= '--hidden ';
+ }
+ if ($this->hotkey !== false) {
+ $str .= '--key ' . $this->hotkey . ' ';
+ }
+ if ($this->menuentryid == $requestedDefaultId) {
+ $str .= '--default ';
+ }
+ $str .= "{$lblPrefix}_{$this->menuentryid} ";
+ }
+ if (empty($this->title)) {
+ $str .= '${}';
+ } else {
+ $str .= $this->title;
+ }
+ return $str . " || prompt Could not create menu item for {$lblPrefix}_{$this->menuentryid}\n";
+ }
+
+ public function getBootEntryScript($lblPrefix, $failLabel, $mode)
+ {
+ if ($this->bootEntry === null || !$this->bootEntry->supportsMode($mode))
+ return '';
+ $str = ":{$lblPrefix}_{$this->menuentryid}\n";
+ if (!empty($this->md5pass)) {
+ $str .= "set slx_hash {$this->md5pass} || goto $failLabel\n"
+ . "set slx_salt {$this->menuentryid} || goto $failLabel\n"
+ . "set slx_pw_ok {$lblPrefix}_ok || goto $failLabel\n"
+ . "set slx_pw_fail slx_menu || goto $failLabel\n"
+ . "goto slx_pass_check || goto $failLabel\n"
+ . ":{$lblPrefix}_ok\n";
+ }
+ return $str . $this->bootEntry->toScript($failLabel, $mode);
+ }
+
+ /*
+ *
+ */
+
+ private static function getKeyArray()
+ {
+ static $data = false;
+ if ($data === false) {
+ $data = [
+ 'F5' => 0x107e,
+ 'F6' => 0x127e,
+ 'F7' => 0x137e,
+ 'F8' => 0x147e,
+ 'F9' => 0x157e,
+ 'F10' => 0x167e,
+ 'F11' => 0x187e,
+ 'F12' => 0x197e,
+ ];
+ for ($i = 1; $i <= 26; ++$i) {
+ $letter = chr(0x40 + $i);
+ $data['SHIFT_' . $letter] = 0x40 + $i;
+ if ($letter !== 'C') {
+ $data['CTRL_' . $letter] = $i;
+ }
+ $data[$letter] = 0x60 + $i;
+ }
+ for ($i = 0; $i <= 9; ++$i) {
+ $data[chr(0x30 + $i)] = 0x30 + $i;
+ }
+ asort($data, SORT_NUMERIC);
+ }
+ return $data;
+ }
+
+ /**
+ * Get all the known/supported keys, usable for menu items.
+ *
+ * @return string[] list of known key names
+ */
+ public static function getKeyList()
+ {
+ return array_keys(self::getKeyArray());
+ }
+
+ /**
+ * Get the key code ipxe expects for the given named
+ * key. Returns false if the key name is unknown.
+ *
+ * @param string $keyName
+ * @return false|string Key code as hex string, or false if not found
+ */
+ public static function getKeyCode($keyName)
+ {
+ $data = self::getKeyArray();
+ if (isset($data[$keyName]))
+ return '0x' . dechex($data[$keyName]);
+ return false;
+ }
+
+ /**
+ * @param string $keyName desired key name
+ * @return string $keyName if it's known, empty string otherwise
+ */
+ public static function filterKeyName($keyName)
+ {
+ $data = self::getKeyArray();
+ if (isset($data[$keyName]))
+ return $keyName;
+ return '';
+ }
+
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php
new file mode 100644
index 00000000..1d022fef
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php
@@ -0,0 +1,302 @@
+<?php
+
+
+class PxeLinux
+{
+
+ /**
+ * Takes a (partial) pxelinux menu and parses it into
+ * a PxeMenu object.
+ * @param string $input The pxelinux menu to parse
+ * @return PxeMenu the parsed menu
+ */
+ public static function parsePxeLinux($input)
+ {
+ /*
+ LABEL openslx-debug
+ MENU LABEL ^bwLehrpool-Umgebung starten (nosplash, debug)
+ KERNEL http://IPADDR/boot/default/kernel
+ INITRD http://IPADDR/boot/default/initramfs-stage31
+ APPEND slxbase=boot/default
+ IPAPPEND 3
+ */
+ $menu = new PxeMenu;
+ $sectionPropMap = [
+ 'menu label' => ['string', 'title'],
+ 'menu default' => ['true', 'isDefault'],
+ 'menu hide' => ['true', 'isHidden'],
+ 'menu disabled' => ['true', 'isDisabled'],
+ 'menu indent' => ['int', 'indent'],
+ 'kernel' => ['string', 'kernel'],
+ 'com32' => ['string', 'kernel'],
+ 'pxe' => ['string', 'kernel'],
+ 'initrd' => ['string', 'initrd'],
+ 'append' => ['string', 'append'],
+ 'ipappend' => ['int', 'ipAppend'],
+ 'sysappend' => ['int', 'ipAppend'],
+ 'localboot' => ['int', 'localBoot'],
+ ];
+ $globalPropMap = [
+ 'timeout' => ['int', 'timeoutMs', 100],
+ 'totaltimeout' => ['int', 'totalTimeoutMs', 100],
+ 'menu title' => ['string', 'title'],
+ 'menu clear' => ['true', 'menuClear'],
+ 'menu immediate' => ['true', 'immediateHotkeys'],
+ 'ontimeout' => ['string', 'timeoutLabel'],
+ ];
+ $lines = preg_split('/[\r\n]+/', $input);
+ $section = null;
+ $count = count($lines);
+ for ($li = 0; $li < $count; ++$li) {
+ $line =& $lines[$li];
+ if (!preg_match('/^\s*([^m]\S*|menu\s+\S+)(\s+.*?|)\s*$/i', $line, $out))
+ continue;
+ $val = trim($out[2]);
+ $key = trim($out[1]);
+ $key = strtolower($key);
+ $key = preg_replace('/\s+/', ' ', $key);
+ if ($key === 'label') {
+ if ($section !== null) {
+ $menu->sections[] = $section;
+ }
+ $section = new PxeSection($val);
+ } elseif ($key === 'menu separator') {
+ if ($section !== null) {
+ $menu->sections[] = $section;
+ $section = null;
+ }
+ $menu->sections[] = new PxeSection(null);
+ } elseif (self::handleKeyword($key, $val, $globalPropMap, $menu)) {
+ continue;
+ } elseif ($section === null) {
+ continue;
+ } elseif ($key === 'text' && strtolower($val) === 'help') {
+ $text = '';
+ while (++$li < $count) {
+ $line =& $lines[$li];
+ if (strtolower(trim($line)) === 'endtext')
+ break;
+ $text .= $line . "\n";
+ }
+ $section->helpText = $text;
+ } elseif (self::handleKeyword($key, $val, $sectionPropMap, $section)) {
+ continue;
+ }
+ }
+ if ($section !== null) {
+ $menu->sections[] = $section;
+ }
+ foreach ($menu->sections as $section) {
+ $section->mangle();
+ }
+ return $menu;
+ }
+
+ /**
+ * Check if keyword is valid and if so, add its interpreted value
+ * to the given object. The map to look up the keyword has to be passed
+ * as well as the object to set the value in. Map and object should
+ * obviously match.
+ * @param string $key keyword of parsed line
+ * @param string $val raw value of currently parsed line (empty if not present)
+ * @param array $map Map in which $key is looked up as key
+ * @param PxeMenu|PxeSection The object to set the parsed and sanitized value in
+ * @return bool true if the value was found in the map (and set in the object), false otherwise
+ */
+ private static function handleKeyword($key, $val, $map, $object)
+ {
+ if (!isset($map[$key]))
+ return false;
+ $opt = $map[$key];
+ // opt[0] is the type the value should be cast to; special case "true" means
+ // this is a bool option that will be set as soon as the keyword is present,
+ // as it doesn't have any parameters
+ if ($opt[0] === 'true') {
+ $val = true;
+ } else {
+ settype($val, $opt[0]);
+ }
+ // If opt[2] is present it's a multiplier for the value
+ if (isset($opt[2])) {
+ $val *= $opt[2];
+ }
+ $object->{$opt[1]} = $val;
+ return true;
+ }
+
+}
+
+/**
+ * Class representing a parsed pxelinux menu. Members
+ * will be set to their annotated type if present or
+ * be null otherwise, except for present-only boolean
+ * options, which will default to false.
+ */
+class PxeMenu
+{
+
+ /**
+ * @var string menu title, shown at the top of the menu
+ */
+ public $title;
+ /**
+ * @var int initial timeout after which $timeoutLabel would be executed
+ */
+ public $timeoutMs;
+ /**
+ * @var int if the user canceled the timeout by pressing a key, this timeout would still eventually
+ * trigger and launch the $timeoutLabel section
+ */
+ public $totalTimeoutMs;
+ /**
+ * @var string label of section which will execute if the timeout expires
+ */
+ public $timeoutLabel;
+ /**
+ * @var bool hide menu and just show background after triggering an entry
+ */
+ public $menuClear = false;
+ /**
+ * @var bool boot the associated entry directly if its corresponding hotkey is pressed instead of just highlighting
+ */
+ public $immediateHotkeys = false;
+ /**
+ * @var PxeSection[] list of sections the menu contains
+ */
+ public $sections = [];
+
+ public function hash($fuzzy)
+ {
+ $ctx = hash_init('md5');
+ if (!$fuzzy) {
+ hash_update($ctx, $this->title);
+ hash_update($ctx, $this->timeoutLabel);
+ }
+ hash_update($ctx, $this->timeoutMs);
+ foreach ($this->sections as $section) {
+ if ($fuzzy) {
+ hash_update($ctx, mb_strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $section->title)));
+ } else {
+ hash_update($ctx, $section->label);
+ hash_update($ctx, $section->title);
+ hash_update($ctx, $section->indent);
+ hash_update($ctx, $section->helpText);
+ hash_update($ctx, $section->isDefault);
+ hash_update($ctx, $section->hotkey);
+ }
+ hash_update($ctx, $section->kernel);
+ hash_update($ctx, $section->append);
+ hash_update($ctx, $section->ipAppend);
+ hash_update($ctx, $section->passwd);
+ hash_update($ctx, $section->isHidden);
+ hash_update($ctx, $section->isDisabled);
+ hash_update($ctx, $section->localBoot);
+ foreach ($section->initrd as $initrd) {
+ hash_update($ctx, $initrd);
+ }
+ }
+ return hash_final($ctx, false);
+ }
+
+}
+
+/**
+ * Class representing a parsed pxelinux menu entry. Members
+ * will be set to their annotated type if present or
+ * be null otherwise, except for present-only boolean
+ * options, which will default to false.
+ */
+class PxeSection
+{
+
+ /**
+ * @var string label used internally in PXEMENU definition to address this entry
+ */
+ public $label;
+ /**
+ * @var string MENU LABEL of PXEMENU - title of entry displayed to the user
+ */
+ public $title;
+ /**
+ * @var int Number of spaces to prefix the title with
+ */
+ public $indent;
+ /**
+ * @var string help text to display when the entry is highlighted
+ */
+ public $helpText;
+ /**
+ * @var string Kernel to load
+ */
+ public $kernel;
+ /**
+ * @var string|string[] initrd to load for the kernel.
+ * If mangle() has been called this will be an array,
+ * otherwise it's a comma separated list.
+ */
+ public $initrd;
+ /**
+ * @var string command line options to pass to the kernel
+ */
+ public $append;
+ /**
+ * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2.
+ */
+ public $ipAppend;
+ /**
+ * @var string Password protecting the entry. This is most likely in crypted form.
+ */
+ public $passwd;
+ /**
+ * @var bool whether this section is marked as default (booted after timeout)
+ */
+ public $isDefault = false;
+ /**
+ * @var bool Menu entry is not visible (can only be triggered by timeout)
+ */
+ public $isHidden = false;
+ /**
+ * @var bool Disable this entry, making it unselectable
+ */
+ public $isDisabled = false;
+ /**
+ * @var int Value of the LOCALBOOT field
+ */
+ public $localBoot;
+ /**
+ * @var string hotkey to trigger item. Only valid after calling mangle()
+ */
+ public $hotkey;
+
+ public function __construct($label) { $this->label = $label; }
+
+ public function mangle()
+ {
+ if (($i = strpos($this->title, '^')) !== false) {
+ $this->hotkey = strtoupper($this->title{$i+1});
+ $this->title = substr($this->title, 0, $i) . substr($this->title, $i + 1);
+ }
+ if (strpos($this->append, 'initrd=') !== false) {
+ $parts = preg_split('/\s+/', $this->append);
+ $this->append = '';
+ for ($i = 0; $i < count($parts); ++$i) {
+ if (preg_match('/^initrd=(.*)$/', $parts[$i], $out)) {
+ if (!empty($this->initrd)) {
+ $this->initrd .= ',';
+ }
+ $this->initrd .= $out[1];
+ } else {
+ $this->append .= ' ' . $parts[$i];
+ }
+ }
+ $this->append = trim($this->append);
+ }
+ if (is_string($this->initrd)) {
+ $this->initrd = explode(',', $this->initrd);
+ } elseif (!is_array($this->initrd)) {
+ $this->initrd = [];
+ }
+ }
+
+}
+