From 2e78eec281815d6ba42ff2cf7c3a937abe6d83c5 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 12 Jun 2018 17:15:44 +0200 Subject: [serversetup-bwlp] Start rewrite as purely iPXE-based --- modules-available/serversetup-bwlp/api.inc.php | 242 +++++++++ modules-available/serversetup-bwlp/config.json | 5 +- .../serversetup-bwlp/inc/bootentry.inc.php | 195 +++++++ .../serversetup-bwlp/inc/ipxe.inc.php | 560 ++++++++++++++------- .../serversetup-bwlp/inc/ipxemenu.inc.php | 142 ++++++ .../serversetup-bwlp/inc/menuentry.inc.php | 170 +++++++ .../serversetup-bwlp/inc/pxelinux.inc.php | 262 ++++++++++ modules-available/serversetup-bwlp/install.inc.php | 74 +++ modules-available/serversetup-bwlp/page.inc.php | 300 ++++++++++- .../serversetup-bwlp/permissions/permissions.json | 14 +- .../serversetup-bwlp/templates/download.html | 38 ++ .../serversetup-bwlp/templates/heading.html | 4 +- .../templates/ipxe-new-boot-entry.html | 108 ++++ .../serversetup-bwlp/templates/ipxe.html | 117 ----- .../serversetup-bwlp/templates/menu-edit.html | 114 +++++ .../serversetup-bwlp/templates/menu-list.html | 36 ++ 16 files changed, 2049 insertions(+), 332 deletions(-) create mode 100644 modules-available/serversetup-bwlp/api.inc.php create mode 100644 modules-available/serversetup-bwlp/inc/bootentry.inc.php create mode 100644 modules-available/serversetup-bwlp/inc/ipxemenu.inc.php create mode 100644 modules-available/serversetup-bwlp/inc/menuentry.inc.php create mode 100644 modules-available/serversetup-bwlp/inc/pxelinux.inc.php create mode 100644 modules-available/serversetup-bwlp/install.inc.php create mode 100644 modules-available/serversetup-bwlp/templates/download.html create mode 100644 modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html delete mode 100644 modules-available/serversetup-bwlp/templates/ipxe.html create mode 100644 modules-available/serversetup-bwlp/templates/menu-edit.html create mode 100644 modules-available/serversetup-bwlp/templates/menu-list.html (limited to 'modules-available/serversetup-bwlp') diff --git a/modules-available/serversetup-bwlp/api.inc.php b/modules-available/serversetup-bwlp/api.inc.php new file mode 100644 index 00000000..36f9063c --- /dev/null +++ b/modules-available/serversetup-bwlp/api.inc.php @@ -0,0 +1,242 @@ + 'exit 1', + 'COMBOOT' => 'chain /tftp/chain.c32 hd0', + 'SANBOOT' => 'sanboot --no-describe', +]; + +$serverIp = Property::getServerIp(); + +$ip = $_SERVER['REMOTE_ADDR']; +if (substr($ip, 0, 7) === '::ffff:') { + $ip = substr($ip, 7); +} +$uuid = Request::any('uuid', false, 'string'); +$menu = IPxeMenu::forClient($ip, $uuid); + +// Get platform - EFI or PCBIOS +$platform = strtoupper(Request::any('platform', 'PCBIOS', 'string')); + +// Get preferred localboot method, depending on system model +$localboot = false; +$model = false; +if ($uuid !== false && Module::get('statistics') !== false) { + $row = Database::queryFirst('SELECT systemmodel FROM machine WHERE machineuuid = :uuid', ['uuid' => $uuid]); + if ($row !== false && !empty($row['systemmodel'])) { + $model = $row['systemmodel']; + } +} +if ($model === false) { + function modfilt($str) + { + if (empty($str) || preg_match('/product\s+name|be\s+filled|unknown|default\s+string/i', $str)) + return false; + return trim(preg_replace('/\s+/', ' ', $str)); + } + $manuf = modfilt(Request::any('manuf', false, 'string')); + $product = modfilt(Request::any('product', false, 'string')); + if (!empty($product)) { + $model = $product; + if (!empty($manuf)) { + $model .= " ($manuf)"; + } + } +} +// Query +if ($model !== false) { + $row = Database::queryFirst("SELECT bootmethod FROM serversetup_localboot WHERE systemmodel = :model LIMIT 1", + ['model' => $model]); + if ($row !== false) { + $localboot = $row['bootmethod']; + } +} +if ($localboot === false || !isset($BOOT_METHODS[$localboot])) { + $localboot = Property::get('serversetup.localboot', false); + if ($localboot === false) { + if ($platform === 'EFI') { + // It seems most (all) EFI platforms won't enumerate any drives in ipxe. + // No idea if this can be fixed in ipxe code in the future. + $localboot = 'EXIT'; + } else { + $localboot = 'SANBOOT'; + } + } +} +if (isset($BOOT_METHODS[$localboot])) { + // Move preferred method first + $BOOT_METHODS[] = $BOOT_METHODS[$localboot]; + unset($BOOT_METHODS[$localboot]); + $BOOT_METHODS = array_reverse($BOOT_METHODS); +} + +$output = <<getMenuDefinition('target'); + +$output .= <<getItemsCode(); + +/* +:i1 +#console || +echo Welcome to Shell || +shell +goto slx_menu + +:i2 +imgfree || +kernel /boot/default/kernel slxbase=boot/default slxsrv=$serverIp splash BOOTIF=01-\${net\${nic}/mac:hexhyp} || echo Could not download kernel +initrd /boot/default/initramfs-stage31 || echo Could not download initrd +boot -ar || goto fail + +:i3 +chain -ar \${self} || +chain -ar /tftp/undionly.kpxe || goto fail + +:i4 +imgfree || +sanboot --no-describe --drive 0x80 || goto fail + +:i5 +chain -a /tftp/memtest.0 passes=1 onepass || goto membad +prompt Memory OK. Press a key. +goto init + +:i6 +console --left 60 --top 130 --right 67 --bottom 96 --quick --picture bg-load --keep || +echo Welcome to Shell || +shell +goto slx_menu + +:i7 +chain -ar tftp://132.230.4.6/ipxelinux.0 || prompt FAILED PRESS A KEY +goto slx_menu + +:i8 +set x:int32 0 +:again +console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep --quick || +console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep --quick || +inc x +iseq \${x} 20 || goto again +prompt DONE. Press dein Knie. +goto slx_menu + +:i9 +reboot || +prompt Reboot failed. Press a key. +goto slx_menu + +:i10 +poweroff || +prompt Poweroff failed. Press a key. +goto slx_menu + +:membad +iseq \${errno} 0x1 || goto memaborted +params +param scrot \${vram} +imgfetch -a http://132.230.8.113/screen.php##params || +prompt Memory is bad. Press a key. +goto init + +:memaborted +params +param scrot \${vram} +imgfetch -a http://132.230.8.113/screen.php##params || +prompt Memory test aborted. Press a key. +goto init + +*/ + +$output .= << $value) { + if (property_exists($this, $key)) { + $this->{$key} = $value; + } + } + } + } + + public abstract function toScript($failLabel); + + public abstract function toArray(); + + public abstract function addFormFields(&$array); + + /* + * + */ + + /** + * Return a BootEntry instance from the serialized data. + * + * @param string $jsonString serialized entry data + * @return BootEntry|null instance representing boot entry, null on error + */ + public static function fromJson($data) + { + if (is_string($data)) { + $data = json_decode($data, true); + } + if (isset($data['script'])) { + return new CustomBootEntry($data); + } + if (isset($data['executable'])) { + return new StandardBootEntry($data); + } + return null; + } + + public static function newStandardBootEntry($initData) + { + if (empty($initData['executable'])) + return null; + return new StandardBootEntry($initData); + } + + public static function newCustomBootEntry($initData) + { + if (empty($initData['script'])) + return null; + return new CustomBootEntry($initData); + } + + /** + * Return a BootEntry instance from database with the given id. + * + * @param string $id + * @return BootEntry|null|false false == unknown id, null = unknown entry type, BootEntry instance on success + */ + public static function fromDatabaseId($id) + { + $row = Database::queryFirst("SELECT data FROM serversetup_bootentry + WHERE entryid = :id LIMIT 1", ['id' => $id]); + if ($row === false) + return false; + return self::fromJson($row['data']); + } + +} + +class StandardBootEntry extends BootEntry +{ + protected $executable; + protected $initRd; + protected $commandLine; + protected $replace; + protected $autoUnload; + protected $resetConsole; + + public function __construct($data = false) + { + if ($data instanceof PxeSection) { + $this->executable = $data->kernel; + $this->initRd = $data->initrd; + $this->commandLine = ' ' . str_replace('vga=current', '', $data->append) . ' '; + $this->resetConsole = true; + $this->replace = true; + $this->autoUnload = true; + if (strpos($this->commandLine, ' quiet ') !== false) { + $this->commandLine .= ' loglevel=5 rd.systemd.show_status=auto'; + } + if ($data->ipAppend & 1) { + $this->commandLine .= ' ${ipappend1}'; + } + if ($data->ipAppend & 2) { + $this->commandLine .= ' ${ipappend2}'; + } + if ($data->ipAppend & 4) { + $this->commandLine .= ' SYSUUID=${uuid}'; + } + $this->commandLine = trim(preg_replace('/\s+/', ' ', $this->commandLine)); + } else { + parent::__construct($data); + } + } + + public function toScript($failLabel) + { + $script = ''; + if ($this->resetConsole) { + $script .= "console ||\n"; + } + if (!empty($this->initRd)) { + $script .= "imgfree ||\n"; + if (!is_array($this->initRd)) { + $script .= "initrd {$this->initRd} || goto $failLabel\n"; + } else { + foreach ($this->initRd as $initrd) { + $script .= "initrd $initrd || goto $failLabel\n"; + } + } + } + $script .= "boot "; + if ($this->autoUnload) { + $script .= "-a "; + } + if ($this->replace) { + $script .= "-r "; + } + $script .= "{$this->executable}"; + if (!empty($this->commandLine)) { + $script .= " {$this->commandLine}"; + } + $script .= " || goto $failLabel\n"; + if ($this->resetConsole) { + $script .= "goto start ||\n"; + } + return $script; + } + + public function addFormFields(&$array) + { + $array['entry'] = [ + 'executable' => $this->executable, + 'initRd' => $this->initRd, + 'commandLine' => $this->commandLine, + 'replace_checked' => $this->replace ? 'checked' : '', + 'autoUnload_checked' => $this->autoUnload ? 'checked' : '', + 'resetConsole_checked' => $this->resetConsole ? 'checked' : '', + ]; + $array['exec_checked'] = 'checked'; + } + + public function toArray() + { + return [ + 'executable' => $this->executable, + 'initRd' => $this->initRd, + 'commandLine' => $this->commandLine, + 'replace' => $this->replace, + 'autoUnload' => $this->autoUnload, + 'resetConsole' => $this->resetConsole, + ]; + } +} + +class CustomBootEntry extends BootEntry +{ + protected $script; + + public function toScript($failLabel) + { + return str_replace('%fail%', $failLabel, $this->script) . "\n"; + } + + public function addFormFields(&$array) + { + $array['entry'] = [ + 'script' => $this->script, + ]; + $array['script_checked'] = 'checked'; + } + + public function toArray() + { + return ['script' => $this->script]; + } +} diff --git a/modules-available/serversetup-bwlp/inc/ipxe.inc.php b/modules-available/serversetup-bwlp/inc/ipxe.inc.php index c42de80b..d5bbb4b2 100644 --- a/modules-available/serversetup-bwlp/inc/ipxe.inc.php +++ b/modules-available/serversetup-bwlp/inc/ipxe.inc.php @@ -1,224 +1,398 @@ ['string', 'title'], - 'menu default' => ['true', 'isDefault'], - 'menu hide' => ['true', 'isHidden'], - 'menu disabled' => ['true', 'isDisabled'], - 'menu indent' => ['int', 'indent'], - 'kernel' => ['string', 'kernel'], - 'initrd' => ['string', 'initrd'], - 'append' => ['string', 'append'], - 'ipappend' => ['int', 'ipAppend'], - 'localboot' => ['int', 'localBoot'], - ]; - $globalPropMap = [ - 'timeout' => ['int', 'timeoutMs', 100], - 'totaltimeout' => ['int', 'totalTimeoutMs', 100], - 'menu title' => ['string', 'title'], - 'menu clear' => ['true', 'menuClear'], - 'menu immediate' => ['true', 'immediateHotkeys'], - 'ontimeout' => ['string', 'timeoutLabel'], - ]; - $lines = preg_split('/[\r\n]+/', $input); - $section = null; - $count = count($lines); - for ($li = 0; $li < $count; ++$li) { - $line =& $lines[$li]; - if (!preg_match('/^\s*([^m]\S*|menu\s+\S+)(\s+.*?|)\s*$/i', $line, $out)) + foreach (glob($configPath . '/*', GLOB_NOSORT) as $file) { + if (!is_file($file) || !preg_match('~/[A-F0-9]{1,8}$~', $file)) continue; - $key = trim($out[1]); - $key = strtolower($key); - $key = preg_replace('/\s+/', ' ', $key); - if ($key === 'label') { - if ($section !== null) { - $menu->sections[] = $section; - } - $section = new PxeSection($out[2]); - } elseif ($key === 'menu separator') { - if ($section !== null) { - $menu->sections[] = $section; - $section = null; - } - $menu->sections[] = new PxeSection(null); - } elseif (self::handleKeyword($key, $out[2], $globalPropMap, $menu)) { + $content = file_get_contents($file); + if ($content === false) continue; - } elseif ($section === null) { - continue; - } elseif ($key === 'text' && strtolower($out[2]) === 'help') { - $text = ''; - while (++$li < $count) { - $line =& $lines[$li]; - if (strtolower(trim($line)) === 'endtext') - break; - $text .= $line . "\n"; + $file = basename($file); + $start = hexdec(str_pad($file,8, '0')); + $end = hexdec(str_pad($file,8, 'f')); // TODO ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^PREFIX + error_log('From ' . long2ip($start) . ' to ' . long2ip($end)); + $res = Database::simpleQuery("SELECT locationid, startaddr, endaddr FROM subnet + WHERE startaddr >= :start AND endaddr <= :end", compact('start', 'end')); + $locations = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + foreach ($locations as &$loc) { + if ($row['startaddr'] <= $loc['startaddr'] && $row['endaddr'] >= $loc['endaddr']) { + $loc = false; + } elseif ($row['startaddr'] >= $loc['startaddr'] && $row['endaddr'] <= $loc['endaddr']) { + continue 2; + } } - $section->helpText = $text; - } elseif (self::handleKeyword($key, $out[2], $sectionPropMap, $section)) { + unset($loc); + $locations[] = $row; + } + $menuId = self::insertMenu($content, 'Imported', false, 0, [], []); + if ($menuId === false) continue; + foreach ($locations as $loc) { + Database::exec('INSERT IGNORE INTO serversetup_menu_x_location (menuid, locationid) + VALUES (:menuid, :locationid)', ['menuid' => $menuId, 'locationid' => $loc['locationid']]); } } - if ($section !== null) { - $menu->sections[] = $section; + } + + public static function importLegacyMenu($force = false) + { + if (!$force && false !== Database::queryFirst("SELECT entryid FROM serversetup_bootentry WHERE entryid = 'bwlp-default'")) + return false; // Already exists + // Now create the default entry + self::createDefaultEntries(); + $prepend = ['bwlp-default' => false, 'localboot' => false]; + $defaultLabel = 'bwlp-default'; + $menuTitle = 'bwLehrpool Bootauswahl'; + $pxeConfig = ''; + $timeoutSecs = 60; + // Try to import any customization + $oldMenu = Property::getBootMenu(); + if (is_array($oldMenu)) { + // + if (isset($oldMenu['timeout'])) { + $timeoutSecs = (int)$oldMenu['timeout']; + } + if (isset($oldMenu['defaultentry'])) { + if ($oldMenu['defaultentry'] === 'net') { + $defaultLabel = 'bwlp-default'; + } elseif ($oldMenu['defaultentry'] === 'hdd') { + $defaultLabel = 'localboot'; + } elseif ($oldMenu['defaultentry'] === 'custom') { + $defaultLabel = 'custom'; + } + } + if (!empty($oldMenu['custom'])) { + $pxeConfig = $oldMenu['custom']; + } } - return $menu; + $append = [ + '', + 'bwlp-default-dbg' => false, + '', + 'poweroff' => false, + ]; + return self::insertMenu($pxeConfig, $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append); } - /** - * Check if keyword is valid and if so, add its interpreted value - * to the given object. The map to look up the keyword has to be passed - * as well as the object to set the value in. Map and object should - * obviously match. - * @param string $key keyword of parsed line - * @param string $val raw value of currently parsed line (empty if not present) - * @param array $map Map in which $key is looked up as key - * @param PxeMenu|PxeSection The object to set the parsed and sanitized value in - * @return bool true if the value was found in the map (and set in the object), false otherwise - */ - private static function handleKeyword($key, $val, $map, $object) + private static function insertMenu($pxeConfig, $menuTitle, $defaultLabel, $defaultTimeoutSeconds, $prepend, $append) { - if (!isset($map[$key])) + $timeoutMs = []; + $menuEntries = $prepend; + settype($menuEntries, 'array'); + if (!empty($pxeConfig)) { + $pxe = PxeLinux::parsePxeLinux($pxeConfig); + if (!empty($pxe->title)) { + $menuTitle = $pxe->title; + } + if ($pxe->timeoutLabel !== null) { + $defaultLabel = $pxe->timeoutLabel; + } + $timeoutMs[] = $pxe->timeoutMs; + $timeoutMs[] = $pxe->totalTimeoutMs; + foreach ($pxe->sections as $section) { + if ($section->localBoot || preg_match('/chain.c32$/i', $section->kernel)) { + $menuEntries['localboot'] = 'localboot'; + continue; + } + $section->mangle(); + if ($section->label === null) { + if (!$section->isHidden && !empty($section->title)) { + $menuEntries[] = $section->title; + } + continue; + } + if (empty($section->kernel)) { + if (!$section->isHidden && !empty($section->title)) { + $menuEntries[] = $section->title; + } + continue; + } + $entry = self::pxe2BootEntry($section); + if ($entry === null) + continue; + $label = self::cleanLabelFixLocal($section); + if ($defaultLabel === $section->label) { + $defaultLabel = $label; + } + $hotkey = MenuEntry::filterKeyName($section->hotkey); + // Create boot entry + $data = $entry->toArray(); + Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) + VALUES (:label, :hotkey, :title, 0, :data)', [ + 'label' => $label, + 'hotkey' => $hotkey, + 'title' => self::sanitizeIpxeString($section->title), + 'data' => json_encode($data), + ]); + $menuEntries[$label] = $section; + } + } + if (is_array($append)) { + $menuEntries += $append; + } + if (empty($menuEntries)) return false; - $opt = $map[$key]; - // opt[0] is the type the value should be cast to; special case "true" means - // this is a bool option that will be set as soon as the keyword is present, - // as it doesn't have any parameters - if ($opt[0] === 'true') { - $val = true; + // Make menu + $timeoutMs = array_filter($timeoutMs, 'is_int'); + if (empty($timeoutMs)) { + $timeoutMs = (int)($defaultTimeoutSeconds * 1000); } else { - settype($val, $opt[0]); + $timeoutMs = min($timeoutMs); } - // If opt[2] is present it's a multiplier for the value - if (isset($opt[2])) { - $val *= $opt[2]; + $isDefault = (int)(Database::queryFirst('SELECT menuid FROM serversetup_menu WHERE isdefault = 1') === false); + Database::exec("INSERT INTO serversetup_menu (timeoutms, title, defaultentryid, isdefault) + VALUES (:timeoutms, :title, NULL, :isdefault)", [ + 'title' => self::sanitizeIpxeString($menuTitle), + 'timeoutms' => $timeoutMs, + 'isdefault' => $isDefault, + ]); + $menuId = Database::lastInsertId(); + if (!array_key_exists($defaultLabel, $menuEntries) && $timeoutMs > 0) { + $defaultLabel = array_keys($menuEntries)[0]; } - $object->{$opt[1]} = $val; - return true; + // Link boot entries to menu + $defaultEntryId = null; + $order = 1000; + foreach ($menuEntries as $label => $entry) { + if (is_string($entry)) { + // Gap entry + Database::exec("INSERT INTO serversetup_menuentry + (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) + VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, '', '')", [ + 'menuid' => $menuId, + 'entryid' => null, + 'hotkey' => '', + 'title' => self::sanitizeIpxeString($entry), + 'hidden' => 0, + 'sortval' => $order += 100, + ]); + continue; + } + $data = Database::queryFirst("SELECT entryid, hotkey, title FROM serversetup_bootentry WHERE entryid = :entryid", ['entryid' => $label]); + if ($data === false) + continue; + $data['pass'] = ''; + $data['hidden'] = 0; + if ($entry instanceof PxeSection) { + $data['hidden'] = (int)$entry->isHidden; + // Prefer explicit data from this imported menu over the defaults + $data['title'] = self::sanitizeIpxeString($entry->title); + if (MenuEntry::getKeyCode($entry->hotkey) !== false) { + $data['hotkey'] = $entry->hotkey; + } + if (!empty($entry->passwd)) { + // Most likely it's a hash so we cannot recover; ask people to reset + $data['pass'] ='please_reset'; + } + } + $data['menuid'] = $menuId; + $data['sortval'] = $order += 100; + $res = Database::exec("INSERT INTO serversetup_menuentry + (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) + VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, :pass, :pass)", $data); + if ($res !== false && $label === $defaultLabel) { + $defaultEntryId = Database::lastInsertId(); + } + } + // Now we can set default entry + if (!empty($defaultEntryId)) { + Database::exec("UPDATE serversetup_menu SET defaultentryid = :entryid WHERE menuid = :menuid", + ['menuid' => $menuId, 'entryid' => $defaultEntryId]); + } + // TODO: masterpw? rather pointless.... + //$oldMenu['masterpasswordclear']; + return $menuId; } -} + private static function createDefaultEntries() + { + $query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data) + VALUES (:entryid, :hotkey, :title, 1, :data)'; + Database::exec($query, + [ + 'entryid' => 'bwlp-default', + 'hotkey' => 'B', + 'title' => 'bwLehrpool-Umgebung starten', + 'data' => json_encode([ + 'executable' => '/boot/default/kernel', + 'initRd' => '/boot/default/initramfs-stage31', + 'commandLine' => 'slxbase=boot/default quiet splash loglevel=5 rd.systemd.show_status=auto ${ipappend1} ${ipappend2}', + 'replace' => true, + 'autoUnload' => true, + 'resetConsole' => true, + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'bwlp-default-dbg', + 'hotkey' => '', + 'title' => 'bwLehrpool-Umgebung starten (nosplash, debug)', + 'data' => json_encode([ + 'executable' => '/boot/default/kernel', + 'initRd' => '/boot/default/initramfs-stage31', + 'commandLine' => 'slxbase=boot/default loglevel=7 ${ipappend1} ${ipappend2}', + 'replace' => true, + 'autoUnload' => true, + 'resetConsole' => true, + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'localboot', + 'hotkey' => 'L', + 'title' => 'Lokales System starten', + 'data' => json_encode([ + 'script' => 'goto slx_localboot || goto %fail% ||', + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'poweroff', + 'hotkey' => 'P', + 'title' => 'Power off', + 'data' => json_encode([ + 'script' => 'poweroff || goto %fail% ||', + ]), + ]); + Database::exec($query, + [ + 'entryid' => 'reboot', + 'hotkey' => 'R', + 'title' => 'Reboot', + 'data' => json_encode([ + 'script' => 'reboot || goto %fail% ||', + ]), + ]); + } -/** - * Class representing a parsed pxelinux menu. Members - * will be set to their annotated type if present or - * be null otherwise, except for present-only boolean - * options, which will default to false. - */ -class PxeMenu -{ - /** - * @var string menu title, shown at the top of the menu - */ - public $title; - /** - * @var int initial timeout after which $timeoutLabel would be executed - */ - public $timeoutMs; - /** - * @var int if the user canceled the timeout by pressing a key, this timeout would still eventually - * trigger and launch the $timeoutLabel section - */ - public $totalTimeoutMs; - /** - * @var string label of section which will execute if the timeout expires - */ - public $timeoutLabel; - /** - * @var bool hide menu and just show background after triggering an entry - */ - public $menuClear = false; /** - * @var bool boot the associated entry directly if its corresponding hotkey is presed instead of just highlighting + * Create unique label for a boot entry. It will try to figure out whether + * this is one of our default entries and if not, create a unique label + * representing the menu entry contents. + * Also it patches the entry if it's referencing the local bwlp install + * because side effects. + * + * @param PxeSection $section + * @return string */ - public $immediateHotkeys = false; - /** - * @var PxeSection[] list of sections the menu contains - */ - public $sections = []; -} + private static function cleanLabelFixLocal($section) + { + $myip = Property::getServerIp(); + // Detect our "old" entry types + if (count($section->initrd) === 1 && preg_match(",$myip/boot/default/kernel\$,", $section->kernel) + && preg_match(",$myip/boot/default/initramfs-stage31\$,", $section->initrd[0])) { + // Kernel and initrd match, examine KCL + if ($section->append === 'slxbase=boot/default vga=current quiet splash') { + // Normal + return 'bwlp-default'; + } elseif ($section->append === 'slxbase=boot/default') { + // Debug output + return 'bwlp-default-dbg'; + } else { + // Transform to relative URL, leave KCL, fall through to generic label gen + $section->kernel = '/boot/default/kernel'; + $section->initrd = ['/boot/default/initramfs-stage31']; + } + } + // Generic -- "smart" hash of kernel, initrd and command line + $str = $section->kernel . ' ' . implode(',', $section->initrd); + $array = preg_split('/\s+/', $section->append, -1, PREG_SPLIT_NO_EMPTY); + sort($array); + $str .= ' ' . implode(' ', $array); + + return 'i-' . substr(md5($str), 0, 12); + } -/** - * Class representing a parsed pxelinux menu entry. Members - * will be set to their annotated type if present or - * be null otherwise, except for present-only boolean - * options, which will default to false. - */ -class PxeSection -{ - /** - * @var string label used internally in PXEMENU definition to address this entry - */ - public $label; - /** - * @var string MENU LABEL of PXEMENU - title of entry displayed to the user - */ - public $title; - /** - * @var int Number of spaces to prefix the title with - */ - public $indent; - /** - * @var string help text to display when the entry is highlighted - */ - public $helpText; - /** - * @var string Kernel to load - */ - public $kernel; - /** - * @var string initrd to load for the kernel - */ - public $initrd; - /** - * @var string command line options to pass to the kernel - */ - public $append; - /** - * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2. - */ - public $ipAppend; - /** - * @var string Password protecting the entry. This is most likely in crypted form. - */ - public $passwd; - /** - * @var bool whether this section is marked as default (booted after timeout) - */ - public $isDefault = false; - /** - * @var bool Menu entry is not visible (can only be triggered by timeout) - */ - public $isHidden = false; /** - * @var bool Disable this entry, making it unselectable + * @param PxeSection $section + * @return BootEntry|null The according boot entry, null if it's unparsable */ - public $isDisabled = false; + private static function pxe2BootEntry($section) + { + if (preg_match('/(pxechain.com|pxechn.c32)$/i', $section->kernel)) { + // Chaining -- create script + $args = preg_split('/\s+/', $section->append); + $script = ''; + $file = false; + for ($i = 0; $i < count($args); ++$i) { + $arg = $args[$i]; + if ($arg === '-c') { // PXELINUX config file option + ++$i; + $script .= "set 209:string {$args[$i]} || goto %fail%\n"; + } elseif ($arg === '-p') { // PXELINUX prefix path option + ++$i; + $script .= "set 210:string {$args[$i]} || goto %fail%\n"; + } elseif ($arg === '-t') { // PXELINUX timeout option + ++$i; + $script .= "set 211:int32 {$args[$i]} || goto %fail%\n"; + } elseif ($arg === '-o') { // Overriding various DHCP options + ++$i; + if (preg_match('/^((?:0x)?[a-f0-9]{1,4})\.([bwlsh])=(.*)$/i', $args[$i], $out)) { + // TODO: 'q' (8byte) unsupported for now + $opt = intval($out[1], 0); + if ($opt > 0 && $opt < 255) { + static $optType = ['b' => 'uint8', 'w' => 'uint16', 'l' => 'int32', 's' => 'string', 'h' => 'hex']; + $type = $optType[$out[2]]; + $script .= "set {$opt}:{$type} {$args[$i]} || goto %fail%\n"; + } + } + } elseif ($arg{0} === '-') { + continue; + } elseif ($file === false) { + $file = self::parseFile($arg); + } + } + if ($file !== false) { + $url = parse_url($file); + if (isset($url['host'])) { + $script .= "set next-server {$url['host']} || goto %fail%\n"; + } + if (isset($url['path'])) { + $script .= "set filename {$url['path']} || goto %fail%\n"; + } + $script .= "chain -ar {$file} || goto %fail%\n"; + return new CustomBootEntry(['script' => $script]); + } + return null; + } + // "Normal" entry that should be convertible into a StandardBootEntry + $section->kernel = self::parseFile($section->kernel); + foreach ($section->initrd as &$initrd) { + $initrd = self::parseFile($initrd); + } + return BootEntry::newStandardBootEntry($section); + } + /** - * @var int Value of the LOCALBOOT field + * Parse PXELINUX file notion. Basically, turn + * server::file into tftp://server/file. + * + * @param string $file + * @return string */ - public $localBoot; + private static function parseFile($file) + { + if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) { + return 'tftp://' . $out[1] . '/' . $out[2]; + } + return $file; + } - public function __construct($label) { $this->label = $label; } -} + public static function sanitizeIpxeString($string) + { + return str_replace(['&', '|', ';', '$', "\r", "\n"], ['+', '/', ':', 'S', ' ', ' '], $string); + } + + public static function makeMd5Pass($plainpass, $salt) + { + if (empty($plainpass)) + return ''; + return md5(md5($plainpass) . '-' . $salt); + } +} diff --git a/modules-available/serversetup-bwlp/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp/inc/ipxemenu.inc.php new file mode 100644 index 00000000..ed9f0986 --- /dev/null +++ b/modules-available/serversetup-bwlp/inc/ipxemenu.inc.php @@ -0,0 +1,142 @@ + $menu]); + if (!is_array($menu)) { + $menu = ['menuid' => 'foo', 'title' => 'Invalid Menu ID: ' . (int)$menu]; + } + } + $this->menuid = (int)$menu['menuid']; + $this->timeoutMs = (int)$menu['timeoutms']; + $this->title = $menu['title']; + $this->defaultEntryId = $menu['defaultentryid']; + $res = Database::simpleQuery("SELECT e.menuentryid, e.entryid, e.hotkey, e.title, e.hidden, e.sortval, e.md5pass, + b.data AS bootentry + FROM serversetup_menuentry e + LEFT JOIN serversetup_bootentry b USING (entryid) + WHERE e.menuid = :menuid + ORDER BY e.sortval ASC, e.title ASC", ['menuid' => $menu['menuid']]); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $this->items[] = new MenuEntry($row); + } + } + + public function getMenuDefinition($targetVar) + { + $str = "menu {$this->title}\n"; + foreach ($this->items as $item) { + $str .= $item->getMenuItemScript("m_{$this->menuid}", $this->defaultEntryId); + } + if ($this->defaultEntryId === null) { + $defaultLabel = "mx_{$this->menuid}_poweroff"; + } else { + $defaultLabel = "m_{$this->menuid}_{$this->defaultEntryId}"; + } + $str .= "choose"; + if ($this->timeoutMs > 0) { + $str .= " --timeout {$this->timeoutMs}"; + } + $str .= " $targetVar || goto $defaultLabel || goto fail\n"; + if ($this->defaultEntryId === null) { + $str .= "goto skip_{$defaultLabel}\n" + . ":{$defaultLabel}\n" + . "poweroff || goto fail\n" + . ":skip_{$defaultLabel}\n"; + } + return $str; + } + + public function getItemsCode() + { + $str = ''; + foreach ($this->items as $item) { + $str .= $item->getBootEntryScript("m_{$this->menuid}", 'fail'); + $str .= "goto slx_menu\n"; + } + return $str; + } + + /* + * + */ + + public static function forLocation($locationId) + { + $chain = null; + if (Module::isAvailable('location')) { + $chain = Location::getLocationRootChain($locationId); + } + if (!empty($chain)) { + $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, m.defaultentryid, ml.locationid + FROM serversetup_menu m + INNER JOIN serversetup_menu_location ml USING (menuid) + WHERE ml.locationid IN (:chain)", ['chain' => $chain]); + if ($res->rowCount() > 0) { + // Make the location id key, preserving order (closest location is first) + $chain = array_flip($chain); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + // Overwrite the value (numeric ascending values, useless) with menu array of according location + $chain[(int)$row['locationid']] = $row; + } + // Use first one that was found + foreach ($chain as $menu) { + if (is_array($menu)) { + return new IPxeMenu($menu); + } + } + // Should never end up here, but we'd just fall through and use the default + } + } + // We're here, no specific menu, use default + $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid + FROM serversetup_menu + ORDER BY isdefault DESC LIMIT 1"); + if ($menu === false) { + return new EmptyIPxeMenu; + } + return new IPxeMenu($menu); + } + + public static function forClient($ip, $uuid) + { + $locationId = 0; + if (Module::isAvailable('location')) { + $locationId = Location::getFromIpAndUuid($ip, $uuid); + } + return self::forLocation($locationId); + } + +} + +class EmptyIPxeMenu extends IPxeMenu +{ + + /** @noinspection PhpMissingParentConstructorInspection */ + public function __construct() + { + $this->title = 'No menu defined'; + $this->menuid = -1; + $this->items[] = new MenuEntry([ + 'title' => 'Please create a menu in Server-Setup first' + ]); + $this->items[] = new MenuEntry([ + 'title' => 'Bitte erstellen Sie zunächst ein Menü' + ]); + } + +} \ No newline at end of file diff --git a/modules-available/serversetup-bwlp/inc/menuentry.inc.php b/modules-available/serversetup-bwlp/inc/menuentry.inc.php new file mode 100644 index 00000000..9736b7bb --- /dev/null +++ b/modules-available/serversetup-bwlp/inc/menuentry.inc.php @@ -0,0 +1,170 @@ + $value) { + if (property_exists($this, $key)) { + $this->{$key} = $value; + } + } + $this->hotkey = self::getKeyCode($row['hotkey']); + if (!empty($row['bootentry'])) { + $this->bootEntry = BootEntry::fromJson($row['bootentry']); + } + $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null); + } + settype($this->hidden, 'bool'); + settype($this->gap, 'bool'); + settype($this->sortval, 'int'); + settype($this->menuentryid, 'int'); + } + + public function getMenuItemScript($lblPrefix, $requestedDefaultId) + { + $str = 'item '; + if ($this->gap) { + $str .= '--gap '; + } else { + if ($this->hidden) { + if ($this->hotkey === false) + return ''; // Hidden entries without hotkey are illegal + $str .= '--hidden '; + } + if ($this->hotkey !== false) { + $str .= '--key ' . $this->hotkey . ' '; + } + if ($this->menuentryid == $requestedDefaultId) { + $str .= '--default '; + } + $str .= "{$lblPrefix}_{$this->menuentryid} "; + } + $str .= $this->title; + return $str . " || prompt Could not create menu item for {$lblPrefix}_{$this->menuentryid}\n"; + } + + public function getBootEntryScript($lblPrefix, $failLabel) + { + if ($this->bootEntry === null) + return ''; + $str = ":{$lblPrefix}_{$this->menuentryid}\n"; + if (!empty($this->md5pass)) { + $str .= "set slx_hash {$this->md5pass} || goto $failLabel\n" + . "set slx_salt {$this->menuentryid} || goto $failLabel\n" + . "set slx_pw_ok {$lblPrefix}_ok || goto $failLabel\n" + . "set slx_pw_fail slx_menu || goto $failLabel\n" + . "goto slx_pass_check || goto $failLabel\n" + . ":{$lblPrefix}_ok\n"; + } + return $str . $this->bootEntry->toScript($failLabel); + } + + /* + * + */ + + private static function getKeyArray() + { + static $data = false; + if ($data === false) { + $data = [ + 'F5' => 0x107e, + 'F6' => 0x127e, + 'F7' => 0x137e, + 'F8' => 0x147e, + 'F9' => 0x157e, + 'F10' => 0x167e, + 'F11' => 0x187e, + 'F12' => 0x197e, + ]; + for ($i = 1; $i <= 26; ++$i) { + $letter = chr(0x40 + $i); + $data['SHIFT_' . $letter] = 0x40 + $i; + if ($letter !== 'C') { + $data['CTRL_' . $letter] = $i; + } + $data[$letter] = 0x60 + $i; + } + for ($i = 0; $i <= 9; ++$i) { + $data[chr(0x30 + $i)] = 0x30 + $i; + } + asort($data, SORT_NUMERIC); + } + return $data; + } + + /** + * Get all the known/supported keys, usable for menu items. + * + * @return string[] list of known key names + */ + public static function getKeyList() + { + return array_keys(self::getKeyArray()); + } + + /** + * Get the key code ipxe expects for the given named + * key. Returns false if the key name is unknown. + * + * @param string $keyName + * @return false|string Key code as hex string, or false if not found + */ + public static function getKeyCode($keyName) + { + $data = self::getKeyArray(); + if (isset($data[$keyName])) + return '0x' . dechex($data[$keyName]); + return false; + } + + /** + * @param string $keyName desired key name + * @return string $keyName if it's known, empty string otherwise + */ + public static function filterKeyName($keyName) + { + if (isset($data[$keyName])) + return $keyName; + return ''; + } + +} diff --git a/modules-available/serversetup-bwlp/inc/pxelinux.inc.php b/modules-available/serversetup-bwlp/inc/pxelinux.inc.php new file mode 100644 index 00000000..db3dac4b --- /dev/null +++ b/modules-available/serversetup-bwlp/inc/pxelinux.inc.php @@ -0,0 +1,262 @@ + ['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 @@ +handleGetImage(); @@ -44,16 +50,45 @@ class Page_ServerSetup extends Page $this->updatePxeMenu(); } + if ($action === 'savebootentry') { + User::assertPermission('ipxe.bootentry.edit'); + $this->saveBootEntry(); + } + + if ($action === 'savemenu') { + User::assertPermission('ipxe.menu.edit'); + $this->saveMenu(); + } + if (Request::isPost()) { Util::redirect('?do=serversetup'); } User::assertPermission('access-page'); + + if (User::hasPermission('ipxe.*')) { + Dashboard::addSubmenu('?do=serversetup&show=menu', Dictionary::translate('submenu_menu', true)); + } + if (User::hasPermission('edit.address')) { + Dashboard::addSubmenu('?do=serversetup&show=address', Dictionary::translate('submenu_address', true)); + } + if (User::hasPermission('download')) { + Dashboard::addSubmenu('?do=serversetup&show=download', Dictionary::translate('submenu_download', true)); + } + if (Request::get('show') === false) { + $subs = Dashboard::getSubmenus(); + if (empty($subs)) { + User::assertPermission('download'); + } else { + Util::redirect($subs[0]['url']); + } + } } protected function doRender() { Render::addTemplate("heading"); + $task = Property::get('ipxe-task-id'); if ($task !== false) { $task = Taskmanager::status($task); @@ -65,32 +100,143 @@ class Page_ServerSetup extends Page Render::addTemplate('ipxe_update', array('taskid' => $task['id'])); } - Permission::addGlobalTags($perms, null, ['edit.menu', 'edit.address', 'download']); + switch (Request::get('show')) { + case 'editbootentry': + User::assertPermission('ipxe.bootentry.edit'); + $this->showEditBootEntry(); + break; + case 'editmenu': + User::assertPermission('ipxe.menu.view'); + $this->showEditMenu(); + break; + case 'download': + User::assertPermission('download'); + $this->showDownload(); + break; + case 'menu': + User::assertPermission('ipxe.menu.view'); + $this->showMenuList(); + break; + default: + Util::redirect('?do=serversetup'); + break; + } + } + + private function showDownload() + { + // TODO: Make nicer, support more variants (taskmanager-plugin) + Render::addTemplate('download'); + } + + private function showMenuList() + { + $allowedEdit = User::getAllowedLocations('ipxe.menu.edit'); + + // TODO Permission::addGlobalTags($perms, null, ['edit.menu', 'edit.address', 'download']); + + $res = Database::simpleQuery("SELECT m.menuid, m.title, m.isdefault, GROUP_CONCAT(l.locationid) AS locations + FROM serversetup_menu m LEFT JOIN serversetup_menu_location l USING (menuid) GROUP BY menuid ORDER BY title"); + $table = []; + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + if (empty($row['locations'])) { + $locations = []; + $row['allowEdit'] = in_array(0, $allowedEdit); + } else { + $locations = explode(',', $row['locations']); + $row['allowEdit'] = empty(array_diff($locations, $allowedEdit)); + } + $row['locationCount'] = empty($locations) ? '' : count($locations); + $table[] = $row; + } - Render::addTemplate('ipaddress', array( - 'ips' => $this->taskStatus['data']['addresses'], - 'chooseHintClass' => $this->hasIpSet ? '' : 'alert alert-danger', - 'editAllowed' => User::hasPermission("edit.address"), - 'perms' => $perms, + Render::addTemplate('menu-list', array( + 'table' => $table, )); - $data = $this->currentMenu; - if (!User::hasPermission('edit.menu')) { - unset($data['masterpasswordclear']); + } + + private function hasMenuPermission($menuid, $permission) + { + $allowedEditLocations = User::getAllowedLocations($permission); + $allowEdit = in_array(0, $allowedEditLocations); + if (!$allowEdit) { + // Get locations + $locations = Database::queryColumnArray('SELECT locationid FROM serversetup_menu_location + WHERE menuid = :menuid', compact('menuid')); + if (!empty($locations)) { + $allowEdit = count(array_diff($locations, $allowedEditLocations)) === 0; + } } - if (!isset($data['defaultentry'])) { - $data['defaultentry'] = 'net'; + return $allowEdit; + } + + private function showEditMenu() + { + $id = Request::get('id', false, 'int'); + $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid, isdefault + FROM serversetup_menu WHERE menuid = :id", compact('id')); + if ($menu === false) { + Message::addError('invalid-menu-id', $id); + Util::redirect('?do=serversetup&show=menu'); } - if ($data['defaultentry'] === 'net') { - $data['active-net'] = 'checked'; + if (!$this->hasMenuPermission($id, 'ipxe.menu.edit')) { + $menu['readonly'] = 'readonly'; + $menu['disabled'] = 'disabled'; + $menu['plainpass'] = ''; } - if ($data['defaultentry'] === 'hdd') { - $data['active-hdd'] = 'checked'; + $menu['timeout'] = round($menu['timeoutms'] / 1000); + $menu['entries'] = Database::queryAll("SELECT menuentryid, entryid, hotkey, title, hidden, sortval, plainpass FROM + serversetup_menuentry WHERE menuid = :id", compact('id')); + $keyList = array_map(function ($item) { return ['key' => $item]; }, MenuEntry::getKeyList()); + $entryList = Database::queryAll("SELECT entryid, title, hotkey FROM serversetup_bootentry ORDER BY title ASC"); + foreach ($menu['entries'] as &$entry) { + $entry['isdefault'] = ($entry['menuentryid'] == $menu['defaultentryid']); + $entry['keys'] = $keyList; + foreach ($entry['keys'] as &$key) { + if ($key['key'] === $entry['hotkey']) { + $key['selected'] = 'selected'; // TODO: plainpass only when permissions + } + } + $entry['entrylist'] = $entryList; + foreach ($entry['entrylist'] as &$item) { + if ($item['entryid'] == $entry['entryid']) { + $item['selected'] = 'selected'; + } + if (empty($item['title'])) { + $item['title'] = $item['entryid']; + } + } } - if ($data['defaultentry'] === 'custom') { - $data['active-custom'] = 'checked'; + // TODO: Make assigned locations editable + Permission::addGlobalTags($menu['perms'], 0, ['ipxe.menu.edit']); + Render::addTemplate('menu-edit', $menu); + } + + private function showEditBootEntry() + { + $params = []; + $id = Request::get('id', false, 'string'); + if ($id === false) { + $params['exec_checked'] = 'checked'; + $params['entryid'] = 'u-' . dechex(mt_rand(0x1000, 0xffff)) . '-' . dechex(time()); + } else { + // Query existing entry + $row = Database::queryFirst('SELECT entryid, title, builtin, data FROM serversetup_bootentry + WHERE entryid = :id LIMIT 1', ['id' => $id]); + if ($row === false) { + Message::addError('invalid-boot-entry', $id); + Util::redirect('?do=serversetup'); + } + $entry = BootEntry::fromJson($row['data']); + if ($entry === null) { + Message::addError('unknown-boot-entry-type', $id); + Util::redirect('?do=serversetup'); + } + $entry->addFormFields($params); + $params['title'] = $row['title']; + $params['oldentryid'] = $params['entryid'] = $row['entryid']; } - $data['perms'] = $perms; - Render::addTemplate('ipxe', $data); + Render::addTemplate('ipxe-new-boot-entry', $params); } // ----------------------------------------------------------------------------------------------- @@ -131,6 +277,76 @@ class Page_ServerSetup extends Page return true; } + private function saveMenu() + { + $id = Request::post('menuid', false, 'int'); + if ($id === false) { + Message::addError('main.parameter-missing', 'menuid'); + return; + } + $menu = Database::queryFirst("SELECT m.menuid, GROUP_CONCAT(l.locationid) AS locations + FROM serversetup_menu m + LEFT JOIN serversetup_menu_location l USING (menuid) + WHERE menuid = :id", compact('id')); + if ($menu === false) { + Message::addError('no-such-menu', $id); + return; + } + if (!$this->hasMenuPermission($id, 'ipxe.menu.edit')) { + Message::addError('locations.no-permission-location', 'TODO'); + return; + } + // TODO: Validate new locations to be saved (and actually save them) + + Database::exec('UPDATE serversetup_menu SET title = :title, timeoutms = :timeoutms + WHERE menuid = :menuid', [ + 'menuid' => $id, + 'title' => IPxe::sanitizeIpxeString(Request::post('title', '', 'string')), + 'timeoutms' => abs(Request::post('timeoutms', 0, 'int') * 1000), + ]); + if (User::hasPermission('ipxe.menu.edit', 0)) { + Database::exec('UPDATE serversetup_menu SET isdefault = (menuid = :menuid)', ['menuid' => $id]); + } + + $keepIds = []; + $entries = Request::post('entry', false, 'array'); + foreach ($entries as $key => $entry) { + $params = [ + 'entryid' => $entry['entryid'], // TODO validate + 'hotkey' => MenuEntry::filterKeyName($entry['hotkey']), + 'title' => IPxe::sanitizeIpxeString($entry['title']), + 'hidden' => (int)$entry['hidden'], + 'sortval' => (int)$entry['sortval'], + 'plainpass' => $entry['plainpass'], + 'menuid' => $menu['menuid'], + ]; + if (is_numeric($key)) { + $keepIds[] = $key; + $params['menuentryid'] = $key; + $params['md5pass'] = IPxe::makeMd5Pass($entry['plainpass'], $key); + $ret = Database::exec('UPDATE serversetup_menuentry + SET entryid = :entryid, hotkey = :hotkey, title = :title, hidden = :hidden, sortval = :sortval, + plainpass = :plainpass, md5pass = :md5pass + WHERE menuid = :menuid AND menuentryid = :menuentryid', $params, true); + } else { + $ret = Database::exec("INSERT INTO serversetup_menuentry + (menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass) + VALUES (:menuid, :entryid, :hotkey, :title, :hidden, :sortval, :plainpass, '')", $params, true); + if ($ret && !empty($entry['plainpass'])) { + $key = Database::lastInsertId(); + Database::exec('UPDATE serversetup_menuentry SET md5pass = :md5pass WHERE menuentryid = :id', [ + 'md5pass' => IPxe::makeMd5Pass($entry['plainpass'], $key), + 'key' => $id, + ]); + } + } + if ($ret === false) { + Message::addWarning('error-saving-entry', $entry['title'], Database::lastError()); + } + } + Message::addSuccess('menu-saved'); + } + private function updateLocalAddress() { $newAddress = Request::post('ip', 'none'); @@ -184,4 +400,50 @@ class Page_ServerSetup extends Page exit; } + private function saveBootEntry() + { + $oldEntryId = Request::post('entryid', false, 'string'); + $newId = Request::post('newid', false, 'string'); + if (!preg_match('/^[a-z0-9\-_]{1,16}$/', $newId)) { + Message::addError('main.parameter-empty', 'newid'); + return; + } + $data = Request::post('entry', false); + if (!is_array($data)) { + Message::addError('missing-entry-data'); + return; + } + $type = Request::post('type', false, 'string'); + if ($type === 'exec') { + $entry = BootEntry::newStandardBootEntry($data); + } elseif ($type === 'script') { + $entry = BootEntry::newCustomBootEntry($data); + } else { + Message::addError('unknown-entry-type', $type); + return; + } + if ($entry === null) { + Message::addError('main.empty-field'); + return; + } + $params = [ + 'entryid' => $newId, + 'title' => Request::post('title', '', 'string'), + 'data' => json_encode($entry->toArray()), + ]; + // New or update? + if (empty($oldEntryId)) { + // New entry + Database::exec('INSERT INTO serversetup_bootentry (entryid, title, builtin, data) + VALUES (:entryid, :title, 0, :data)', $params); + Message::addSuccess('boot-entry-created', $newId); + } else { + // Edit existing entry + $params['oldid'] = $oldEntryId; + Database::exec('UPDATE serversetup_bootentry SET entryid = :entryid, title = :title, data = :data + WHERE entryid = :oldid AND builtin = 0', $params); + Message::addSuccess('boot-entry-updated', $newId); + } + } + } diff --git a/modules-available/serversetup-bwlp/permissions/permissions.json b/modules-available/serversetup-bwlp/permissions/permissions.json index 44927506..aa2aa001 100644 --- a/modules-available/serversetup-bwlp/permissions/permissions.json +++ b/modules-available/serversetup-bwlp/permissions/permissions.json @@ -8,7 +8,19 @@ "edit.address": { "location-aware": false }, - "edit.menu": { + "ipxe.bootentry.view": { + "location-aware": false + }, + "ipxe.bootentry.edit": { + "location-aware": false + }, + "ipxe.menu.view": { + "location-aware": false + }, + "ipxe.menu.edit": { + "location-aware": true + }, + "ipxe.localboot.edit": { "location-aware": false } } \ No newline at end of file diff --git a/modules-available/serversetup-bwlp/templates/download.html b/modules-available/serversetup-bwlp/templates/download.html new file mode 100644 index 00000000..6752f7fc --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/download.html @@ -0,0 +1,38 @@ + + + \ 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 @@ -

