diff options
Diffstat (limited to 'modules-available')
24 files changed, 2974 insertions, 393 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..4ed316a7 --- /dev/null +++ b/modules-available/serversetup-bwlp/api.inc.php @@ -0,0 +1,264 @@ +<?php + +// Menu mode + +$serverIp = Property::getServerIp(); + +// Check if required arguments are given; if not, spit out according script and chain to self +$uuid = Request::any('uuid', false, 'string'); +// Get platform - EFI or PCBIOS +$platform = Request::any('platform', false, 'string'); +$manuf = Request::any('manuf', false, 'string'); +$product = Request::any('product', false, 'string'); +$slxExtensions = Request::any('slx-extensions', false, 'int'); + +if ($platform === false || ($uuid === false && $product === false) || $slxExtensions === false) { + error_log(print_r($_SERVER, true)); + sleep(1); + $url = parse_url($_SERVER['REQUEST_URI']); + if (isset($_SERVER['SCRIPT_URI']) && preg_match('#(\w+://[^/]+)#', $_SERVER['SCRIPT_URI'], $out)) { + $urlbase = $out[1]; + } elseif (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['SERVER_NAME'])) { + $urlbase = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME']; + } elseif (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['SERVER_ADDR'])) { + $urlbase = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_ADDR']; + } else { + $urlbase = 'http://' . $serverIp; + } + $urlbase .= $url['path']; + if (empty($url['query'])) { + $arr = []; + } else { + parse_str($url['query'], $arr); + foreach ($arr as &$v) { + $v = urlencode($v); + } + unset($v); + } + $arr['uuid'] = '${uuid}'; + $arr['mac'] = '${mac}'; + $arr['manuf'] = '${manufacturer:uristring}'; + $arr['product'] = '${product:uristring}'; + $arr['platform'] = '${platform:uristring}'; + $query = '?'; + foreach ($arr as $k => $v) { + $query .= $k . '=' . $v . '&'; + } + //$query = substr($query, 0, -1); + echo <<<HERE +#!ipxe +set slxtest:string something || +iseq \${slxtest:md5} \${} && set slxext 0 || set slxext 1 || +clear slxtest || +set self {$urlbase}{$query}slx-extensions=\${slxext} +:retry +echo Chaining to \${self} +chain -ar \${self} || +echo Chaining to self failed with \${errno}, retrying in a bit... +sleep 5 +goto retry +HERE; + exit; +} +$platform = strtoupper($platform); + +$BOOT_METHODS = [ + 'EXIT' => 'exit 1', + 'COMBOOT' => 'chain /tftp/chain.c32 hd0', + 'SANBOOT' => 'sanboot --no-describe', +]; + +$ip = $_SERVER['REMOTE_ADDR']; +if (substr($ip, 0, 7) === '::ffff:') { + $ip = substr($ip, 7); +} +$menu = IPxeMenu::forClient($ip, $uuid); + + +// Get preferred localboot method, depending on system model +$localboot = false; +$model = false; +if ($uuid !== false && Module::get('statistics') !== false) { + // If we have the machine table, we rather try to look up the system model from there, using the UUID + $row = Database::queryFirst('SELECT systemmodel FROM machine WHERE machineuuid = :uuid', ['uuid' => $uuid]); + if ($row !== false && !empty($row['systemmodel'])) { + $model = $row['systemmodel']; + } +} +if ($model === false) { + // Otherwise use what iPXE sent us + 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($manuf); + $product = modfilt($product); + 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); +} + +if ($slxExtensions) { + $slxConsoleUpdate = '--update'; +} else { + $slxConsoleUpdate = ''; +} + +$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 + +set ipappend1 ip=\${ip}:{$serverIp}:\${gateway}:\${netmask} +set ipappend2 BOOTIF=01-\${mac:hexhyp} +set serverip $serverIp || + +# Clean up in case we've been chained to +imgfree || + +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 $slxConsoleUpdate --keep --picture bg-menu || + +HERE; + +$output .= $menu->getMenuDefinition('target', $platform, $slxExtensions); + +$output .= <<<HERE + +console --left 60 --top 130 --right 67 --bottom 86 $slxConsoleUpdate || +goto \${target} || +echo Could not find menu entry in script. +prompt Press any key to continue. +goto start + +HERE; + +$output .= $menu->getItemsCode($platform); + +/* + +:i5 +chain -a /tftp/memtest.0 passes=1 onepass || goto membad +prompt Memory OK. Press a key. +goto init + +:i8 +set x:int32 0 +:again +console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep || +console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep || +inc x +iseq \${x} 20 || goto again +prompt DONE. Press dein Knie. +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..8b3ce2a3 100644 --- a/modules-available/serversetup-bwlp/config.json +++ b/modules-available/serversetup-bwlp/config.json @@ -1,3 +1,8 @@ { - "category": "main.settings-server" + "category": "main.settings-server", + "dependencies" : [ + "locations", + "js_jqueryui", + "bootstrap_multiselect" + ] }
\ 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..010b660c --- /dev/null +++ b/modules-available/serversetup-bwlp/inc/bootentry.inc.php @@ -0,0 +1,257 @@ +<?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; // true == available, false == not available + + 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) { + $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/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..6429a2a7 --- /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, $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, 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..d243fd23 --- /dev/null +++ b/modules-available/serversetup-bwlp/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/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/lang/de/messages.json b/modules-available/serversetup-bwlp/lang/de/messages.json index 3e2cc834..2af5cc57 100644 --- a/modules-available/serversetup-bwlp/lang/de/messages.json +++ b/modules-available/serversetup-bwlp/lang/de/messages.json @@ -1,5 +1,17 @@ { + "boot-entry-created": "Booteintrag {{0}} erzeugt", + "boot-entry-updated": "Booteintrag {{0}} aktualisiert", + "bootentry-deleted": "Booteintrag gel\u00f6scht", + "error-saving-entry": "Fehler beim Speichern des Eintrags {{0}}: {{1}}", "image-not-found": "USB-Image nicht gefunden. Generieren Sie das Bootmen\u00fc neu.", + "invalid-boot-entry": "Ung\u00fcltiger Booteintrag: {{0}}", "invalid-ip": "Kein Interface ist auf die Adresse {{0}} konfiguriert", - "no-ip-addr-set": "Bitte w\u00e4hlen Sie die prim\u00e4re IP-Adresse des Servers" + "invalid-menu-id": "Ung\u00fcltige Men\u00fc-ID: {{0}}", + "menu-deleted": "Men\u00fc gel\u00f6scht", + "menu-saved": "Men\u00fc wurde gespeichert", + "menu-set-default": "Standardmen\u00fc wurde gesetzt", + "missing-bootentry-data": "Fehlende Daten f\u00fcr den Booteintrag", + "no-ip-addr-set": "Bitte w\u00e4hlen Sie die prim\u00e4re IP-Adresse des Servers", + "no-such-menu": "Men\u00fc mit ID {{0}} existiert nicht", + "unknown-bootentry-type": "Unbekannter Eintrags-Typ: {{0}}" }
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp/lang/de/module.json b/modules-available/serversetup-bwlp/lang/de/module.json index da71d558..6de5076e 100644 --- a/modules-available/serversetup-bwlp/lang/de/module.json +++ b/modules-available/serversetup-bwlp/lang/de/module.json @@ -1,4 +1,8 @@ { "module_name": "iPXE \/ Boot Menu", - "page_title": "PXE- und Boot-Einstellungen" + "page_title": "PXE- und Boot-Einstellungen", + "submenu_address": "Server-Adresse", + "submenu_bootentry": "Booteintr\u00e4ge verwalten", + "submenu_download": "Downloads", + "submenu_menu": "Men\u00fcs verwalten" }
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp/lang/de/permissions.json b/modules-available/serversetup-bwlp/lang/de/permissions.json index 98baec3c..a6cdbce2 100644 --- a/modules-available/serversetup-bwlp/lang/de/permissions.json +++ b/modules-available/serversetup-bwlp/lang/de/permissions.json @@ -2,5 +2,10 @@ "access-page": "Seite sehen.", "download": "USB-Image herunterladen.", "edit.address": "Boot-Adresse des Servers ausw\u00e4hlen.", - "edit.menu": "Bootmen\u00fc anpassen." + "edit.menu": "Bootmen\u00fc anpassen.", + "ipxe.bootentry.edit": "Einen Boot-Eintrag bearbeiten.", + "ipxe.bootentry.view": "Liste aller Boot-Eintr\u00e4ge sehen.", + "ipxe.localboot.edit": "Ausnahmeliste f\u00fcr Localboot-Modus bearbeiten.", + "ipxe.menu.edit": "Men\u00fc editieren.", + "ipxe.menu.view": "Liste der Men\u00fcs sehen." }
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp/lang/de/template-tags.json b/modules-available/serversetup-bwlp/lang/de/template-tags.json index 8d612ab0..4f255bbd 100644 --- a/modules-available/serversetup-bwlp/lang/de/template-tags.json +++ b/modules-available/serversetup-bwlp/lang/de/template-tags.json @@ -1,30 +1,72 @@ { "lang_active": "Aktiv", + "lang_add": "Hinzuf\u00fcgen", + "lang_addBootentry": "Booteintrag hinzuf\u00fcgen", + "lang_addMenu": "Men\u00fc hinzuf\u00fcgen", + "lang_archAgnostic": "Architekturunabh\u00e4ngig", + "lang_archBoth": "BIOS und EFI", + "lang_archSelector": "Architekturauswahl", + "lang_biosOnly": "Nur BIOS", "lang_bootAddress": "Boot-Adresse des Servers", "lang_bootBehavior": "Standard-Bootverhalten", + "lang_bootEntryData": "Daten des Booteintrags", "lang_bootHint": "Das Bootmen\u00fc muss nach einer \u00c4nderung der IP-Adresse neu generiert werden. In der Regel geschieht dies automatisch, der Vorgang kann in der Sektion Bootmen\u00fc allerdings auch manuell ausgel\u00f6st werden.", "lang_bootInfo": "Hier k\u00f6nnen Anpassungen am Erscheinungsbild des Bootmen\u00fcs vorgenommen werden.", "lang_bootMenu": "Bootmen\u00fc", "lang_bootMenuCreate": "Bootmen\u00fc erzeugen", + "lang_bootentryDeleteConfirm": "Sind Sie sicher, dass Sie diesen Booteintrag l\u00f6schen wollen?", + "lang_bootentryTitle": "Booteintrag", "lang_chooseIP": "Bitte w\u00e4hlen Sie die IP-Adresse, \u00fcber die der Server von den Clients zum Booten angesprochen werden soll.", + "lang_commandLine": "Command line", "lang_customEntry": "Eigener Eintrag", "lang_downloadImage": "USB-Image herunterladen", "lang_downloadRufus": "Rufus herunterladen", + "lang_editBuiltinWarn": "Achtung! Sie bearbeiten einen der vorgegebenen Eintr\u00e4ge! Bei einem Update k\u00f6nnten Ihre \u00c4nderungen wieder \u00fcberschrieben werden", + "lang_editMenuHead": "Men\u00fc bearbeiten", + "lang_efiOnly": "Nur EFI", + "lang_entryChooserTitle": "Booteintrag ausw\u00e4hlen", + "lang_entryId": "ID", + "lang_entryTitle": "Bezeichnung", "lang_example": "Beispiel", + "lang_execAutoUnload": "Nach Ausf\u00fchrung entladen (--autofree)", + "lang_execReplace": "Aktuellen iPXE-Stack erstzen (--replace)", + "lang_execResetConsole": "Konsole vor Ausf\u00fchrung zur\u00fccksetzen", + "lang_forceRecompile": "Jetzt neu compilieren", "lang_generationFailed": "Erzeugen des Bootmen\u00fcs fehlgeschlagen. Der Netzwerkboot von bwLehrpool wird wahrscheinlich nicht funktionieren. Wenn Sie den Fehler nicht selbst beheben k\u00f6nnen, melden Sie bitte die Logausgabe an das bwLehrpool-Projekt.", + "lang_globalMenuWarning": "Dieses Men\u00fc ist keinem Raum zugeordnet", + "lang_hotkey": "Hotkey", + "lang_idFormatHint": "(Max. 16 Zeichen, nur a-z 0-9 - _)", + "lang_imageToLoad": "Zu ladendes Image (z.B. Kernel)", + "lang_initRd": "Zu ladendes initramfs", + "lang_isDefault": "Standard", + "lang_listOfMenus": "Men\u00fcliste", "lang_localHDD": "Lokale HDD", + "lang_locationCount": "Anzahl Orte", "lang_masterPassword": "Master-Passwort", "lang_masterPasswordHelp": "Das Master-Passwort wird ben\u00f6tigt, um einen Booteintrag direkt am Client tempor\u00e4r durch Dr\u00fccken der Tab-Taste zu editieren. Da dies f\u00fcr Manipulation am Client genutzt werden kann, sollte diese Funktion unbedingt mit einem Passwort gesch\u00fctzt werden.", "lang_menuCustom": "Benutzerdefinierter Men\u00fczusatz", "lang_menuCustomHint1": "Hier haben Sie die M\u00f6glichkeit, eigenen Men\u00fc-Code zum angezeigten PXE-Men\u00fc hinzuzuf\u00fcgen, um z.B. auf weitere PXE-Server zu verweisen. Das Format entspricht dem syslinux Men\u00fcformat.", "lang_menuCustomHint2": "Sie k\u00f6nnen ein oder mehrere Eintr\u00e4ge erzeugen. Wenn Sie einen Eintrag erzeugen m\u00f6chten, der automatisch gestartet wird, wenn der Benutzer keine Auswahl t\u00e4tigt, vergeben Sie als", "lang_menuCustomHint3": "und w\u00e4hlen Sie als Standard-Bootverhalten ebenfalls custom.", + "lang_menuDeleteConfirm": "Sind Sie sicher, dass Sie dieses Men\u00fc l\u00f6schen wollen?", "lang_menuDisplayTime": "Anzeigedauer des Men\u00fcs", "lang_menuGeneration": "Erzeugen des Bootmen\u00fcs", + "lang_menuLocations": "Zugewiesene Orte", + "lang_menuTimeout": "Timeout", + "lang_menuTitle": "Men\u00fc", "lang_moduleHeading": "iPXE \/ Boot Menu", + "lang_newBootEntryHead": "Neuer Booteintrag", + "lang_newMenu": "Neues Men\u00fc", + "lang_none": "(keine)", "lang_pxeBuilt": "PXE-Binary gebaut", + "lang_recompileHint": "iPXE-Binaries jetzt neu kompilieren. Normalerweise wird dieser Vorgang bei \u00c4nderungen automatisch ausgef\u00fchrt. Sollten Bootprobleme auftreten, k\u00f6nnen Sie hier den Vorgang manuell ansto\u00dfen.", + "lang_scriptContent": "Script", "lang_seconds": "Sekunden", "lang_set": "Setzen", + "lang_spacer": "Abstandhalter\/\u00dcberschrift", + "lang_title": "Titel", + "lang_typeExecEntry": "Standardeintrag", + "lang_typeScriptEntry": "Benutzerdefiniertes Script", "lang_usbBuilt": "USB-Image gebaut", "lang_usbImage": "USB-Image", "lang_usbImgHelp": "Mit dem USB-Image k\u00f6nnen Sie einen bootbaren USB-Stick erstellen, \u00fcber den sich bwLehrpool an Rechnern starten l\u00e4sst, die keinen Netzwerkboot unterst\u00fctzen, bzw. f\u00fcr die keine entsprechende DHCP-Konfiguration vorhanden ist. Dies erfordert dann lediglich, dass in der BIOS-Konfiguration des Rechners USB-Boot zugelassen ist. Der Stick dient dabei lediglich als Einstiegspunkt; es ist nach wie vor ein bwLehrpool-Satellitenserver f\u00fcr den eigentlichen Bootvorgang von N\u00f6ten.", diff --git a/modules-available/serversetup-bwlp/lang/en/template-tags.json b/modules-available/serversetup-bwlp/lang/en/template-tags.json index 9bb55f93..121ed3e7 100644 --- a/modules-available/serversetup-bwlp/lang/en/template-tags.json +++ b/modules-available/serversetup-bwlp/lang/en/template-tags.json @@ -1,18 +1,26 @@ { "lang_active": "Active", + "lang_addBootentry": "Add Bootentry", + "lang_addMenu": "Add Menu", "lang_bootAddress": "Boot Address of the Server", "lang_bootBehavior": "Default Boot Behavior", + "lang_bootentryTitle": "Bootentry", "lang_bootHint": "The Boot menu must be recreated after changing the IP address. Usually this is done automatically, but the process can also be triggered manually in the section of the boot menu.", "lang_bootInfo": "Here adjustments can be made to the appearance of the boot menu.", "lang_bootMenu": "Boot Menu", "lang_bootMenuCreate": "Create Boot Menu", "lang_chooseIP": "Please select the IP address that the client server will use to boot.", "lang_customEntry": "Custom entry", + "lang_bootentryDeleteConfirm": "Are you sure you want to delete this bootentry?", + "lang_menuDeleteConfirm": "Are you sure you want to delete this menu?", "lang_downloadImage": "Download USB Image", "lang_downloadRufus": "Download Rufus", "lang_example": "Example", "lang_generationFailed": "Could not generate boot menu. The bwLehrpool-System might not work properly. If you can't fix the problem, please report the error log below to the bwLehrpool project.", + "lang_isDefault": "Default", + "lang_listOfMenus": "Menulist", "lang_localHDD": "Local HDD", + "lang_locationCount": "Number of Locations", "lang_masterPassword": "Master Password", "lang_masterPasswordHelp": "The master password is required to edit a boot menu entry. This should be set for security reasons.", "lang_menuCustom": "Custom Extra Menu", @@ -21,6 +29,7 @@ "lang_menuCustomHint3": "and select as the default boot behavior custom as well.", "lang_menuDisplayTime": "Menu Display Time", "lang_menuGeneration": "Generating boot menu...", + "lang_menuTitle": "Menu", "lang_moduleHeading": "iPXE \/ Boot Menu", "lang_pxeBuilt": "Built PXE binary", "lang_seconds": "Seconds", diff --git a/modules-available/serversetup-bwlp/page.inc.php b/modules-available/serversetup-bwlp/page.inc.php index 52b3afe4..f8a21227 100644 --- a/modules-available/serversetup-bwlp/page.inc.php +++ b/modules-available/serversetup-bwlp/page.inc.php @@ -3,11 +3,26 @@ class Page_ServerSetup extends Page { - private $taskStatus; + private $addrListTask; + private $compileTask = null; private $currentAddress; private $currentMenu; private $hasIpSet = false; + private function getCompileTask() + { + if ($this->compileTask !== null) + return $this->compileTask; + $this->compileTask = Property::get('ipxe-task-id'); + if ($this->compileTask !== false) { + $this->compileTask = Taskmanager::status($this->compileTask); + if (!Taskmanager::isTask($this->compileTask) || Taskmanager::isFinished($this->compileTask)) { + $this->compileTask = false; + } + } + return $this->compileTask; + } + protected function doPreprocess() { User::load(); @@ -17,6 +32,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(); @@ -31,6 +52,14 @@ class Page_ServerSetup extends Page $this->getLocalAddresses(); } + if ($action === 'compile') { + User::assertPermission("edit.address"); + if ($this->getCompileTask() === false) { + Trigger::ipxe(); + } + Util::redirect('?do=serversetup'); + } + if ($action === 'ip') { User::assertPermission("edit.address"); // New address is to be set @@ -38,10 +67,29 @@ class Page_ServerSetup extends Page $this->updateLocalAddress(); } - if ($action === 'ipxe') { - User::assertPermission("edit.menu"); - // iPXE stuff changes - $this->updatePxeMenu(); + if ($action === 'savebootentry') { + User::assertPermission('ipxe.bootentry.edit'); + $this->saveBootEntry(); + } + + if ($action === 'deleteBootentry') { + User::assertPermission('ipxe.bootentry.edit'); + $this->deleteBootEntry(); + } + + if ($action === 'savemenu') { + User::assertPermission('ipxe.menu.edit'); + $this->saveMenu(); + } + + if ($action === 'deleteMenu') { + // Permcheck in function + $this->deleteMenu(); + } + + if ($action === 'setDefaultMenu') { + User::assertPermission('ipxe.menu.edit', 0); + $this->setDefaultMenu(); } if (Request::isPost()) { @@ -49,75 +97,288 @@ class Page_ServerSetup extends Page } User::assertPermission('access-page'); + + if (User::hasPermission('ipxe.*')) { + Dashboard::addSubmenu('?do=serversetup&show=menu', Dictionary::translate('submenu_menu', true)); + Dashboard::addSubmenu('?do=serversetup&show=bootentry', Dictionary::translate('submenu_bootentry', 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'); + + $task = $this->getCompileTask(); if ($task !== false) { - $task = Taskmanager::status($task); - if (!Taskmanager::isTask($task) || Taskmanager::isFinished($task)) { - $task = false; + $files = []; + if ($task['data'] && $task['data']['files']) { + foreach ($task['data']['files'] as $k => $v) { + $files[] = ['name' => $k, 'namehyphen' => str_replace(['/', '.'], '-', $k)]; + } } + Render::addTemplate('ipxe_update', array('taskid' => $task['id'], 'files' => $files)); } - if ($task !== false) { - Render::addTemplate('ipxe_update', array('taskid' => $task['id'])); + + 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; + case 'bootentry': + User::assertPermission('ipxe.bootentry.view'); + $this->showBootentryList(); + break; + case 'address': + User::assertPermission('edit.address'); + $this->showEditAddress(); + break; + default: + Util::redirect('?do=serversetup'); + break; } + } - Permission::addGlobalTags($perms, null, ['edit.menu', 'edit.address', 'download']); + private function showDownload() + { + // TODO: Make nicer, support more variants (taskmanager-plugin) + Render::addTemplate('download'); + } - Render::addTemplate('ipaddress', array( - 'ips' => $this->taskStatus['data']['addresses'], - 'chooseHintClass' => $this->hasIpSet ? '' : 'alert alert-danger', - 'editAllowed' => User::hasPermission("edit.address"), - 'perms' => $perms, + private function showBootentryList() + { + $allowEdit = User::hasPermission('ipxe.bootentry.edit'); + + $res = Database::simpleQuery("SELECT entryid, hotkey, title, builtin FROM serversetup_bootentry"); + $bootentryTable = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $bootentryTable[] = $row; + } + + Render::addTemplate('bootentry-list', array( + 'bootentryTable' => $bootentryTable, + 'allowEdit' => $allowEdit, + )); + } + + 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"); + $menuTable = []; + 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); + $menuTable[] = $row; + } + + Render::addTemplate('menu-list', array( + 'menuTable' => $menuTable, + 'showSetDefault' => User::hasPermission('ipxe.menu.edit', 0) )); - $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; + } + } + return $allowEdit; + } + + private function showEditMenu() + { + $id = Request::get('id', false, 'int'); + // if = edit, else = add new + if ($id !== 0) { + $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid, isdefault + FROM serversetup_menu WHERE menuid = :id", compact('id')); + } else { + $menu = []; + $menu['menuid'] = 0; + $menu['timeoutms'] = 0; + $menu['title'] = ''; + $menu['defaultentryid'] = null; + $menu['isdefault'] = false; } - if (!isset($data['defaultentry'])) { - $data['defaultentry'] = 'net'; + + if ($menu === false) { + Message::addError('invalid-menu-id', $id); + Util::redirect('?do=serversetup&show=menu'); + } + if ($id !== 0 && !$this->hasMenuPermission($id, 'ipxe.menu.edit')) { + $menu['readonly'] = 'readonly'; + $menu['disabled'] = 'disabled'; + $menu['plainpass'] = ''; } - if ($data['defaultentry'] === 'net') { - $data['active-net'] = 'checked'; + if (!User::hasPermission('ipxe.menu.edit', 0)) { + $menu['globalMenuWarning'] = true; } - 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 ORDER BY sortval ASC", compact('id')); + $menu['keys'] = array_map(function ($item) { return ['key' => $item]; }, MenuEntry::getKeyList()); + $menu['entrylist'] = Database::queryAll("SELECT entryid, title, hotkey, data FROM serversetup_bootentry ORDER BY title ASC"); + foreach ($menu['entrylist'] as &$bootentry) { + $bootentry['json'] = $bootentry['data']; + $bootentry['data'] = json_decode($bootentry['data'], true); + if (array_key_exists('arch', $bootentry['data'])) { + $bootentry['data']['PCBIOS'] = array('executable' => $bootentry['data']['executable']['PCBIOS'], + 'initRd' => $bootentry['data']['initRd']['PCBIOS'], + 'commandLine' => $bootentry['data']['commandLine']['PCBIOS']); + $bootentry['data']['EFI'] = array('executable' => $bootentry['data']['executable']['EFI'], + 'initRd' => $bootentry['data']['initRd']['EFI'], + 'commandLine' => $bootentry['data']['commandLine']['EFI']); + + if ($bootentry['data']['arch'] === 'PCBIOS') { + $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_biosOnly', true); + unset($bootentry['data']['EFI']); + } else if ($bootentry['data']['arch'] === 'EFI') { + $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_efiOnly', true); + unset($bootentry['data']['PCBIOS']); + } else { + $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_archBoth', true); + } + + } else { + $bootentry['data']['arch'] = Dictionary::translateFile('template-tags','lang_archAgnostic', true); + $bootentry['data']['archAgnostic'] = array('executable' => $bootentry['data']['executable'], + 'initRd' => $bootentry['data']['initRd'], + 'commandLine' => $bootentry['data']['commandLine']); + } + } + foreach ($menu['entries'] as &$entry) { + $entry['isdefault'] = ($entry['menuentryid'] == $menu['defaultentryid']); + // TODO: plainpass only when permissions + } + // TODO: Make assigned locations editable + + $currentLocations = Database::queryColumnArray('SELECT locationid FROM serversetup_menu_location + WHERE menuid = :menuid', array('menuid' => $id)); + $menu['locations'] = Location::getLocations($currentLocations); + + // if user has no permission to edit for this location, disable the location in the select + $allowedEditLocations = User::getAllowedLocations('ipxe.menu.edit'); + foreach ($menu['locations'] as &$loc) { + if (!in_array($loc["locationid"], $allowedEditLocations)) { + $loc["disabled"] = "disabled"; + } } - if ($data['defaultentry'] === 'custom') { - $data['active-custom'] = 'checked'; + + 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()); + $params['entries'] = [ + ['mode' => 'PCBIOS'], + ['mode' => 'EFI'], + ]; + } 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-bootentry-type', $id); + Util::redirect('?do=serversetup'); + } + $entry->addFormFields($params); + $params['title'] = $row['title']; + $params['oldentryid'] = $params['entryid'] = $row['entryid']; + $params['builtin'] = $row['builtin']; } - $data['perms'] = $perms; - Render::addTemplate('ipxe', $data); + + Render::addTemplate('ipxe-new-boot-entry', $params); + } + + private function showEditAddress() + { + Render::addTemplate('ipaddress', array( + 'ips' => $this->addrListTask['data']['addresses'], + 'chooseHintClass' => $this->hasIpSet ? '' : 'alert alert-danger', + 'disabled' => ($this->getCompileTask() === false) ? '' : 'disabled', + )); } // ----------------------------------------------------------------------------------------------- private function getLocalAddresses() { - $this->taskStatus = Taskmanager::submit('LocalAddressesList', array()); + $this->addrListTask = Taskmanager::submit('LocalAddressesList', array()); - if ($this->taskStatus === false) { - $this->taskStatus['data']['addresses'] = false; + if ($this->addrListTask === false) { + $this->addrListTask['data']['addresses'] = false; return false; } - if (!Taskmanager::isFinished($this->taskStatus)) { // TODO: Async if just displaying - $this->taskStatus = Taskmanager::waitComplete($this->taskStatus['id'], 4000); + if (!Taskmanager::isFinished($this->addrListTask)) { // TODO: Async if just displaying + $this->addrListTask = Taskmanager::waitComplete($this->addrListTask['id'], 4000); } - if (Taskmanager::isFailed($this->taskStatus) || !isset($this->taskStatus['data']['addresses'])) { - $this->taskStatus['data']['addresses'] = false; + if (Taskmanager::isFailed($this->addrListTask) || !isset($this->addrListTask['data']['addresses'])) { + $this->addrListTask['data']['addresses'] = false; return false; } $sortIp = array(); - foreach (array_keys($this->taskStatus['data']['addresses']) as $key) { - $item = & $this->taskStatus['data']['addresses'][$key]; + foreach (array_keys($this->addrListTask['data']['addresses']) as $key) { + $item = & $this->addrListTask['data']['addresses'][$key]; if (!isset($item['ip']) || !preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $item['ip']) || substr($item['ip'], 0, 4) === '127.') { - unset($this->taskStatus['data']['addresses'][$key]); + unset($this->addrListTask['data']['addresses'][$key]); continue; } if ($this->currentAddress === $item['ip']) { @@ -127,15 +388,197 @@ class Page_ServerSetup extends Page $sortIp[] = $item['ip']; } unset($item); - array_multisort($sortIp, SORT_STRING, $this->taskStatus['data']['addresses']); + array_multisort($sortIp, SORT_STRING, $this->addrListTask['data']['addresses']); return true; } + private function deleteBootEntry() { + $id = Request::post('deleteid', false, 'string'); + if ($id === false) { + Message::addError('main.parameter-missing', 'deleteid'); + return; + } + Database::exec("DELETE FROM serversetup_bootentry WHERE entryid = :entryid", array("entryid" => $id)); + // TODO: Redirect to &show=bootentry + Message::addSuccess('bootentry-deleted'); + } + + private function setDefaultMenu() + { + $id = Request::post('menuid', false, 'int'); + if ($id === false) { + Message::addError('main.parameter-missing', 'menuid'); + return; + } + Database::exec('UPDATE serversetup_menu SET isdefault = (menuid = :menuid)', ['menuid' => $id]); + Message::addSuccess('menu-set-default'); + } + + private function deleteMenu() + { + $id = Request::post('deleteid', false, 'int'); + if ($id === false) { + Message::addError('main.parameter-missing', 'deleteid'); + return; + } + if (!$this->hasMenuPermission($id, 'ipxe.menu.edit')) { + Message::addError('locations.no-permission-location', $id); + return; + } + Database::exec("DELETE FROM serversetup_menu WHERE menuid = :menuid", array("menuid" => $id)); + Message::addSuccess('menu-deleted'); + } + + private function saveMenu() + { + $id = Request::post('menuid', false, 'int'); + if ($id === false) { + Message::addError('main.parameter-missing', 'menuid'); + return; + } + + $locationids = Request::post('locations', [], "ARRAY"); + // check if the user is allowed to edit the menu on the affected locations + $allowedEditLocations = User::getAllowedLocations('ipxe.menu.edit'); + $currentLocations = Database::queryColumnArray('SELECT locationid FROM serversetup_menu_location + WHERE menuid = :menuid', array('menuid' => $id)); + // permission denied if the user tries to assign or remove a menu to/from locations he has no edit rights for + // or if the user tries to save a menu without locations but does not have the permission for the root location (0) + if (!in_array(0, $allowedEditLocations) + && ( + (!empty(array_diff($locationids, $allowedEditLocations)) && !empty(array_diff($currentLocations, $allowedEditLocations))) + || empty($locationids) + ) + ) { + Message::addError('main.no-permission'); + Util::redirect('?do=serversetup'); + } + + $insertParams = [ + 'title' => IPxe::sanitizeIpxeString(Request::post('title', '', 'string')), + 'timeoutms' => abs(Request::post('timeout', 0, 'int') * 1000), + ]; + if ($id === 0) { + Database::exec("INSERT INTO serversetup_menu (title, timeoutms, isdefault) VALUES (:title, :timeoutms, 0)", $insertParams); + $menu['menuid'] = $id = Database::lastInsertId(); + } else { + $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; + } + $insertParams['menuid'] = $id; + Database::exec('UPDATE serversetup_menu SET title = :title, timeoutms = :timeoutms + WHERE menuid = :menuid', $insertParams); + } + + $keepIds = []; + $entries = Request::post('entry', false, 'array'); + $wantedDefaultEntryId = Request::post('defaultentry', null, 'string'); + $defaultEntryId = null; + + if ($entries) { + foreach ($entries as $key => $entry) { + if (!isset($entry['sortval'])) { + error_log(print_r($entry, true)); + continue; + } + // Fallback defaults + $entry += [ + 'entryid' => null, + 'title' => '', + 'hidden' => 0, + 'plainpass' => '', + ]; + $params = [ + 'title' => IPxe::sanitizeIpxeString($entry['title']), + 'sortval' => (int)$entry['sortval'], + 'menuid' => $menu['menuid'], + ]; + if (empty($entry['entryid'])) { + // Spacer + $params += [ + 'entryid' => null, + 'hotkey' => '', + 'hidden' => 0, // Doesn't make any sense + 'plainpass' => '', // Doesn't make any sense + ]; + } else { + $params += [ + 'entryid' => $entry['entryid'], // TODO validate? + 'hotkey' => MenuEntry::filterKeyName($entry['hotkey']), + 'hidden' => (int)$entry['hidden'], // TODO (needs hotkey to make sense) + 'plainpass' => $entry['plainpass'], + ]; + } + if (is_numeric($key)) { + if ((string)$key === $wantedDefaultEntryId) { // Check now that we have generated our key + $defaultEntryId = $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) { + $newKey = Database::lastInsertId(); + if ((string)$key === $wantedDefaultEntryId) { // Check now that we have generated our key + $defaultEntryId = $newKey; + } + $keepIds[] = (int)$newKey; + if (!empty($entry['plainpass'])) { + Database::exec('UPDATE serversetup_menuentry SET md5pass = :md5pass WHERE menuentryid = :id', [ + 'md5pass' => IPxe::makeMd5Pass($entry['plainpass'], $newKey), + 'id' => $newKey, + ]); + } + } + } + + if ($ret === false) { + Message::addWarning('error-saving-entry', $entry['title'], Database::lastError()); + } + } + Database::exec('DELETE FROM serversetup_menuentry WHERE menuid = :menuid AND menuentryid NOT IN (:keep)', + ['menuid' => $menu['menuid'], 'keep' => $keepIds]); + // Set default entry + Database::exec('UPDATE serversetup_menu SET defaultentryid = :default WHERE menuid = :menuid', + ['menuid' => $menu['menuid'], 'default' => $defaultEntryId]); + } else { + Database::exec('DELETE FROM serversetup_menuentry WHERE menuid = :menuid', ['menuid' => $menu['menuid']]); + Database::exec('UPDATE serversetup_menu SET defaultentryid = NULL WHERE menuid = :menuid', ['menuid' => $menu['menuid']]); + } + + Database::exec('DELETE FROM serversetup_menu_location WHERE menuid = :menuid', ['menuid' => $menu['menuid']]); + if (!empty($locationids)) { + Database::exec('DELETE FROM serversetup_menu_location WHERE locationid IN (:locationids)', ['locationids' => $locationids]); + foreach ($locationids as $locationid) { + Database::exec('INSERT INTO serversetup_menu_location (menuid, locationid) VALUES (:menuid, :locationid)', + ['menuid' => $menu['menuid'], 'locationid' => $locationid]); + } + } + + Message::addSuccess('menu-saved'); + } + private function updateLocalAddress() { - $newAddress = Request::post('ip', 'none'); + $newAddress = Request::post('ip', 'none', 'string'); $valid = false; - foreach ($this->taskStatus['data']['addresses'] as $item) { + foreach ($this->addrListTask['data']['addresses'] as $item) { if ($item['ip'] !== $newAddress) continue; $valid = true; @@ -150,27 +593,6 @@ class Page_ServerSetup extends Page Util::redirect(); } - private function updatePxeMenu() - { - $timeout = Request::post('timeout', 10); - if ($timeout === '') - $timeout = 0; - if (!is_numeric($timeout) || $timeout < 0) { - Message::addError('main.value-invalid', 'timeout', $timeout); - } - $this->currentMenu['defaultentry'] = Request::post('defaultentry', 'net'); - $this->currentMenu['timeout'] = $timeout; - $this->currentMenu['custom'] = Request::post('custom', ''); - $this->currentMenu['masterpasswordclear'] = Request::post('masterpassword', ''); - if (empty($this->currentMenu['masterpasswordclear'])) - $this->currentMenu['masterpassword'] = 'invalid'; - else - $this->currentMenu['masterpassword'] = Crypto::hash6($this->currentMenu['masterpasswordclear']); - Property::setBootMenu($this->currentMenu); - Trigger::ipxe(); - Util::redirect('?do=ServerSetup'); - } - private function handleGetImage() { $file = "/opt/openslx/ipxe/openslx-bootstick.raw"; @@ -184,4 +606,51 @@ 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-bootentry-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-bootentry-type', $type); + return; + } + if ($entry === null) { + Message::addError('main.empty-field'); + Util::redirect('?do=serversetup&show=bootentry'); + } + $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', $params); + Message::addSuccess('boot-entry-updated', $newId); + } + Util::redirect('?do=serversetup&show=bootentry'); + } + } 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/bootentry-list.html b/modules-available/serversetup-bwlp/templates/bootentry-list.html new file mode 100644 index 00000000..929b8c47 --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/bootentry-list.html @@ -0,0 +1,73 @@ +<table class="table"> + <thead> + <tr> + <th>{{lang_bootentryTitle}}</th> + <th>Hotkey</th> + <th class="slx-smallcol">{{lang_edit}}</th> + <th class="slx-smallcol">{{lang_delete}}</th> + </tr> + </thead> + <tbody> + {{#bootentryTable}} + <tr> + <td> + {{title}} + </td> + <td> + {{hotkey}} + </td> + <td align="center"> + {{#allowEdit}} + <a href="?do=serversetup&show=editbootentry&id={{entryid}}" class="btn btn-xs btn-default"> + <span class="glyphicon glyphicon-edit"></span> + </a> + {{/allowEdit}} + </td> + <td align="center"> + {{#allowEdit}} + <button type="button" class="btn btn-xs btn-danger" data-toggle="modal" data-target="#deleteModal" onclick="deleteBootentry('{{entryid}}', '{{builtin}}')"> + <span class="glyphicon glyphicon-trash"></span> + </button> + {{/allowEdit}} + </td> + </tr> + {{/bootentryTable}} + </tbody> +</table> +<div class="pull-right"> + {{#allowEdit}} + <a href="?do=serversetup&show=editbootentry" class="btn btn-success"> + <span class="glyphicon glyphicon-plus"></span> + {{lang_addBootentry}} + </a> + {{/allowEdit}} +</div> + +<!-- Modals --> +<form method="post" action="?do=serversetup"> + <input type="hidden" name="token" value="{{token}}"> + <div class ="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title" id="myModalLabel">{{lang_delete}}</h4> + </div> + <div class="modal-body"> + <p>{{lang_bootentryDeleteConfirm}}</p> + </div> + <div class="modal-footer"> + <input type="hidden" id="delete-bootentry-id" name="deleteid" value=""> + <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button> + <button type="submit" name="action" value="deleteBootentry" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span> {{lang_delete}}</button> + </div> + </div> + </div> + </div> +</form> + +<script> + function deleteBootentry(entryid) { + $("#delete-bootentry-id").val(entryid); + } +</script>
\ 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&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">×</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/ipaddress.html b/modules-available/serversetup-bwlp/templates/ipaddress.html index 8d73dfac..ea19c417 100644 --- a/modules-available/serversetup-bwlp/templates/ipaddress.html +++ b/modules-available/serversetup-bwlp/templates/ipaddress.html @@ -20,7 +20,7 @@ {{/default}} {{^default}} <td> - <button class="btn btn-primary btn-xs" name="ip" value="{{ip}}" {{perms.edit.address.disabled}}> + <button class="btn btn-primary btn-xs" name="ip" value="{{ip}}" {{disabled}}> <span class="glyphicon glyphicon-flag"></span> {{lang_set}} </button> @@ -30,8 +30,15 @@ {{/ips}} </table> <p> - {{lang_bootHint}} + {{lang_recompileHint}} </p> </form> + <form method="post" action="?do=ServerSetup"> + <input type="hidden" name="token" value="{{token}}"> + <button class="btn btn-default" name="action" value="compile" {{disabled}}> + <span class="glyphicon glyphicon-refresh"></span> + {{lang_forceRecompile}} + </button> + </form> </div> </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..fe496029 --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html @@ -0,0 +1,158 @@ +<h2>{{lang_newBootEntryHead}}</h2> + +{{#builtin}} + <div class="alert alert-warning"> + {{lang_editBuiltinWarn}} + </div> +{{/builtin}} + +<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_typeExecEntry}}</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_typeScriptEntry}}</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="form-group"> + <label for="arch-selector"> + {{lang_archSelector}} + </label> + <select id="arch-selector" class="form-control" name="entry[arch]"> + <option value="agnostic" {{agnostic_selected}}>{{lang_archAgnostic}}</option> + <option value="PCBIOS" {{PCBIOS_selected}}>{{lang_biosOnly}}</option> + <option value="EFI" {{EFI_selected}}>{{lang_efiOnly}}</option> + <option value="PCBIOS-EFI" {{PCBIOS-EFI_selected}}>{{lang_archBoth}}</option> + </select> + </div> + + <div class="type-form" id="form-exec"> + <div class="row"> + {{#entries}} + <div class="mode-class col-md-6" id="col-{{mode}}"> + <div class="panel panel-default"> + <div class="panel-body"> + <h4 class="arch-heading">{{mode}}</h4> + <div class="form-group"> + <label for="input-ex"> + {{lang_imageToLoad}} + </label> + <input id="input-ex" class="form-control" name="entry[executable][{{mode}}]" value="{{executable}}"> + </div> + <div class="form-group"> + <label for="input-rd"> + {{lang_initRd}} + </label> + <input id="input-rd" class="form-control" name="entry[initRd][{{mode}}]" value="{{initRd}}"> + </div> + <div class="form-group"> + <label for="input-cmd"> + {{lang_commandLine}} + </label> + <input id="input-cmd" class="form-control" name="entry[commandLine][{{mode}}]" + value="{{commandLine}}"> + </div> + <div class="form-group"> + <div class="checkbox checkbox-inline"> + <input id="exec-replace-{{mode}}" class="form-control" type="checkbox" + name="entry[replace][{{mode}}]" {{replace_checked}}> + <label for="exec-replace-{{mode}}">{{lang_execReplace}}</label> + </div> + </div> + <div class="form-group"> + <div class="checkbox checkbox-inline"> + <input id="exec-au-{{mode}}" class="form-control" type="checkbox" + name="entry[autoUnload][{{mode}}]" {{autoUnload_checked}}> + <label for="exec-au-{{mode}}">{{lang_execAutoUnload}}</label> + </div> + </div> + <div class="form-group"> + <div class="checkbox checkbox-inline"> + <input id="exec-reset-{{mode}}" class="form-control" type="checkbox" + name="entry[resetConsole][{{mode}}]" {{resetConsole_checked}}> + <label for="exec-reset-{{mode}}">{{lang_execResetConsole}}</label> + </div> + </div> + </div> + </div> + </div> + {{/entries}} + </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> + + {{#builtin}} + <div class="alert alert-warning"> + {{lang_editBuiltinWarn}} + </div> + {{/builtin}} + + <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(); + var $as = $('#arch-selector'); + $as.change(function() { + var v = $as.val(); + if (v === 'agnostic') { + v = 'PCBIOS'; + $('.arch-heading').hide(); + } else { + $('.arch-heading').show(); + } + var vs = v.split('-'); + var cols = 12 / vs.length; + $('.mode-class').hide(); + for (var i = 0; i < vs.length; ++i) { + $('#col-' + vs[i]).attr('class', 'mode-class col-md-' + cols).show(); + } + }).change(); +}); +// --></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}} ("custom")</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&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">×</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">×</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/ipxe_update.html b/modules-available/serversetup-bwlp/templates/ipxe_update.html index c5aafa1c..344d3905 100644 --- a/modules-available/serversetup-bwlp/templates/ipxe_update.html +++ b/modules-available/serversetup-bwlp/templates/ipxe_update.html @@ -1,31 +1,35 @@ <div class="panel panel-default"> <div class="panel-heading">{{lang_menuGeneration}}</div> <div class="panel-body"> - <div id="built-pxe" class="invisible"> - <span class="glyphicon glyphicon-ok"></span> - {{lang_pxeBuilt}} - </div> - <div id="built-usb" class="invisible"> - <span class="glyphicon glyphicon-ok"></span> - {{lang_usbBuilt}} + <div id="file-list"> + {{#files}} + <div id="built-{{namehyphen}}"> + <span class="glyphicon glyphicon-question-sign"></span> + {{name}} + </div> + {{/files}} </div> <div id="genfailed" class="collapse"> <div class="alert alert-danger"> {{lang_generationFailed}} </div> </div> - <div data-tm-id="{{taskid}}" data-tm-log="log" data-tm-log-height="31em" data-tm-callback="ipxeGenCb">{{lang_menuGeneration}}</div> + <div id="tm-compile-div" data-tm-id="{{taskid}}" data-tm-log="log" data-tm-log-height="36em" data-tm-callback="ipxeGenCb">{{lang_menuGeneration}}</div> </div> </div> <script type="text/javascript"> + document.addEventListener('DOMContentLoaded', function() { + var slxFileList = $('#file-list').find('.glyphicon'); + }); + function ipxeGenCb(task) { if (!task || !task.statusCode) return; - if (task.data) { - if (task.data.pxeDone) $('#built-pxe').removeClass('invisible'); - if (task.data.usbDone) $('#built-usb').removeClass('invisible'); + + if (task.statusCode === 'TASK_FINISHED') { + $('#tm-compile-div').find('pre').hide(); } if (task.statusCode === 'TASK_ERROR') { var $gf = $('#genfailed'); @@ -33,6 +37,18 @@ $gf.append($('<pre>').text(task.data.errors)); } $gf.show('slow'); + slxFileList.find('.glyphicon-question-sign').removeClass('glyphicon-question-sign').addClass('glyphicon-stop'); + } else { + // Working or finished + if (task.data && task.data.files && task.data.files) { + for (var k in task.data.files) { + if (!task.data.files[k]) + continue; + var f = '#built-' + k.replace('/', '-').replace('.', '-'); + var $e = $(f); + $e.find('.glyphicon-question-sign').removeClass('glyphicon-question-sign').addClass('glyphicon-ok text-success'); + } + } } } </script> 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..2141103f --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/menu-edit.html @@ -0,0 +1,405 @@ +<h2>{{lang_editMenuHead}}</h2> + +<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"> + +<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"> + <label for="panel-locations">{{lang_menuLocations}}</label> + </div> + <div class="col-sm-9"> + <select id="panel-locations" multiple name="locations[]"> + {{#locations}} + <option value="{{locationid}}" {{disabled}} {{#selected}}selected{{/selected}}>{{locationpad}} {{locationname}}</option> + {{/locations}} + </select> + {{#globalMenuWarning}} + <span id="global-menu-warning" style="margin-left: 20px; color: red; display: none;">{{lang_globalMenuWarning}}</span> + {{/globalMenuWarning}} + </div> + </div> + <div> + <table class="table"> + <thead> + <tr> + <th style="width: 10px"></th> + <th style="width: 10px"></th> + <th style="width: 10px">{{lang_entryId}}</th> + <th>{{lang_title}}</th> + <th style="width: 150px">{{lang_hotkey}}</th> + <th style="width: 200px">{{lang_password}}</th> + <th style="width: 10px"><span class="glyphicon glyphicon-eye-close"></span></th> + <th style="width: 10px"></th> + </tr> + </thead> + <tbody id="table-body" style="overflow: auto;"> + {{#entries}} + <tr> + <input type="hidden" class="sort-val" name="entry[{{menuentryid}}][sortval]" value="{{sortval}}"> + <input type="hidden" name="entry[{{menuentryid}}][hidden]" value="0"> + <td class="drag-handler" style="cursor: pointer;text-align: center; vertical-align: middle;"> + <span class="glyphicon glyphicon-th-list"></span> + </td> + + <td class="slx-smallcol" style="text-align: center; vertical-align: middle;"> + <div class="radio radio-inline no-spacer" style="margin: 0;{{^entryid}}display: none;{{/entryid}}"> + <input type="radio" name="defaultentry" value="{{menuentryid}}" + {{#isdefault}}checked{{/isdefault}} {{perms.ipxe.menu.edit.disabled}} {{disabled}}> + <label></label> + </div> + </td> + + <td class="text-nowrap"> + <input class="entry-id" type="hidden" name="entry[{{menuentryid}}][entryid]" value="{{entryid}}"> + <button type="button" class="btn btn-default" style="width: 100%; text-align: left" {{disabled}} data-toggle="modal" data-target="#entry-chooser-modal"> + {{#entryid}} + {{entryid}} + {{/entryid}} + {{^entryid}} + {{lang_spacer}} + {{/entryid}} + </button> + </td> + <td> + <input class="form-control title" name="entry[{{menuentryid}}][title]" value="{{title}}" + maxlength="100" {{readonly}}> + </td> + + <td> + <select class="form-control key-list no-spacer" {{^entryid}}style="display: none;"{{/entryid}} name="entry[{{menuentryid}}][hotkey]" {{readonly}} data-default="{{hotkey}}"> + </select> + </td> + + <td> + <input class="form-control no-spacer" {{^entryid}}style="display: none;"{{/entryid}} name="entry[{{menuentryid}}][plainpass]" type="{{password_type}}" + value="{{plainpass}}" {{readonly}}> + </td> + <td class="slx-smallcol" style="text-align: center; vertical-align: middle;"> + <div class="checkbox checkbox-inline no-spacer" style="text-align: left;margin: 0;{{^entryid}}display: none;{{/entryid}}"> + <input name="entry[{{menuentryid}}][hidden]" value="1" type="checkbox" {{#hidden}}checked{{/hidden}}> + <label></label> + </div> + </td> + <td class="slx-smallcol" style="text-align: center; vertical-align: middle;"> + <button type="button" class="btn btn-default remove-button"><span class="glyphicon glyphicon-remove"></span></button> + </td> + </tr> + {{/entries}} + </tbody> + </table> + </div> + <div class="text-right" style="margin-bottom: 20px"> + <button id="add-btn" type="button" class="btn btn-success" {{disabled}}> + <span class="glyphicon glyphicon-plus-sign"></span> + {{lang_add}} + </button> + </div> + <div class="text-right"> + <a href="?do=serversetup&show=menu" type="button" class="btn btn-default">{{lang_cancel}}</a> + <button id="save-button" type="submit" class="btn btn-primary" {{disabled}}> + <span class="glyphicon glyphicon-floppy-disk"></span> + {{lang_save}} + </button> + </div> + </form> + + <div class="modal fade" id="entry-chooser-modal" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">{{lang_entryChooserTitle}}</h5> + </div> + <div class="modal-body"> + <div class="form-group"> + <select id="entry-list" class="form-control"> + <option value="">{{lang_spacer}}</option> + {{#entrylist}} + <option value="{{entryid}}">{{entryid}}</option> + {{/entrylist}} + </select> + </div> + {{#entrylist}} + <div id="entrydata-{{entryid}}" class="entrydata"> + <div class="form-group"> + <label for="{{entryid}}-name">{{lang_entryTitle}}</label> + <pre id="{{entryid}}-name">{{title}}</pre> + </div> + {{#data}} + {{#script}} + <div class="form-group"> + <label for="{{entryid}}-script">{{lang_scriptContent}}</label> + <pre id="{{entryid}}-script">{{.}}</pre> + </div> + {{/script}} + {{^script}} + <div class="form-group"> + <label for="{{entryid}}-script">{{lang_archSelector}}</label> + <pre id="{{entryid}}-arch">{{arch}}</pre> + </div> + {{#archAgnostic}} + <div class="form-group"> + <label for="{{entryid}}-executable">{{lang_imageToLoad}}</label> + <pre id="{{entryid}}-executable">{{executable}}</pre> + </div> + <div class="form-group"> + <label for="{{entryid}}-initRd">{{lang_initRd}}</label> + <pre id="{{entryid}}-initRd">{{initRd}}</pre> + </div> + <div class="form-group"> + <label for="{{entryid}}-commandLine">{{lang_commandLine}}</label> + <pre id="{{entryid}}-commandLine" >{{commandLine}}</pre> + </div> + {{/archAgnostic}} + {{#PCBIOS}} + <div class="panel panel-default"> + <div class="panel-heading">PCBIOS</div> + <div class="panel-body"> + <div class="form-group"> + <label for="{{entryid}}-executable">{{lang_imageToLoad}}</label> + <pre id="{{entryid}}-executable">{{executable}}</pre> + </div> + <div class="form-group"> + <label for="{{entryid}}-initRd">{{lang_initRd}}</label> + <pre id="{{entryid}}-initRd">{{initRd}}</pre> + </div> + <div class="form-group"> + <label for="{{entryid}}-commandLine">{{lang_commandLine}}</label> + <pre id="{{entryid}}-commandLine" >{{commandLine}}</pre> + </div> + </div> + </div> + {{/PCBIOS}} + {{#EFI}} + <div class="panel panel-default"> + <div class="panel-heading">EFI</div> + <div class="panel-body"> + <div class="form-group"> + <label for="{{entryid}}-executable">{{lang_imageToLoad}}</label> + <pre id="{{entryid}}-executable">{{executable}}</pre> + </div> + <div class="form-group"> + <label for="{{entryid}}-initRd">{{lang_initRd}}</label> + <pre id="{{entryid}}-initRd">{{initRd}}</pre> + </div> + <div class="form-group"> + <label for="{{entryid}}-commandLine">{{lang_commandLine}}</label> + <pre id="{{entryid}}-commandLine" >{{commandLine}}</pre> + </div> + </div> + </div> + {{/EFI}} + {{/script}} + {{/data}} + </div> + {{/entrylist}} + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button> + <button type="button" class="btn btn-primary" id="choose-entry">{{lang_save}}</button> + </div> + </div> + </div> + </div> + </div> +</div> +<div class="hidden"> + <select id="key-list-template"> + <option value="">{{lang_none}}</option> + {{#keys}} + <option value="{{key}}">{{key}}</option> + {{/keys}} + </select> +</div> +<table class="hidden" id="row-template"> + <tr> + <input type="hidden" class="sort-val" name="entry[%new%][sortval]" value="99999"> + <td class="drag-handler" style="cursor: pointer;text-align: center; vertical-align: middle;"> + <span class="glyphicon glyphicon-th-list"></span> + </td> + + <td class="slx-smallcol" style="text-align: center; vertical-align: middle;"> + <div class="radio radio-inline no-spacer" style="margin: 0; display: none;"> + <input type="radio" name="defaultentry" value="%new%"> + <label></label> + </div> + </td> + + <td class="text-nowrap"> + <input class="entry-id" type="hidden" name="entry[%new%][entryid]" value=""> + <button type="button" class="btn btn-default" style="width: 100%; text-align: left" {{disabled}} data-toggle="modal" data-target="#entry-chooser-modal"> + {{lang_spacer}} + </button> + </td> + <td> + <input class="form-control title" data-old="#new#" name="entry[%new%][title]" maxlength="100"> + </td> + <td> + <select class="form-control key-list no-spacer" style="display: none;" name="entry[%new%][hotkey]"> + </select> + </td> + <td> + <input class="form-control no-spacer" style="display: none;" name="entry[%new%][plainpass]" type="{{password_type}}"> + </td> + <td class="slx-smallcol" style="text-align: center; vertical-align: middle;"> + <div class="checkbox checkbox-inline no-spacer" style="text-align: left;margin: 0;{{^entryid}}display: none;{{/entryid}}"> + <input name="entry[%new%][hidden]" value="1" type="checkbox"> + <label></label> + </div> + </td> + <td class="slx-smallcol" style="text-align: center; vertical-align: middle;"> + <button type="button" class="btn btn-default remove-button"><span class="glyphicon glyphicon-remove"></span></button> + </td> + </tr> +</table> + +<script type="text/javascript"> + var spacerText = "{{lang_spacer}}"; + + document.addEventListener("DOMContentLoaded", function() { + var locationSelect = $('#panel-locations'); + locationSelect.multiselect({numberDisplayed: 1}); + var globalMenuWarning = $('#global-menu-warning'); + if (globalMenuWarning.length) { + var saveButton = $('#save-button'); + if (locationSelect.val() !== null) { + saveButton.prop('disabled', false); + globalMenuWarning.hide(); + } else { + saveButton.prop('disabled', true); + globalMenuWarning.show(); + } + locationSelect.change(function () { + if ($(this).val() !== null) { + saveButton.prop('disabled', false); + globalMenuWarning.hide(); + } else { + saveButton.prop('disabled', true); + globalMenuWarning.show(); + } + }); + } + + function reassignSortValues() { + var startValue = 1; + $('.sort-val').each(function(index, element) { + element.value = startValue * 10; + startValue++; + }); + } + + $('#table-body').sortable({ + opacity: 0.8, + handle: '.drag-handler', + start: function(evt, ui) { + ui.placeholder.css("visibility", "visible"); + ui.placeholder.css("opacity", "0.152"); + ui.placeholder.css("background-color", "#ddd"); + }, + stop: reassignSortValues + }); + + $('.key-list').each(function() { + $select = $(this); + $source = $('#key-list-template').find('option'); + var def = $select.data('default'); + $select.append($source.clone(true)); + $select.find('option[value="' + def + '"]').attr('selected', true); + }); + var newIndex = 0; + $('#add-btn').click(function() { + var $new = $('#row-template').find('tr').clone(true); + newIndex++; + $('#table-body').append($new); + $new.find('[name]').each(function() { + var $this = $(this); + var val = $this.val(); + var name = $this.attr('name'); + if (name) { + $this.attr('name', name.replace('%new%', 'new-' + newIndex)); + } + if (val) { + $this.val(val.replace('%new%', 'new-' + newIndex)); + } + }); + reassignSortValues(); + }); + + $('.remove-button').click(function() { + $(this).parent().parent().remove(); + reassignSortValues(); + }); + + $('#entry-list').change(function(e) { + var modal = $('#entry-chooser-modal'); + modal.find('.entrydata').hide(); + modal.find('#entrydata-' + $(this).val()).show(); + }); + + var currentEntryButton = null; + + $('#entry-chooser-modal').on('show.bs.modal', function(e) { + currentEntryButton = $(e.relatedTarget); + var entryId = currentEntryButton.parent().find('.entry-id').val(); + $('#entry-list').val(entryId).change(); + }); + + $('#choose-entry').click(function() { + $('#entry-chooser-modal').modal('hide'); + var entryId = $('#entry-list').val(); + currentEntryButton.parent().find('.entry-id').val(entryId); + currentEntryButton.text(entryId || spacerText); + var tableRow = currentEntryButton.parent().parent(); + if (!entryId) { + tableRow.find('.no-spacer').hide(); + tableRow.find('input.no-spacer').val(''); + tableRow.find('div.no-spacer').find('input').prop('checked', false); + + } else { + tableRow.find('.no-spacer').show(); + } + var $title = tableRow.find('.title'); + var oldval = $title.data('old'); + if (oldval === '#stop#') + return; + if (oldval !== '#new#' && oldval !== $title.val()) { + $title.data('old', '#stop#'); + return; + } + var text = $('#' + entryId + '-name').text(); + $title.val(text).data('old', text); + }); + }); +</script>
\ 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..67365a33 --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/menu-list.html @@ -0,0 +1,91 @@ +<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> + <th class="slx-smallcol">{{lang_delete}}</th> + </tr> + </thead> + <tbody> + {{#menuTable}} + <tr> + <td> + {{title}} + </td> + <td class="text-right"> + {{locationCount}} + </td> + <td align="center"> + {{^isdefault}} + {{#showSetDefault}} + <form method="post" action="?do=serversetup"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="menuid" value="{{menuid}}"> + <button type="submit" name="action" value="setDefaultMenu" class="btn btn-xs btn-info"> + <span class="glyphicon glyphicon-flag"></span> + </button> + </form> + {{/showSetDefault}} + {{/isdefault}} + {{#isdefault}} + <span class="glyphicon glyphicon-ok"></span> + {{/isdefault}} + </td> + <td align="center"> + {{#allowEdit}} + <a href="?do=serversetup&show=editmenu&id={{menuid}}" class="btn btn-xs btn-primary"> + <span class="glyphicon glyphicon-edit"></span> + </a> + {{/allowEdit}} + </td> + <td align="center"> + {{#allowDelete}} + <button type="button" class="btn btn-xs btn-danger" data-toggle="modal" data-target="#deleteModal" onclick="deleteMenu('{{menuid}}')"> + <span class="glyphicon glyphicon-trash"></span> + </button> + {{/allowDelete}} + </td> + </tr> + {{/menuTable}} + </tbody> +</table> +<div class="pull-right"> + <a href="?do=serversetup&show=editmenu&id=0" class="btn btn-success {{allowAddMenu}}"> + <span class="glyphicon glyphicon-plus"></span> + {{lang_addMenu}} + </a> +</div> + + +<!-- Modals --> +<form method="post" action="?do=serversetup"> + <input type="hidden" name="token" value="{{token}}"> + <div class ="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title" id="myModalLabel">{{lang_delete}}</h4> + </div> + <div class="modal-body"> + <p>{{lang_menuDeleteConfirm}}</p> + </div> + <div class="modal-footer"> + <input type="hidden" id="delete-menu-id" name="deleteid" value=""> + <button type="button" class="btn btn-default" data-dismiss="modal">{{lang_cancel}}</button> + <button type="submit" name="action" value="deleteMenu" class="btn btn-danger"><span class="glyphicon glyphicon-trash"></span> {{lang_delete}}</button> + </div> + </div> + </div> + </div> +</form> + +<script> + function deleteMenu(menuid) { + $("#delete-menu-id").val(menuid); + } +</script>
\ No newline at end of file |