summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2018-06-12 17:15:44 +0200
committerSimon Rettberg2018-06-12 17:15:44 +0200
commit2e78eec281815d6ba42ff2cf7c3a937abe6d83c5 (patch)
treed98495500354de9b735b31c64e02856eeb179098
parent[inc/Database] Method to return single-column queries as array (diff)
downloadslx-admin-2e78eec281815d6ba42ff2cf7c3a937abe6d83c5.tar.gz
slx-admin-2e78eec281815d6ba42ff2cf7c3a937abe6d83c5.tar.xz
slx-admin-2e78eec281815d6ba42ff2cf7c3a937abe6d83c5.zip
[serversetup-bwlp] Start rewrite as purely iPXE-based
-rw-r--r--modules-available/serversetup-bwlp/api.inc.php242
-rw-r--r--modules-available/serversetup-bwlp/config.json5
-rw-r--r--modules-available/serversetup-bwlp/inc/bootentry.inc.php195
-rw-r--r--modules-available/serversetup-bwlp/inc/ipxe.inc.php560
-rw-r--r--modules-available/serversetup-bwlp/inc/ipxemenu.inc.php142
-rw-r--r--modules-available/serversetup-bwlp/inc/menuentry.inc.php170
-rw-r--r--modules-available/serversetup-bwlp/inc/pxelinux.inc.php262
-rw-r--r--modules-available/serversetup-bwlp/install.inc.php74
-rw-r--r--modules-available/serversetup-bwlp/page.inc.php300
-rw-r--r--modules-available/serversetup-bwlp/permissions/permissions.json14
-rw-r--r--modules-available/serversetup-bwlp/templates/download.html38
-rw-r--r--modules-available/serversetup-bwlp/templates/heading.html4
-rw-r--r--modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html108
-rw-r--r--modules-available/serversetup-bwlp/templates/ipxe.html117
-rw-r--r--modules-available/serversetup-bwlp/templates/menu-edit.html114
-rw-r--r--modules-available/serversetup-bwlp/templates/menu-list.html36
16 files changed, 2049 insertions, 332 deletions
diff --git a/modules-available/serversetup-bwlp/api.inc.php b/modules-available/serversetup-bwlp/api.inc.php
new file mode 100644
index 00000000..36f9063c
--- /dev/null
+++ b/modules-available/serversetup-bwlp/api.inc.php
@@ -0,0 +1,242 @@
+<?php
+
+$BOOT_METHODS = [
+ 'EXIT' => 'exit 1',
+ 'COMBOOT' => 'chain /tftp/chain.c32 hd0',
+ 'SANBOOT' => 'sanboot --no-describe',
+];
+
+$serverIp = Property::getServerIp();
+
+$ip = $_SERVER['REMOTE_ADDR'];
+if (substr($ip, 0, 7) === '::ffff:') {
+ $ip = substr($ip, 7);
+}
+$uuid = Request::any('uuid', false, 'string');
+$menu = IPxeMenu::forClient($ip, $uuid);
+
+// Get platform - EFI or PCBIOS
+$platform = strtoupper(Request::any('platform', 'PCBIOS', 'string'));
+
+// Get preferred localboot method, depending on system model
+$localboot = false;
+$model = false;
+if ($uuid !== false && Module::get('statistics') !== false) {
+ $row = Database::queryFirst('SELECT systemmodel FROM machine WHERE machineuuid = :uuid', ['uuid' => $uuid]);
+ if ($row !== false && !empty($row['systemmodel'])) {
+ $model = $row['systemmodel'];
+ }
+}
+if ($model === false) {
+ function modfilt($str)
+ {
+ if (empty($str) || preg_match('/product\s+name|be\s+filled|unknown|default\s+string/i', $str))
+ return false;
+ return trim(preg_replace('/\s+/', ' ', $str));
+ }
+ $manuf = modfilt(Request::any('manuf', false, 'string'));
+ $product = modfilt(Request::any('product', false, 'string'));
+ if (!empty($product)) {
+ $model = $product;
+ if (!empty($manuf)) {
+ $model .= " ($manuf)";
+ }
+ }
+}
+// Query
+if ($model !== false) {
+ $row = Database::queryFirst("SELECT bootmethod FROM serversetup_localboot WHERE systemmodel = :model LIMIT 1",
+ ['model' => $model]);
+ if ($row !== false) {
+ $localboot = $row['bootmethod'];
+ }
+}
+if ($localboot === false || !isset($BOOT_METHODS[$localboot])) {
+ $localboot = Property::get('serversetup.localboot', false);
+ if ($localboot === false) {
+ if ($platform === 'EFI') {
+ // It seems most (all) EFI platforms won't enumerate any drives in ipxe.
+ // No idea if this can be fixed in ipxe code in the future.
+ $localboot = 'EXIT';
+ } else {
+ $localboot = 'SANBOOT';
+ }
+ }
+}
+if (isset($BOOT_METHODS[$localboot])) {
+ // Move preferred method first
+ $BOOT_METHODS[] = $BOOT_METHODS[$localboot];
+ unset($BOOT_METHODS[$localboot]);
+ $BOOT_METHODS = array_reverse($BOOT_METHODS);
+}
+
+$output = <<<HERE
+#!ipxe
+
+goto init || goto fail ||
+
+# functions
+
+# password check with gotos
+# set slx_hash to the expected hash
+# slx_salt to the salt to use
+# slx_pw_ok to the label to jump on success
+# slx_pw_fail to label for wrong pw
+:slx_pass_check
+login ||
+set slxtmp_pw \${password:md5}-\${slx_salt} || goto fail
+set slxtmp_pw \${slxtmp_pw:md5} || goto fail
+clear password ||
+iseq \${slxtmp_pw} \${slx_hash} || prompt Wrong password. Press a key. ||
+iseq \${slxtmp_pw} \${slx_hash} || goto \${slx_pw_fail} ||
+iseq \${slxtmp_pw} \${slx_hash} && goto \${slx_pw_ok} ||
+goto fail
+
+# local boot with either exit 1 or sanboot
+:slx_localboot
+console ||
+
+HERE;
+
+foreach ($BOOT_METHODS as $line) {
+ $output .= "$line || goto fail\n";
+}
+
+$output .= <<<HERE
+goto fail
+
+# start
+:init
+
+iseq \${nic} \${} && set nic 0 ||
+
+set ipappend1 ip=\${net\${nic}/ip}:{$serverIp}:\${net\${nic}/gateway}:\${net\${nic}/netmask}
+set ipappend2 BOOTIF=01-\${net\${nic}/mac:hexhyp}
+set serverip $serverIp ||
+
+# Clean up in case we've been chained to
+imgfree ||
+
+ifopen ||
+
+imgfetch --name bg-load /tftp/openslx.png ||
+
+imgfetch --name bg-menu /tftp/pxe-menu.png ||
+
+:start
+
+console --left 55 --top 88 --right 63 --bottom 64 --keep --picture bg-menu ||
+
+colour --rgb 0xffffff 7
+colour --rgb 0xcccccc 5
+cpair --foreground 0 --background 4 1
+cpair --foreground 7 --background 5 2
+cpair --foreground 7 --background 9 0
+
+:slx_menu
+
+console --left 55 --top 88 --right 63 --bottom 64 --quick --keep --picture bg-menu ||
+
+HERE;
+
+$output .= $menu->getMenuDefinition('target');
+
+$output .= <<<HERE
+
+console --left 60 --top 130 --right 67 --bottom 86 --quick ||
+goto \${target} ||
+echo Could not find menu entry in script.
+prompt Press any key to continue.
+goto start
+
+HERE;
+
+$output .= $menu->getItemsCode();
+
+/*
+:i1
+#console ||
+echo Welcome to Shell ||
+shell
+goto slx_menu
+
+:i2
+imgfree ||
+kernel /boot/default/kernel slxbase=boot/default slxsrv=$serverIp splash BOOTIF=01-\${net\${nic}/mac:hexhyp} || echo Could not download kernel
+initrd /boot/default/initramfs-stage31 || echo Could not download initrd
+boot -ar || goto fail
+
+:i3
+chain -ar \${self} ||
+chain -ar /tftp/undionly.kpxe || goto fail
+
+:i4
+imgfree ||
+sanboot --no-describe --drive 0x80 || goto fail
+
+:i5
+chain -a /tftp/memtest.0 passes=1 onepass || goto membad
+prompt Memory OK. Press a key.
+goto init
+
+:i6
+console --left 60 --top 130 --right 67 --bottom 96 --quick --picture bg-load --keep ||
+echo Welcome to Shell ||
+shell
+goto slx_menu
+
+:i7
+chain -ar tftp://132.230.4.6/ipxelinux.0 || prompt FAILED PRESS A KEY
+goto slx_menu
+
+:i8
+set x:int32 0
+:again
+console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep --quick ||
+console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep --quick ||
+inc x
+iseq \${x} 20 || goto again
+prompt DONE. Press dein Knie.
+goto slx_menu
+
+:i9
+reboot ||
+prompt Reboot failed. Press a key.
+goto slx_menu
+
+:i10
+poweroff ||
+prompt Poweroff failed. Press a key.
+goto slx_menu
+
+:membad
+iseq \${errno} 0x1 || goto memaborted
+params
+param scrot \${vram}
+imgfetch -a http://132.230.8.113/screen.php##params ||
+prompt Memory is bad. Press a key.
+goto init
+
+:memaborted
+params
+param scrot \${vram}
+imgfetch -a http://132.230.8.113/screen.php##params ||
+prompt Memory test aborted. Press a key.
+goto init
+
+*/
+
+$output .= <<<HERE
+:fail
+prompt Boot failed. Press any key to start.
+goto init
+HERE;
+
+if ($platform === 'EFI') {
+ $cs = 'ASCII';
+} else {
+ $cs = 'IBM437';
+}
+Header('Content-Type: text/plain; charset=' . $cs);
+
+echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $output);
diff --git a/modules-available/serversetup-bwlp/config.json b/modules-available/serversetup-bwlp/config.json
index 36268c6a..8ce65a85 100644
--- a/modules-available/serversetup-bwlp/config.json
+++ b/modules-available/serversetup-bwlp/config.json
@@ -1,3 +1,6 @@
{
- "category": "main.settings-server"
+ "category": "main.settings-server",
+ "dependencies" : [
+ "locations"
+ ]
} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/inc/bootentry.inc.php b/modules-available/serversetup-bwlp/inc/bootentry.inc.php
new file mode 100644
index 00000000..f488959b
--- /dev/null
+++ b/modules-available/serversetup-bwlp/inc/bootentry.inc.php
@@ -0,0 +1,195 @@
+<?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 toScript($failLabel);
+
+ 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)
+ {
+ if (empty($initData['executable']))
+ return null;
+ return new StandardBootEntry($initData);
+ }
+
+ 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;
+
+ public function __construct($data = false)
+ {
+ if ($data instanceof PxeSection) {
+ $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);
+ }
+ }
+
+ public function toScript($failLabel)
+ {
+ $script = '';
+ if ($this->resetConsole) {
+ $script .= "console ||\n";
+ }
+ if (!empty($this->initRd)) {
+ $script .= "imgfree ||\n";
+ if (!is_array($this->initRd)) {
+ $script .= "initrd {$this->initRd} || goto $failLabel\n";
+ } else {
+ foreach ($this->initRd as $initrd) {
+ $script .= "initrd $initrd || goto $failLabel\n";
+ }
+ }
+ }
+ $script .= "boot ";
+ if ($this->autoUnload) {
+ $script .= "-a ";
+ }
+ if ($this->replace) {
+ $script .= "-r ";
+ }
+ $script .= "{$this->executable}";
+ if (!empty($this->commandLine)) {
+ $script .= " {$this->commandLine}";
+ }
+ $script .= " || goto $failLabel\n";
+ if ($this->resetConsole) {
+ $script .= "goto start ||\n";
+ }
+ return $script;
+ }
+
+ public function addFormFields(&$array)
+ {
+ $array['entry'] = [
+ 'executable' => $this->executable,
+ 'initRd' => $this->initRd,
+ 'commandLine' => $this->commandLine,
+ 'replace_checked' => $this->replace ? 'checked' : '',
+ 'autoUnload_checked' => $this->autoUnload ? 'checked' : '',
+ 'resetConsole_checked' => $this->resetConsole ? '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,
+ ];
+ }
+}
+
+class CustomBootEntry extends BootEntry
+{
+ protected $script;
+
+ public function toScript($failLabel)
+ {
+ 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/inc/ipxe.inc.php b/modules-available/serversetup-bwlp/inc/ipxe.inc.php
index c42de80b..d5bbb4b2 100644
--- a/modules-available/serversetup-bwlp/inc/ipxe.inc.php
+++ b/modules-available/serversetup-bwlp/inc/ipxe.inc.php
@@ -1,224 +1,398 @@
<?php
-
-class Ipxe
+class IPxe
{
- /**
- * 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)
+ public static function importPxeMenus($configPath)
{
- /*
- 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'],
- 'initrd' => ['string', 'initrd'],
- 'append' => ['string', 'append'],
- 'ipappend' => ['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))
+ foreach (glob($configPath . '/*', GLOB_NOSORT) as $file) {
+ if (!is_file($file) || !preg_match('~/[A-F0-9]{1,8}$~', $file))
continue;
- $key = trim($out[1]);
- $key = strtolower($key);
- $key = preg_replace('/\s+/', ' ', $key);
- if ($key === 'label') {
- if ($section !== null) {
- $menu->sections[] = $section;
- }
- $section = new PxeSection($out[2]);
- } elseif ($key === 'menu separator') {
- if ($section !== null) {
- $menu->sections[] = $section;
- $section = null;
- }
- $menu->sections[] = new PxeSection(null);
- } elseif (self::handleKeyword($key, $out[2], $globalPropMap, $menu)) {
+ $content = file_get_contents($file);
+ if ($content === false)
continue;
- } elseif ($section === null) {
- continue;
- } elseif ($key === 'text' && strtolower($out[2]) === 'help') {
- $text = '';
- while (++$li < $count) {
- $line =& $lines[$li];
- if (strtolower(trim($line)) === 'endtext')
- break;
- $text .= $line . "\n";
+ $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;
+ }
}
- $section->helpText = $text;
- } elseif (self::handleKeyword($key, $out[2], $sectionPropMap, $section)) {
+ unset($loc);
+ $locations[] = $row;
+ }
+ $menuId = self::insertMenu($content, 'Imported', false, 0, [], []);
+ if ($menuId === false)
continue;
+ foreach ($locations as $loc) {
+ Database::exec('INSERT IGNORE INTO serversetup_menu_x_location (menuid, locationid)
+ VALUES (:menuid, :locationid)', ['menuid' => $menuId, 'locationid' => $loc['locationid']]);
}
}
- if ($section !== null) {
- $menu->sections[] = $section;
+ }
+
+ 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'];
+ }
}
- return $menu;
+ $append = [
+ '',
+ 'bwlp-default-dbg' => false,
+ '',
+ 'poweroff' => false,
+ ];
+ return self::insertMenu($pxeConfig, $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append);
}
- /**
- * 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)
+ private static function insertMenu($pxeConfig, $menuTitle, $defaultLabel, $defaultTimeoutSeconds, $prepend, $append)
{
- if (!isset($map[$key]))
+ $timeoutMs = [];
+ $menuEntries = $prepend;
+ settype($menuEntries, 'array');
+ if (!empty($pxeConfig)) {
+ $pxe = PxeLinux::parsePxeLinux($pxeConfig);
+ 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;
+ }
+ $section->mangle();
+ 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;
- $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;
+ // Make menu
+ $timeoutMs = array_filter($timeoutMs, 'is_int');
+ if (empty($timeoutMs)) {
+ $timeoutMs = (int)($defaultTimeoutSeconds * 1000);
} else {
- settype($val, $opt[0]);
+ $timeoutMs = min($timeoutMs);
}
- // If opt[2] is present it's a multiplier for the value
- if (isset($opt[2])) {
- $val *= $opt[2];
+ $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];
}
- $object->{$opt[1]} = $val;
- return true;
+ // 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 ${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 ${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% ||',
+ ]),
+ ]);
+ }
-/**
- * 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 presed instead of just highlighting
+ * 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
*/
- public $immediateHotkeys = false;
- /**
- * @var PxeSection[] list of sections the menu contains
- */
- public $sections = [];
-}
+ 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);
+ }
-/**
- * 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 initrd to load for the kernel
- */
- 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
+ * @param PxeSection $section
+ * @return BootEntry|null The according boot entry, null if it's unparsable
*/
- public $isDisabled = false;
+ 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);
+ }
+
/**
- * @var int Value of the LOCALBOOT field
+ * Parse PXELINUX file notion. Basically, turn
+ * server::file into tftp://server/file.
+ *
+ * @param string $file
+ * @return string
*/
- public $localBoot;
+ private static function parseFile($file)
+ {
+ if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) {
+ return 'tftp://' . $out[1] . '/' . $out[2];
+ }
+ return $file;
+ }
- public function __construct($label) { $this->label = $label; }
-}
+ 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/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp/inc/ipxemenu.inc.php
new file mode 100644
index 00000000..ed9f0986
--- /dev/null
+++ b/modules-available/serversetup-bwlp/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)
+ {
+ $str = "menu {$this->title}\n";
+ foreach ($this->items as $item) {
+ $str .= $item->getMenuItemScript("m_{$this->menuid}", $this->defaultEntryId);
+ }
+ 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()
+ {
+ $str = '';
+ foreach ($this->items as $item) {
+ $str .= $item->getBootEntryScript("m_{$this->menuid}", 'fail');
+ $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, m.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/inc/menuentry.inc.php b/modules-available/serversetup-bwlp/inc/menuentry.inc.php
new file mode 100644
index 00000000..9736b7bb
--- /dev/null
+++ b/modules-available/serversetup-bwlp/inc/menuentry.inc.php
@@ -0,0 +1,170 @@
+<?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)
+ {
+ $str = 'item ';
+ if ($this->gap) {
+ $str .= '--gap ';
+ } else {
+ if ($this->hidden) {
+ 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} ";
+ }
+ $str .= $this->title;
+ return $str . " || prompt Could not create menu item for {$lblPrefix}_{$this->menuentryid}\n";
+ }
+
+ public function getBootEntryScript($lblPrefix, $failLabel)
+ {
+ if ($this->bootEntry === null)
+ 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);
+ }
+
+ /*
+ *
+ */
+
+ 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)
+ {
+ if (isset($data[$keyName]))
+ return $keyName;
+ return '';
+ }
+
+}
diff --git a/modules-available/serversetup-bwlp/inc/pxelinux.inc.php b/modules-available/serversetup-bwlp/inc/pxelinux.inc.php
new file mode 100644
index 00000000..db3dac4b
--- /dev/null
+++ b/modules-available/serversetup-bwlp/inc/pxelinux.inc.php
@@ -0,0 +1,262 @@
+<?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;
+ }
+ 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 = [];
+}
+
+/**
+ * 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 = [];
+ }
+ }
+}
+
diff --git a/modules-available/serversetup-bwlp/install.inc.php b/modules-available/serversetup-bwlp/install.inc.php
new file mode 100644
index 00000000..8814bb7c
--- /dev/null
+++ b/modules-available/serversetup-bwlp/install.inc.php
@@ -0,0 +1,74 @@
+<?php
+
+$res = array();
+
+$res[] = tableCreate('serversetup_bootentry', "
+ `entryid` varchar(16) CHARACTER SET ascii NOT NULL,
+ `hotkey` varchar(8) CHARACTER SET ascii NOT NULL,
+ `title` varchar(100) NOT NULL,
+ `builtin` tinyint(1) NOT NULL,
+ `data` blob NOT NULL,
+ PRIMARY KEY (`entryid`)
+");
+
+$res[] = tableCreate('serversetup_menu', "
+ `menuid` int(11) NOT NULL AUTO_INCREMENT,
+ `timeoutms` int(10) unsigned NOT NULL,
+ `title` varchar(100) NOT NULL COMMENT 'Escaped/Sanitized for iPXE!',
+ `defaultentryid` int(11) DEFAULT NULL,
+ `isdefault` tinyint(1) NOT NULL,
+ PRIMARY KEY (`menuid`),
+ KEY `defaultentryid` (`defaultentryid`),
+ KEY `isdefault` (`isdefault`)
+");
+
+$res[] = tableCreate('serversetup_menuentry', "
+ `menuentryid` int(11) NOT NULL AUTO_INCREMENT,
+ `menuid` int(11) NOT NULL,
+ `entryid` varchar(16) CHARACTER SET ascii NULL COMMENT 'If NULL, entry is gap',
+ `hotkey` varchar(8) CHARACTER SET ascii NOT NULL,
+ `title` varchar(100) NOT NULL COMMENT 'Sanitize this before insert',
+ `hidden` tinyint(1) NOT NULL,
+ `sortval` int(11) NOT NULL,
+ `plainpass` varchar(80) NOT NULL,
+ `md5pass` char(32) CHARACTER SET ascii NOT NULL,
+ PRIMARY KEY (`menuentryid`),
+ KEY `menuid` (`menuid`,`entryid`),
+ KEY `entryid` (`entryid`)
+");
+
+$res[] = tableCreate('serversetup_menu_location', '
+ `menuid` int(11) NOT NULL,
+ `locationid` int(11) NOT NULL,
+ PRIMARY KEY (`menuid`,`locationid`),
+ UNIQUE `locationid` (`locationid`)
+');
+
+$res[] = tableCreate('serversetup_localboot', "
+ `systemmodel` varchar(120) NOT NULL,
+ `bootmethod` enum('EXIT','COMBOOT','SANBOOT') CHARACTER SET ascii NOT NULL,
+ PRIMARY KEY (`systemmodel`)
+");
+
+$res[] = tableAddConstraint('serversetup_menu', 'defaultentryid', 'serversetup_menuentry', 'menuentryid',
+ 'ON DELETE SET NULL');
+
+$res[] = tableAddConstraint('serversetup_menuentry', 'entryid', 'serversetup_bootentry', 'entryid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+$res[] = tableAddConstraint('serversetup_menuentry', 'menuid', 'serversetup_menu', 'menuid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+$res[] = tableAddConstraint('serversetup_menu_location', 'menuid', 'serversetup_menu', 'menuid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+
+if (Module::get('location') !== false) {
+ if (!tableExists('location')) {
+ $res[] = UPDATE_RETRY;
+ } else {
+ $res[] = tableAddConstraint('serversetup_menu_location', 'locationid', 'location', 'locationid',
+ 'ON UPDATE CASCADE ON DELETE CASCADE');
+ }
+}
+
+responseFromArray($res);
diff --git a/modules-available/serversetup-bwlp/page.inc.php b/modules-available/serversetup-bwlp/page.inc.php
index 52b3afe4..4207bd53 100644
--- a/modules-available/serversetup-bwlp/page.inc.php
+++ b/modules-available/serversetup-bwlp/page.inc.php
@@ -17,6 +17,12 @@ class Page_ServerSetup extends Page
Util::redirect('?do=Main');
}
+ if (Request::any('bla') == 'blu') {
+ IPxe::importLegacyMenu();
+ IPxe::importPxeMenus('/srv/openslx/tftp/pxelinux.cfg');
+ die('DONE');
+ }
+
if (Request::any('action') === 'getimage') {
User::assertPermission("download");
$this->handleGetImage();
@@ -44,16 +50,45 @@ class Page_ServerSetup extends Page
$this->updatePxeMenu();
}
+ if ($action === 'savebootentry') {
+ User::assertPermission('ipxe.bootentry.edit');
+ $this->saveBootEntry();
+ }
+
+ if ($action === 'savemenu') {
+ User::assertPermission('ipxe.menu.edit');
+ $this->saveMenu();
+ }
+
if (Request::isPost()) {
Util::redirect('?do=serversetup');
}
User::assertPermission('access-page');
+
+ if (User::hasPermission('ipxe.*')) {
+ Dashboard::addSubmenu('?do=serversetup&show=menu', Dictionary::translate('submenu_menu', true));
+ }
+ if (User::hasPermission('edit.address')) {
+ Dashboard::addSubmenu('?do=serversetup&show=address', Dictionary::translate('submenu_address', true));
+ }
+ if (User::hasPermission('download')) {
+ Dashboard::addSubmenu('?do=serversetup&show=download', Dictionary::translate('submenu_download', true));
+ }
+ if (Request::get('show') === false) {
+ $subs = Dashboard::getSubmenus();
+ if (empty($subs)) {
+ User::assertPermission('download');
+ } else {
+ Util::redirect($subs[0]['url']);
+ }
+ }
}
protected function doRender()
{
Render::addTemplate("heading");
+
$task = Property::get('ipxe-task-id');
if ($task !== false) {
$task = Taskmanager::status($task);
@@ -65,32 +100,143 @@ class Page_ServerSetup extends Page
Render::addTemplate('ipxe_update', array('taskid' => $task['id']));
}
- Permission::addGlobalTags($perms, null, ['edit.menu', 'edit.address', 'download']);
+ switch (Request::get('show')) {
+ case 'editbootentry':
+ User::assertPermission('ipxe.bootentry.edit');
+ $this->showEditBootEntry();
+ break;
+ case 'editmenu':
+ User::assertPermission('ipxe.menu.view');
+ $this->showEditMenu();
+ break;
+ case 'download':
+ User::assertPermission('download');
+ $this->showDownload();
+ break;
+ case 'menu':
+ User::assertPermission('ipxe.menu.view');
+ $this->showMenuList();
+ break;
+ default:
+ Util::redirect('?do=serversetup');
+ break;
+ }
+ }
+
+ private function showDownload()
+ {
+ // TODO: Make nicer, support more variants (taskmanager-plugin)
+ Render::addTemplate('download');
+ }
+
+ private function showMenuList()
+ {
+ $allowedEdit = User::getAllowedLocations('ipxe.menu.edit');
+
+ // TODO Permission::addGlobalTags($perms, null, ['edit.menu', 'edit.address', 'download']);
+
+ $res = Database::simpleQuery("SELECT m.menuid, m.title, m.isdefault, GROUP_CONCAT(l.locationid) AS locations
+ FROM serversetup_menu m LEFT JOIN serversetup_menu_location l USING (menuid) GROUP BY menuid ORDER BY title");
+ $table = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ if (empty($row['locations'])) {
+ $locations = [];
+ $row['allowEdit'] = in_array(0, $allowedEdit);
+ } else {
+ $locations = explode(',', $row['locations']);
+ $row['allowEdit'] = empty(array_diff($locations, $allowedEdit));
+ }
+ $row['locationCount'] = empty($locations) ? '' : count($locations);
+ $table[] = $row;
+ }
- Render::addTemplate('ipaddress', array(
- 'ips' => $this->taskStatus['data']['addresses'],
- 'chooseHintClass' => $this->hasIpSet ? '' : 'alert alert-danger',
- 'editAllowed' => User::hasPermission("edit.address"),
- 'perms' => $perms,
+ Render::addTemplate('menu-list', array(
+ 'table' => $table,
));
- $data = $this->currentMenu;
- if (!User::hasPermission('edit.menu')) {
- unset($data['masterpasswordclear']);
+ }
+
+ private function hasMenuPermission($menuid, $permission)
+ {
+ $allowedEditLocations = User::getAllowedLocations($permission);
+ $allowEdit = in_array(0, $allowedEditLocations);
+ if (!$allowEdit) {
+ // Get locations
+ $locations = Database::queryColumnArray('SELECT locationid FROM serversetup_menu_location
+ WHERE menuid = :menuid', compact('menuid'));
+ if (!empty($locations)) {
+ $allowEdit = count(array_diff($locations, $allowedEditLocations)) === 0;
+ }
}
- if (!isset($data['defaultentry'])) {
- $data['defaultentry'] = 'net';
+ return $allowEdit;
+ }
+
+ private function showEditMenu()
+ {
+ $id = Request::get('id', false, 'int');
+ $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid, isdefault
+ FROM serversetup_menu WHERE menuid = :id", compact('id'));
+ if ($menu === false) {
+ Message::addError('invalid-menu-id', $id);
+ Util::redirect('?do=serversetup&show=menu');
}
- if ($data['defaultentry'] === 'net') {
- $data['active-net'] = 'checked';
+ if (!$this->hasMenuPermission($id, 'ipxe.menu.edit')) {
+ $menu['readonly'] = 'readonly';
+ $menu['disabled'] = 'disabled';
+ $menu['plainpass'] = '';
}
- if ($data['defaultentry'] === 'hdd') {
- $data['active-hdd'] = 'checked';
+ $menu['timeout'] = round($menu['timeoutms'] / 1000);
+ $menu['entries'] = Database::queryAll("SELECT menuentryid, entryid, hotkey, title, hidden, sortval, plainpass FROM
+ serversetup_menuentry WHERE menuid = :id", compact('id'));
+ $keyList = array_map(function ($item) { return ['key' => $item]; }, MenuEntry::getKeyList());
+ $entryList = Database::queryAll("SELECT entryid, title, hotkey FROM serversetup_bootentry ORDER BY title ASC");
+ foreach ($menu['entries'] as &$entry) {
+ $entry['isdefault'] = ($entry['menuentryid'] == $menu['defaultentryid']);
+ $entry['keys'] = $keyList;
+ foreach ($entry['keys'] as &$key) {
+ if ($key['key'] === $entry['hotkey']) {
+ $key['selected'] = 'selected'; // TODO: plainpass only when permissions
+ }
+ }
+ $entry['entrylist'] = $entryList;
+ foreach ($entry['entrylist'] as &$item) {
+ if ($item['entryid'] == $entry['entryid']) {
+ $item['selected'] = 'selected';
+ }
+ if (empty($item['title'])) {
+ $item['title'] = $item['entryid'];
+ }
+ }
}
- if ($data['defaultentry'] === 'custom') {
- $data['active-custom'] = 'checked';
+ // TODO: Make assigned locations editable
+ Permission::addGlobalTags($menu['perms'], 0, ['ipxe.menu.edit']);
+ Render::addTemplate('menu-edit', $menu);
+ }
+
+ private function showEditBootEntry()
+ {
+ $params = [];
+ $id = Request::get('id', false, 'string');
+ if ($id === false) {
+ $params['exec_checked'] = 'checked';
+ $params['entryid'] = 'u-' . dechex(mt_rand(0x1000, 0xffff)) . '-' . dechex(time());
+ } else {
+ // Query existing entry
+ $row = Database::queryFirst('SELECT entryid, title, builtin, 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-boot-entry-type', $id);
+ Util::redirect('?do=serversetup');
+ }
+ $entry->addFormFields($params);
+ $params['title'] = $row['title'];
+ $params['oldentryid'] = $params['entryid'] = $row['entryid'];
}
- $data['perms'] = $perms;
- Render::addTemplate('ipxe', $data);
+ Render::addTemplate('ipxe-new-boot-entry', $params);
}
// -----------------------------------------------------------------------------------------------
@@ -131,6 +277,76 @@ class Page_ServerSetup extends Page
return true;
}
+ private function saveMenu()
+ {
+ $id = Request::post('menuid', false, 'int');
+ if ($id === false) {
+ Message::addError('main.parameter-missing', 'menuid');
+ return;
+ }
+ $menu = Database::queryFirst("SELECT m.menuid, GROUP_CONCAT(l.locationid) AS locations
+ FROM serversetup_menu m
+ LEFT JOIN serversetup_menu_location l USING (menuid)
+ WHERE menuid = :id", compact('id'));
+ if ($menu === false) {
+ Message::addError('no-such-menu', $id);
+ return;
+ }
+ if (!$this->hasMenuPermission($id, 'ipxe.menu.edit')) {
+ Message::addError('locations.no-permission-location', 'TODO');
+ return;
+ }
+ // TODO: Validate new locations to be saved (and actually save them)
+
+ Database::exec('UPDATE serversetup_menu SET title = :title, timeoutms = :timeoutms
+ WHERE menuid = :menuid', [
+ 'menuid' => $id,
+ 'title' => IPxe::sanitizeIpxeString(Request::post('title', '', 'string')),
+ 'timeoutms' => abs(Request::post('timeoutms', 0, 'int') * 1000),
+ ]);
+ if (User::hasPermission('ipxe.menu.edit', 0)) {
+ Database::exec('UPDATE serversetup_menu SET isdefault = (menuid = :menuid)', ['menuid' => $id]);
+ }
+
+ $keepIds = [];
+ $entries = Request::post('entry', false, 'array');
+ foreach ($entries as $key => $entry) {
+ $params = [
+ 'entryid' => $entry['entryid'], // TODO validate
+ 'hotkey' => MenuEntry::filterKeyName($entry['hotkey']),
+ 'title' => IPxe::sanitizeIpxeString($entry['title']),
+ 'hidden' => (int)$entry['hidden'],
+ 'sortval' => (int)$entry['sortval'],
+ 'plainpass' => $entry['plainpass'],
+ 'menuid' => $menu['menuid'],
+ ];
+ if (is_numeric($key)) {
+ $keepIds[] = $key;
+ $params['menuentryid'] = $key;
+ $params['md5pass'] = IPxe::makeMd5Pass($entry['plainpass'], $key);
+ $ret = Database::exec('UPDATE serversetup_menuentry
+ SET entryid = :entryid, hotkey = :hotkey, title = :title, hidden = :hidden, sortval = :sortval,
+ plainpass = :plainpass, md5pass = :md5pass
+ WHERE menuid = :menuid AND menuentryid = :menuentryid', $params, true);
+ } else {
+ $ret = Database::exec("INSERT INTO serversetup_menuentry
+ (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass)
+ VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, :plainpass, '')", $params, true);
+ if ($ret && !empty($entry['plainpass'])) {
+ $key = Database::lastInsertId();
+ Database::exec('UPDATE serversetup_menuentry SET md5pass = :md5pass WHERE menuentryid = :id', [
+ 'md5pass' => IPxe::makeMd5Pass($entry['plainpass'], $key),
+ 'key' => $id,
+ ]);
+ }
+ }
+ if ($ret === false) {
+ Message::addWarning('error-saving-entry', $entry['title'], Database::lastError());
+ }
+ }
+ Message::addSuccess('menu-saved');
+ }
+
private function updateLocalAddress()
{
$newAddress = Request::post('ip', 'none');
@@ -184,4 +400,50 @@ class Page_ServerSetup extends Page
exit;
}
+ private function saveBootEntry()
+ {
+ $oldEntryId = Request::post('entryid', false, 'string');
+ $newId = Request::post('newid', false, 'string');
+ if (!preg_match('/^[a-z0-9\-_]{1,16}$/', $newId)) {
+ Message::addError('main.parameter-empty', 'newid');
+ return;
+ }
+ $data = Request::post('entry', false);
+ if (!is_array($data)) {
+ Message::addError('missing-entry-data');
+ return;
+ }
+ $type = Request::post('type', false, 'string');
+ if ($type === 'exec') {
+ $entry = BootEntry::newStandardBootEntry($data);
+ } elseif ($type === 'script') {
+ $entry = BootEntry::newCustomBootEntry($data);
+ } else {
+ Message::addError('unknown-entry-type', $type);
+ return;
+ }
+ if ($entry === null) {
+ Message::addError('main.empty-field');
+ return;
+ }
+ $params = [
+ 'entryid' => $newId,
+ 'title' => Request::post('title', '', 'string'),
+ 'data' => json_encode($entry->toArray()),
+ ];
+ // New or update?
+ if (empty($oldEntryId)) {
+ // New entry
+ Database::exec('INSERT INTO serversetup_bootentry (entryid, title, builtin, data)
+ VALUES (:entryid, :title, 0, :data)', $params);
+ Message::addSuccess('boot-entry-created', $newId);
+ } else {
+ // Edit existing entry
+ $params['oldid'] = $oldEntryId;
+ Database::exec('UPDATE serversetup_bootentry SET entryid = :entryid, title = :title, data = :data
+ WHERE entryid = :oldid AND builtin = 0', $params);
+ Message::addSuccess('boot-entry-updated', $newId);
+ }
+ }
+
}
diff --git a/modules-available/serversetup-bwlp/permissions/permissions.json b/modules-available/serversetup-bwlp/permissions/permissions.json
index 44927506..aa2aa001 100644
--- a/modules-available/serversetup-bwlp/permissions/permissions.json
+++ b/modules-available/serversetup-bwlp/permissions/permissions.json
@@ -8,7 +8,19 @@
"edit.address": {
"location-aware": false
},
- "edit.menu": {
+ "ipxe.bootentry.view": {
+ "location-aware": false
+ },
+ "ipxe.bootentry.edit": {
+ "location-aware": false
+ },
+ "ipxe.menu.view": {
+ "location-aware": false
+ },
+ "ipxe.menu.edit": {
+ "location-aware": true
+ },
+ "ipxe.localboot.edit": {
"location-aware": false
}
} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/templates/download.html b/modules-available/serversetup-bwlp/templates/download.html
new file mode 100644
index 00000000..6752f7fc
--- /dev/null
+++ b/modules-available/serversetup-bwlp/templates/download.html
@@ -0,0 +1,38 @@
+<div class="panel-footer">
+ <div>
+ <div class="btn-group" role="group">
+ <a class="btn btn-default" href="?do=ServerSetup&amp;action=getimage">
+ <span class="glyphicon glyphicon-download-alt"></span>
+ {{lang_downloadImage}}
+ </a>
+ <span class="btn btn-default" data-toggle="modal" data-target="#help-usbimg"><span class="glyphicon glyphicon-question-sign"></span></span>
+ </div>
+ </div>
+</div>
+
+<div class="modal fade" id="help-usbimg" tabindex="-1" role="dialog">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ {{lang_usbImage}}
+ </div>
+ <div class="modal-body">
+ <p>{{lang_usbImgHelp}}</p>
+ <p>
+ <b>Linux</b>
+ <br>
+ {{lang_usbImgHelpLinux}}
+ </p>
+ <p>
+ <b>Windows</b>
+ <br>
+ {{lang_usbImgHelpWindows}}
+ </p>
+ <p>
+ <a href="https://rufus.akeo.ie/#download">{{lang_downloadRufus}}</a>
+ </p>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/templates/heading.html b/modules-available/serversetup-bwlp/templates/heading.html
index d68360f1..e2aa0bff 100644
--- a/modules-available/serversetup-bwlp/templates/heading.html
+++ b/modules-available/serversetup-bwlp/templates/heading.html
@@ -1 +1,3 @@
-<h1>{{lang_moduleHeading}}</h1> \ No newline at end of file
+<div class="page-header">
+ <h1>{{lang_moduleHeading}}</h1>
+</div> \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html b/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html
new file mode 100644
index 00000000..fd9e1d72
--- /dev/null
+++ b/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html
@@ -0,0 +1,108 @@
+<h2>{{lang_newBootEntryHead}}</h2>
+
+<div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_bootEntryData}}
+ </div>
+ <div class="panel-body">
+ <form method="post" action="?do=serversetup">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="savebootentry">
+ <input type="hidden" name="entryid" value="{{oldentryid}}">
+
+ <div class="form-group">
+ <div class="radio">
+ <input class="type-radio" type="radio" name="type" value="exec" id="type-exec" {{exec_checked}}>
+ <label for="type-exec">{{lang_execEntry}}</label>
+ </div>
+ <div class="radio">
+ <input class="type-radio" type="radio" name="type" value="script" id="type-script" {{script_checked}}>
+ <label for="type-script">{{lang_scriptEntry}}</label>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label for="input-id">
+ {{lang_entryId}} {{lang_idFormatHint}}
+ </label>
+ <input id="input-id" class="form-control" name="newid" value="{{entryid}}" pattern="^[a-z0-9\-_]{1,16}$" minlength="1" maxlength="16" required>
+ </div>
+ <div class="form-group">
+ <label for="input-title">
+ {{lang_entryTitle}}
+ </label>
+ <input id="input-title" class="form-control" name="title" value="{{title}}" maxlength="100">
+ </div>
+
+ <div class="type-form" id="form-exec">
+ <div class="form-group">
+ <label for="input-ex">
+ {{lang_imageToLoad}}
+ </label>
+ <input id="input-ex" class="form-control" name="entry[executable]" value="{{entry.executable}}">
+ </div>
+ <div class="form-group">
+ <label for="input-rd">
+ {{lang_initRd}}
+ </label>
+ <input id="input-rd" class="form-control" name="entry[initRd]" value="{{entry.initRd}}">
+ </div>
+ <div class="form-group">
+ <label for="input-cmd">
+ {{lang_commandLine}}
+ </label>
+ <input id="input-cmd" class="form-control" name="entry[commandLine]"
+ value="{{entry.commandLine}}">
+ </div>
+ <div class="form-group">
+ <div class="checkbox checkbox-inline">
+ <input id="exec-replace" class="form-control" type="checkbox"
+ name="entry[replace]" {{entry.replace_checked}}>
+ <label for="exec-replace">{{lang_execReplace}}</label>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="checkbox checkbox-inline">
+ <input id="exec-au" class="form-control" type="checkbox"
+ name="entry[autoUnload]" {{entry.autoUnload_checked}}>
+ <label for="exec-au">{{lang_execAutoUnload}}</label>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="checkbox checkbox-inline">
+ <input id="exec-reset" class="form-control" type="checkbox"
+ name="entry[resetConsole]" {{entry.resetConsole_checked}}>
+ <label for="exec-reset">{{lang_execResetConsole}}</label>
+ </div>
+ </div>
+ </div>
+
+ <div class="type-form" id="form-script">
+ <div class="form-group">
+ <label for="script-ta">
+ {{lang_scriptContent}}
+ </label>
+ <textarea id="script-ta" class="form-control" rows="10"
+ name="entry[script]">{{entry.script}}</textarea>
+ </div>
+ </div>
+
+ <div class="buttonbar text-right">
+ <button type="submit" class="btn btn-primary">
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ </form>
+ </div>
+</div>
+
+<script><!--
+document.addEventListener('DOMContentLoaded', function () {
+ $('.type-radio').click(function () {
+ $('.type-form').hide();
+ $('#form-' + $(this).val()).show();
+ });
+ $('.type-radio[checked]').click();
+});
+// --></script> \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/templates/ipxe.html b/modules-available/serversetup-bwlp/templates/ipxe.html
deleted file mode 100644
index f4b0b4d3..00000000
--- a/modules-available/serversetup-bwlp/templates/ipxe.html
+++ /dev/null
@@ -1,117 +0,0 @@
-<form method="post" action="?do=ServerSetup">
- <input type="text" name="prevent_autofill" id="prevent_autofill" value="" style="position:absolute;top:-2000px" tabindex="-1">
- <input type="password" name="password_fake" id="password_fake" value="" style="position:absolute;top:-2000px" tabindex="-1">
- <input type="hidden" name="action" value="ipxe">
- <input type="hidden" name="token" value="{{token}}">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{lang_bootMenu}}
- </div>
- <div class="panel-body">
- <p>
- {{lang_bootInfo}}
- </p>
- <br>
-
- <div class="form-group">
- <strong>{{lang_bootBehavior}}</strong>
- <div class="radio">
- <input type="radio" name="defaultentry" value="net" {{active-net}} id="id-net" {{perms.edit.menu.disabled}}>
- <label for="id-net">bwLehrpool</label>
- </div>
- <div class="radio">
- <input type="radio" name="defaultentry" value="hdd" {{active-hdd}} id="id-hdd" {{perms.edit.menu.disabled}}>
- <label for="id-hdd">{{lang_localHDD}}</label>
- </div>
- <div class="radio">
- <input type="radio" name="defaultentry" value="custom" {{active-custom}} id="id-custom" {{perms.edit.menu.disabled}}>
- <label for="id-custom">{{lang_customEntry}} (&quot;custom&quot;)</label>
- </div>
- </div>
-
- <div class="form-group">
- <strong>{{lang_menuDisplayTime}}</strong>
- <div class="input-group form-narrow">
- <input type="text" class="form-control" name="timeout" value="{{timeout}}" pattern="\d+" {{perms.edit.menu.readonly}}>
- <span class="input-group-addon">{{lang_seconds}}</span>
- </div>
- </div>
-
- <div class="form-group">
- <strong>{{lang_masterPassword}}</strong>
- <div class="form-narrow">
- <input type="{{password_type}}" class="form-control" name="masterpassword" value="{{masterpasswordclear}}" {{perms.edit.menu.readonly}}>
- </div>
- <i>{{lang_masterPasswordHelp}}</i>
- </div>
-
- <div class="form-group">
- <strong>{{lang_menuCustom}}</strong> <a class="btn btn-default btn-xs" data-toggle="modal" data-target="#help-custom"><span class="glyphicon glyphicon-question-sign"></span></a>
- <textarea class="form-control" name="custom" rows="8" {{perms.edit.menu.readonly}}>{{custom}}</textarea>
- </div>
- </div>
-
- <div class="panel-footer">
- <button class="btn btn-primary pull-right" name="action" value="ipxe" {{perms.edit.menu.disabled}}>{{lang_bootMenuCreate}}</button>
- <div>
- <div class="btn-group" role="group">
- <a class="btn btn-default {{perms.download.disabled}}" href="?do=ServerSetup&amp;action=getimage">
- <span class="glyphicon glyphicon-download-alt"></span>
- {{lang_downloadImage}}
- </a>
- <span class="btn btn-default" data-toggle="modal" data-target="#help-usbimg"><span class="glyphicon glyphicon-question-sign"></span></span>
- </div>
- </div>
- </div>
- </div>
-</form>
-
-<div class="modal fade" id="help-custom" tabindex="-1" role="dialog">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- {{lang_menuCustom}}
- </div>
- <div class="modal-body">
- {{lang_menuCustomHint1}}
- <br>{{lang_example}}:
- <pre>LABEL custom
- MENU LABEL ^My Boot Entry
- KERNEL http://1.2.3.4/kernel
- INITRD http://1.2.3.4/initramfs-stage31
- APPEND custom=option
- IPAPPEND 3</pre>
- {{lang_menuCustomHint2}} LABEL <strong>custom</strong>
- {{lang_menuCustomHint3}}
- </div>
- </div>
- </div>
-</div>
-
-<div class="modal fade" id="help-usbimg" tabindex="-1" role="dialog">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- {{lang_usbImage}}
- </div>
- <div class="modal-body">
- <p>{{lang_usbImgHelp}}</p>
- <p>
- <b>Linux</b>
- <br>
- {{lang_usbImgHelpLinux}}
- </p>
- <p>
- <b>Windows</b>
- <br>
- {{lang_usbImgHelpWindows}}
- </p>
- <p>
- <a href="https://rufus.akeo.ie/#download">{{lang_downloadRufus}}</a>
- </p>
- </div>
- </div>
- </div>
-</div> \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/templates/menu-edit.html b/modules-available/serversetup-bwlp/templates/menu-edit.html
new file mode 100644
index 00000000..cf10296e
--- /dev/null
+++ b/modules-available/serversetup-bwlp/templates/menu-edit.html
@@ -0,0 +1,114 @@
+<h2>{{lang_editMenuHead}}</h2>
+
+<div class="panel panel-default">
+ <div class="panel-heading">
+ {{title}}
+ {{^title}}
+ {{lang_newMenu}}
+ {{/title}}
+ </div>
+ <div class="panel-body list-group">
+ <form method="post" action="?do=serversetup">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="savemenu">
+ <input type="hidden" name="menuid" value="{{menuid}}">
+ <div class="row list-group-item">
+ <div class="col-sm-3">
+ <label for="panel-title">{{lang_menuTitle}}</label>
+ </div>
+ <div class="col-sm-9">
+ <input class="form-control" name="title" id="panel-title" type="text" value="{{title}}" {{readonly}}>
+ </div>
+ </div>
+ <div class="row list-group-item">
+ <div class="col-sm-3">
+ <label for="panel-timeout">{{lang_menuTimeout}}</label>
+ </div>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input class="form-control" name="timeout" id="panel-timeout" type="number" min="0" max="9999"
+ value="{{timeout}}" {{readonly}}>
+ <span class="input-group-addon">{{lang_seconds}}</span>
+ </div>
+ </div>
+ </div>
+ <div class="row list-group-item">
+ <div class="col-sm-3">
+ </div>
+ <div class="col-sm-9">
+ <div class="checkbox">
+ <input name="defmenu" id="panel-defmenu" type="checkbox" {{#isdefault}}checked{{/isdefault}} {{disabled}}>
+ <label for="panel-defmenu">{{lang_defaultMenu}}</label>
+ </div>
+ </div>
+ </div>
+ <div>
+ <table class="table">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{lang_entryId}}</th>
+ <th>{{lang_title}}</th>
+ <th>{{lang_hotkey}}</th>
+ <th>{{lang_sortOrder}}</th>
+ <th>{{lang_password}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#entries}}
+ <tr>
+ <td class="slx-smallcol">
+ <div class="radio radio-inline">
+ <input type="radio" name="defaultentry" value="{{menuentryid}}"
+ {{#isdefault}}checked{{/isdefault}} {{perms.ipxe.menu.edit.disabled}} {{disabled}}>
+ <label></label>
+ </div>
+ </td>
+ <td class="text-nowrap">
+ {{#entryid}}
+ <select class="form-control" name="entry[{{menuentryid}}][entryid]" {{readonly}}>
+ <option value="">{{lang_spacer}}</option>
+ {{#entrylist}}
+ <option value="{{entryid}}" {{selected}}>{{title}}</option>
+ {{/entrylist}}
+ </select>
+ {{/entryid}}
+ {{^entryid}}
+ <i>{{lang_spacer}}</i>
+ {{/entryid}}
+ </td>
+ <td {{^entryid}}colspan="2"{{/entryid}}>
+ <input class="form-control" name="entry[{{menuentryid}}][title]" value="{{title}}"
+ maxlength="100" {{readonly}}>
+ </td>
+ {{#entryid}}
+ <td>
+ <select class="form-control" name="entry[{{menuentryid}}][hotkey]" {{readonly}}>
+ <option value="">{{lang_none}}</option>
+ {{#keys}}
+ <option {{selected}}>{{key}}</option>
+ {{/keys}}
+ </select>
+ </td>
+ {{/entryid}}
+ <td>
+ <input class="form-control" name="entry[{{menuentryid}}][sortval]" value="{{sortval}}" {{readonly}}>
+ </td>
+ <td>
+ <input class="form-control" name="entry[{{menuentryid}}][plainpass]" id="panel-passwd" type="{{password_type}}"
+ value="{{plainpass}}" {{readonly}}>
+ </td>
+ </tr>
+ {{/entries}}
+ </tbody>
+ </table>
+ </div>
+ <div class="text-right">
+ <button type="submit" class="btn btn-primary" {{disabled}}>
+ <span class="glyphicon glyphicon-floppy-disk"></span>
+ {{lang_save}}
+ </button>
+ </div>
+ </form>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp/templates/menu-list.html b/modules-available/serversetup-bwlp/templates/menu-list.html
new file mode 100644
index 00000000..a862cff2
--- /dev/null
+++ b/modules-available/serversetup-bwlp/templates/menu-list.html
@@ -0,0 +1,36 @@
+<h2>{{lang_listOfMenus}}</h2>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th>{{lang_menuTitle}}</th>
+ <th class="slx-smallcol">{{lang_locationCount}}</th>
+ <th class="slx-smallcol">{{lang_isDefault}}</th>
+ <th class="slx-smallcol">{{lang_edit}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#table}}
+ <tr>
+ <td>
+ {{title}}
+ </td>
+ <td class="text-right">
+ {{locationCount}}
+ </td>
+ <td>
+ {{#isdefault}}
+ <span class="glyphicon glyphicon-ok"></span>
+ {{/isdefault}}
+ </td>
+ <td>
+ {{#allowEdit}}
+ <a href="?do=serversetup&amp;show=editmenu&amp;id={{menuid}}" class="btn btn-xs btn-default">
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ {{/allowEdit}}
+ </td>
+ </tr>
+ {{/table}}
+ </tbody>
+</table> \ No newline at end of file