{{lang_moduleHeading}}

\ No newline at end of file + \ No newline at end of file diff --git a/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html b/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html new file mode 100644 index 00000000..fd9e1d72 --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/ipxe-new-boot-entry.html @@ -0,0 +1,108 @@ +

{{lang_newBootEntryHead}}

+ +
+
+ {{lang_bootEntryData}} +
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ + +
+
+ +
+ +
+
+
+
+ + \ 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 @@ -
- - - - -
-
- {{lang_bootMenu}} -
-
-

- {{lang_bootInfo}} -

-
- -
- {{lang_bootBehavior}} -
- - -
-
- - -
-
- - -
-
- -
- {{lang_menuDisplayTime}} -
- - {{lang_seconds}} -
-
- -
- {{lang_masterPassword}} -
- -
- {{lang_masterPasswordHelp}} -
- -
- {{lang_menuCustom}} - -
-
- - -
-
- - - - \ No newline at end of file diff --git a/modules-available/serversetup-bwlp/templates/menu-edit.html b/modules-available/serversetup-bwlp/templates/menu-edit.html new file mode 100644 index 00000000..cf10296e --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/menu-edit.html @@ -0,0 +1,114 @@ +

{{lang_editMenuHead}}

+ +
+
+ {{title}} + {{^title}} + {{lang_newMenu}} + {{/title}} +
+
+
+ + + +
+
+ +
+
+ +
+
+
+
+ +
+
+
+ + {{lang_seconds}} +
+
+
+
+
+
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + {{#entries}} + + + + + {{#entryid}} + + {{/entryid}} + + + + {{/entries}} + +
{{lang_entryId}}{{lang_title}}{{lang_hotkey}}{{lang_sortOrder}}{{lang_password}}
+
+ + +
+
+ {{#entryid}} + + {{/entryid}} + {{^entryid}} + {{lang_spacer}} + {{/entryid}} + + + + + + + + +
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/modules-available/serversetup-bwlp/templates/menu-list.html b/modules-available/serversetup-bwlp/templates/menu-list.html new file mode 100644 index 00000000..a862cff2 --- /dev/null +++ b/modules-available/serversetup-bwlp/templates/menu-list.html @@ -0,0 +1,36 @@ +

{{lang_listOfMenus}}

+ + + + + + + + + + + + {{#table}} + + + + + + + {{/table}} + +
{{lang_menuTitle}}{{lang_locationCount}}{{lang_isDefault}}{{lang_edit}}
+ {{title}} + + {{locationCount}} + + {{#isdefault}} + + {{/isdefault}} + + {{#allowEdit}} + + + + {{/allowEdit}} +
\ No newline at end of file -- cgit v1.2.3-55-g7522