summaryrefslogtreecommitdiffstats
path: root/modules-available/serversetup-bwlp-ipxe/inc
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/serversetup-bwlp-ipxe/inc')
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php244
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php67
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php9
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php211
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php80
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php189
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php37
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php119
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php174
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php59
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php117
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php102
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php97
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php330
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php478
15 files changed, 1732 insertions, 581 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
index dec70528..5812c0cd 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
@@ -12,13 +12,28 @@ abstract class BootEntry
/** Supports both via distinct entry */
const BOTH = 'PCBIOS-EFI';
- public abstract function supportsMode($mode);
+ /**
+ * @var string Internal ID - set to your liking, e.g. the MiniLinux version identifier
+ */
+ protected $internalId;
+
+ public function __construct(string $internalId)
+ {
+ $this->internalId = $internalId;
+ }
+
+ public abstract function supportsMode(string $mode): bool;
- public abstract function toScript($failLabel, $mode);
+ public abstract function toScript(ScriptBuilderBase $builder): string;
- public abstract function toArray();
+ public abstract function toArray(): array;
- public abstract function addFormFields(&$array);
+ public abstract function addFormFields(array &$array): void;
+
+ public function internalId(): string
+ {
+ return $this->internalId;
+ }
/*
*
@@ -28,15 +43,15 @@ abstract class BootEntry
* Return a BootEntry instance from the serialized data.
*
* @param string $module module this entry belongs to, or special values .script/.exec
- * @param string $jsonString serialized entry data
- * @return BootEntry|null instance representing boot entry, null on error
+ * @param string $data serialized entry data
+ * @return ?BootEntry instance representing boot entry, null on error
*/
- public static function fromJson($module, $data)
+ public static function fromJson(string $module, string $data): ?BootEntry
{
- if ($module{0} !== '.') {
+ if ($module[0] !== '.') {
// Hook from other module
$hook = Hook::loadSingle($module, 'ipxe-bootentry');
- if ($hook === false) {
+ if ($hook === null) {
error_log('Module ' . $module . ' doesnt have an ipxe-bootentry hook');
return null;
}
@@ -45,26 +60,29 @@ abstract class BootEntry
return null;
return $ret->getBootEntry($data);
}
- if (is_string($data)) {
- $data = json_decode($data, true);
- }
+ $data = json_decode($data, true);
+ if (!is_array($data))
+ return null;
if ($module === '.script') {
return new CustomBootEntry($data);
}
if ($module === '.exec') {
return new StandardBootEntry($data);
}
+ if ($module === '.special') {
+ return new SpecialBootEntry($data);
+ }
return null;
}
- public static function forMenu($menuId)
+ public static function forMenu(int $menuId): MenuBootEntry
{
return new MenuBootEntry($menuId);
}
- public static function newStandardBootEntry($initData, $efi = false, $arch = false)
+ public static function newStandardBootEntry($initData, $efi = false, $arch = false, string $internalId = ''): ?StandardBootEntry
{
- $ret = new StandardBootEntry($initData, $efi, $arch);
+ $ret = new StandardBootEntry($initData, $efi, $arch, $internalId);
$list = [];
if ($ret->arch() !== self::EFI) {
$list[] = self::BIOS;
@@ -82,9 +100,9 @@ abstract class BootEntry
return $ret;
}
- public static function newCustomBootEntry($initData)
+ public static function newCustomBootEntry($initData): ?CustomBootEntry
{
- if (empty($initData['script']))
+ if (!is_array($initData) || empty($initData))
return null;
return new CustomBootEntry($initData);
}
@@ -92,15 +110,14 @@ abstract class BootEntry
/**
* 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
+ * @return ?BootEntry null = unknown entry type, BootEntry instance on success
*/
- public static function fromDatabaseId($id)
+ public static function fromDatabaseId(string $id): ?BootEntry
{
$row = Database::queryFirst("SELECT module, data FROM serversetup_bootentry
WHERE entryid = :id LIMIT 1", ['id' => $id]);
if ($row === false)
- return false;
+ return null;
return self::fromJson($row['module'], $row['data']);
}
@@ -110,11 +127,11 @@ abstract class BootEntry
*
* @return BootEntry[] all existing BootEntries
*/
- public static function getAll()
+ public static function getAll(): array
{
- $res = Database::simpleQuery("SELECT entryid, data FROM serversetup_bootentry");
+ $res = Database::simpleQuery("SELECT entryid, module, data FROM serversetup_bootentry");
$ret = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$tmp = self::fromJson($row['module'], $row['data']);
if ($tmp === null)
continue;
@@ -135,13 +152,16 @@ class StandardBootEntry extends BootEntry
* @var ExecData same for EFI
*/
protected $efi;
-
- protected $arch; // Constants below
+ /**
+ * @var ?string BootEntry Constants above
+ */
+ protected $arch;
const KEYS = ['executable', 'initRd', 'commandLine', 'replace', 'imageFree', 'autoUnload', 'resetConsole', 'dhcpOptions'];
- public function __construct($data, $efi = false, $arch = false)
+ public function __construct($data, $efi = false, ?string $arch = null, string $internalId = '')
{
+ parent::__construct($internalId);
$this->pcbios = new ExecData();
$this->efi = new ExecData();
if ($data instanceof PxeSection) {
@@ -214,10 +234,7 @@ class StandardBootEntry extends BootEntry
}
}
- /**
- * @param PxeSection $data
- */
- private function fromPxeMenu($data)
+ private function fromPxeMenu(PxeSection $data): void
{
$bios = $this->pcbios;
$bios->executable = $data->kernel;
@@ -241,12 +258,12 @@ class StandardBootEntry extends BootEntry
$bios->commandLine = trim(preg_replace('/\s+/', ' ', $bios->commandLine));
}
- public function arch()
+ public function arch(): ?string
{
return $this->arch;
}
- public function supportsMode($mode)
+ public function supportsMode(string $mode): bool
{
if ($mode === $this->arch || $this->arch === BootEntry::AGNOSTIC)
return true;
@@ -257,73 +274,16 @@ class StandardBootEntry extends BootEntry
return false;
}
- public function toScript($failLabel, $mode)
+ public function toScript(ScriptBuilderBase $builder): string
{
- if (!$this->supportsMode($mode)) {
- return "prompt Entry doesn't have an executable for mode $mode\n";
- }
- if ($this->arch === BootEntry::AGNOSTIC || $mode == BootEntry::BIOS) {
- $entry = $this->pcbios;
- } else {
- $entry = $this->efi;
- }
- $entry->sanitize();
-
- $script = '';
- if ($entry->resetConsole) {
- $script .= "console ||\n";
- }
- if ($entry->imageFree) {
- $script .= "imgfree ||\n";
- }
- foreach ($entry->dhcpOptions as $opt) {
- if (empty($opt['value'])) {
- $val = '${}';
- } else {
- if (empty($opt['hex'])) {
- $val = bin2hex($opt['value']);
- } else {
- $val = $opt['value'];
- }
- preg_match_all('/[0-9a-f]{2}/', $val, $out);
- $val = implode(':', $out[0]);
- }
- $script .= 'set net${idx}/' . $opt['opt'] . ':hex ' . $val
- . ' || prompt Cannot override DHCP server option ' . $opt['opt'] . ". Press any key to continue anyways.\n";
- }
- $initrds = [];
- if (!empty($entry->initRd)) {
- foreach (array_values($entry->initRd) as $i => $initrd) {
- if (empty($initrd))
- continue;
- $script .= "initrd --name initrd$i $initrd || goto $failLabel\n";
- $initrds[] = "initrd$i";
- }
- }
- $script .= "boot ";
- if ($entry->autoUnload) {
- $script .= "-a ";
- }
- if ($entry->replace) {
- $script .= "-r ";
- }
- $script .= $entry->executable;
- if (empty($initrds)) {
- $rdBase = '';
- } else {
- $rdBase = " initrd=" . implode(',', $initrds);
- }
- if (!empty($entry->commandLine)) {
- $script .= "$rdBase {$entry->commandLine}";
- }
- $script .= " || goto $failLabel\n";
- if ($entry->resetConsole) {
- $script .= "goto start ||\n";
- }
- return $script;
+ if ($this->arch === BootEntry::AGNOSTIC) // Same as below, could construct fall-through but this is more clear
+ return $builder->execDataToScript($this->pcbios, null, null);
+ return $builder->execDataToScript(null,
+ $this->supportsMode(BootEntry::BIOS) ? $this->pcbios : null,
+ $this->supportsMode(BootEntry::EFI) ? $this->efi : null);
}
- public function addFormFields(&$array)
+ public function addFormFields(array &$array): void
{
$array[$this->arch . '_selected'] = 'selected';
$array['entries'][] = $this->pcbios->toFormFields(BootEntry::BIOS);
@@ -331,7 +291,10 @@ class StandardBootEntry extends BootEntry
$array['exec_checked'] = 'checked';
}
- public function toArray()
+ /**
+ * @return array{PCBIOS: array, EFI: array, arch: string}
+ */
+ public function toArray(): array
{
return [
BootEntry::BIOS => $this->pcbios->toArray(),
@@ -343,65 +306,118 @@ class StandardBootEntry extends BootEntry
class CustomBootEntry extends BootEntry
{
- protected $script;
+ /**
+ * @var string iPXE
+ */
+ protected $ipxe = '';
+
+ protected $bash;
+
+ protected $grub;
public function __construct($data)
{
- if (is_array($data) && isset($data['script'])) {
- $this->script = $data['script'];
+ parent::__construct('custom');
+ if (is_array($data)) {
+ $this->ipxe = $data['script'] ?? ''; // LEGACY
+ foreach (['bash', 'grub'] as $key) {
+ $this->{$key} = $data[$key] ?? '';
+ }
}
}
- public function supportsMode($mode)
+ public function supportsMode(string $mode): bool
{
return true;
}
- public function toScript($failLabel, $mode)
+ public function toScript(ScriptBuilderBase $builder): string
{
- return str_replace('%fail%', $failLabel, $this->script) . "\n";
+ // TODO: A (very) simple translator for oneliners like "poweroff || goto fail" maybe?
+ if ($builder instanceof ScriptBuilderIpxe)
+ return $this->ipxe;
+ if ($builder instanceof ScriptBuilderBash)
+ return $this->bash;
+ if ($builder instanceof ScriptBuilderGrub)
+ return $this->grub;
+ return '';
}
- public function addFormFields(&$array)
+ public function addFormFields(array &$array): void
{
$array['entry'] = [
- 'script' => $this->script,
+ 'script' => $this->ipxe,
];
$array['script_checked'] = 'checked';
}
- public function toArray()
+ /**
+ * @return array{script: string}
+ */
+ public function toArray(): array
{
- return ['script' => $this->script];
+ return ['script' => $this->ipxe];
}
}
class MenuBootEntry extends BootEntry
{
+ /** @var int */
protected $menuId;
- public function __construct($menuId)
+ public function __construct(int $menuId)
{
+ parent::__construct('menu-' . $menuId);
$this->menuId = $menuId;
}
- public function supportsMode($mode)
+ public function supportsMode(string $mode): bool
{
return true;
}
- public function toScript($failLabel, $mode)
+ public function toScript(ScriptBuilderBase $builder): string
{
- return 'chain -ar ${self}&menuid=' . $this->menuId . ' || goto ' . $failLabel . "\n";
+ $menu = IPxeMenu::get($this->menuId, true);
+ return $builder->menuToScript($menu);
}
- public function toArray()
+ public function toArray(): array
{
return [];
}
- public function addFormFields(&$array)
+ public function addFormFields(array &$array): void
{
}
}
+class SpecialBootEntry extends BootEntry
+{
+
+ private $type;
+
+ public function __construct($type)
+ {
+ $this->type = $type['type'] ?? $type;
+ parent::__construct('special-' . $this->type);
+ }
+
+ public function supportsMode(string $mode): bool
+ {
+ return true;
+ }
+
+ public function toScript(ScriptBuilderBase $builder): string
+ {
+ return $builder->getSpecial($this->type);
+ }
+
+ public function toArray(): array
+ {
+ return [];
+ }
+
+ public function addFormFields(array &$array): void { }
+
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php
index cf180006..ab55c888 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php
@@ -6,37 +6,37 @@ abstract class BootEntryHook
/**
* @var string -- set by ipxe, not module implementing hook
*/
- public $moduleId;
+ public $moduleId = '';
/**
* @var string -- set by ipxe, not module implementing hook
*/
- public $checked;
+ public $checked = '';
- private $selectedId;
+ private $selectedId = '';
private $data = [];
/**
* @return string
*/
- public abstract function name();
+ public abstract function name(): string;
/**
* @return HookExtraField[]
*/
- public abstract function extraFields();
+ public abstract function extraFields(): array;
- public abstract function isValidId($id);
+ public abstract function isValidId(string $id): bool;
/**
* @return HookEntryGroup[]
*/
- protected abstract function groupsInternal();
+ protected abstract function groupsInternal(): array;
/**
* @return HookEntryGroup[]
*/
- public final function groups()
+ public final function groups(): array
{
$groups = $this->groupsInternal();
foreach ($groups as $group) {
@@ -50,34 +50,48 @@ abstract class BootEntryHook
}
/**
- * @param $id
* @return BootEntry|null the actual boot entry instance for given entry, null if invalid id
*/
- public abstract function getBootEntryInternal($localData);
+ public abstract function getBootEntryInternal(array $localData): ?BootEntry;
- public final function getBootEntry($data)
+ public final function getBootEntry(string $jsonString): ?BootEntry
{
- if (!is_array($data)) {
- $data = json_decode($data, true);
- }
+ $data = json_decode($jsonString, true);
return $this->getBootEntryInternal($data);
}
- public function setSelected($id)
+ /**
+ * @param string $mixed either the plain ID if the entry to be marked as selected, or the JSON string representing
+ * the entire entry, which must have a key called 'id' that will be used as the ID then.
+ */
+ public function setSelected(string $mixed): void
{
- $json = @json_decode($id, true);
+ $json = @json_decode($mixed, true);
if (is_array($json)) {
$id = $json['id'];
$this->data = $json;
+ } else {
+ $id = $mixed;
}
$this->selectedId = $id;
}
- public function renderExtraFields()
+ /**
+ * @return string ID of entry that was marked as selected by setSelected()
+ */
+ public function getSelected(): string
+ {
+ return $this->selectedId;
+ }
+
+ /**
+ * @return HookExtraField[]
+ */
+ public function renderExtraFields(): array
{
$list = $this->extraFields();
- foreach ($list as &$entry) {
- $entry->currentValue = isset($this->data[$entry->name]) ? $this->data[$entry->name] : $entry->default;
+ foreach ($list as $entry) {
+ $entry->currentValue = $this->data[$entry->name] ?? $entry->default;
$entry->hook = $this;
}
return $list;
@@ -126,14 +140,7 @@ class HookEntry
*/
public $selected;
- /**
- * HookEntry constructor.
- *
- * @param string $id
- * @param string $name
- * @param bool $valid
- */
- public function __construct($id, $name, $valid)
+ public function __construct(string $id, string $name, bool $valid)
{
$this->id = $id;
$this->name = $name;
@@ -164,7 +171,7 @@ class HookExtraField
*/
public $hook;
- public function __construct($name, $type, $default)
+ public function __construct(string $name, string $type, $default)
{
$this->name = $name;
$this->type = $type;
@@ -185,10 +192,10 @@ class HookExtraField
return $val;
}
- public function html()
+ public function html(): string
{
$fieldId = 'extra-' . $this->hook->moduleId . '-' . $this->name;
- $fieldText = htmlspecialchars(Dictionary::translateFileModule($this->hook->moduleId, 'module', 'ipxe-' . $this->name, true));
+ $fieldText = htmlspecialchars(Dictionary::translateFileModule($this->hook->moduleId, 'module', 'ipxe-' . $this->name));
if (is_array($this->type)) {
$out = '<label for="' . $fieldId . '">' . $fieldText . '</label><select class="form-control" name="' . $fieldId . '" id="' . $fieldId . '">';
foreach ($this->type as $entry) {
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php
index 97f98b94..e4f7a1d7 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php
@@ -108,7 +108,10 @@ class ExecData
$this->dhcpOptions = array_values($this->dhcpOptions);
}
- public function toArray()
+ /**
+ * @return array{executable: string, initRd: string[], commandLine: string, imageFree: bool, replace: bool, autoUnload: bool, resetConsole: bool, dhcpOptions: array}
+ */
+ public function toArray(): array
{
$this->sanitize();
return [
@@ -123,7 +126,7 @@ class ExecData
];
}
- public function toFormFields($arch)
+ public function toFormFields(string $arch): array
{
$this->sanitize();
$opts = [];
@@ -159,4 +162,4 @@ class ExecData
];
}
-} \ No newline at end of file
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php
index 4c2a7678..5e0531ab 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php
@@ -12,13 +12,13 @@ class IPxe
* Import all IP-Range based pxe menus from the given directory.
*
* @param string $configPath The pxelinux.cfg path where to look for menu files in hexadecimal IP format.
- * @return Number of menus imported
+ * @return int Number of menus imported
*/
- public static function importSubnetPxeMenus($configPath)
+ public static function importSubnetPxeMenus(string $configPath): int
{
$res = Database::simpleQuery('SELECT menuid, entryid FROM serversetup_menuentry ORDER BY sortval ASC');
$menus = [];
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
if (!isset($menus[$row['menuid']])) {
$menus[(int)$row['menuid']] = [];
}
@@ -40,7 +40,7 @@ class IPxe
WHERE startaddr >= :start AND endaddr <= :end", compact('start', 'end'));
$locations = [];
// Iterate over result, eliminate those that are dominated by others
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
foreach ($locations as &$loc) {
if ($row['startaddr'] <= $loc['startaddr'] && $row['endaddr'] >= $loc['endaddr']) {
$loc = false;
@@ -52,6 +52,10 @@ class IPxe
$locations[] = $row;
}
$menu = PxeLinux::parsePxeLinux($content, true);
+ if ($menu === null) {
+ error_log("Skipping empty pxelinux menu file $file");
+ continue;
+ }
// Insert all entries first, so we can get the list of entry IDs
$entries = [];
self::importPxeMenuEntries($menu, $entries);
@@ -83,10 +87,10 @@ class IPxe
} else {
error_log('Imported menu ' . $menu->title . ' is NEW, using for ' . count($locations) . ' locations.');
// Insert new menu
- $menuId = self::insertMenu($menu, 'Auto Imported', false, 0, [], []);
- if ($menuId === false)
+ $menuId = self::insertMenu($menu, 'Auto Imported', null, 0, [], []);
+ if ($menuId === null)
continue;
- $menus[(int)$menuId] = $entries;
+ $menus[$menuId] = $entries;
$importCount++;
}
foreach ($locations as $loc) {
@@ -103,20 +107,20 @@ class IPxe
return $importCount;
}
- public static function importLegacyMenu($force = false)
+ public static function importLegacyMenu(bool $force = false): bool
{
// See if anything is there
if (!$force && false !== Database::queryFirst("SELECT menuentryid FROM serversetup_menuentry LIMIT 1"))
return false; // Already exists
// Now create the default entry
self::createDefaultEntries();
- $prepend = ['bwlp-default' => false, 'localboot' => false];
+ $prepend = ['bwlp-default' => null, 'localboot' => null];
$defaultLabel = 'bwlp-default';
$menuTitle = 'bwLehrpool Bootauswahl';
$pxeConfig = '';
$timeoutSecs = 60;
- // Try to import any customization
- $oldMenu = Property::getBootMenu();
+ // Try to import any customization of the legacy PXELinux menu (despite the property name hinting at iPXE)
+ $oldMenu = json_decode(Property::get('ipxe-menu'), true);
if (is_array($oldMenu)) {
//
if (isset($oldMenu['timeout'])) {
@@ -136,47 +140,45 @@ class IPxe
}
}
$append = [
- '',
- 'bwlp-default-dbg' => false,
- '',
- 'poweroff' => false,
+ new PxeSection(null),
+ 'bwlp-default-dbg' => null,
+ new PxeSection(null),
+ 'poweroff' => null,
];
- return self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append);
+ self::insertMenu(PxeLinux::parsePxeLinux($pxeConfig, false), $menuTitle, $defaultLabel, $timeoutSecs, $prepend, $append);
+ return !empty($pxeConfig);
}
/**
- * @param PxeMenu $pxeMenu
- * @param string $menuTitle
- * @param string|false $defaultLabel Fallback for the default label, if PxeMenu doesn't set one
+ * @param ?string $defaultLabel Fallback for the default label, if PxeMenu doesn't set one
* @param int $defaultTimeoutSeconds Default timeout, if PxeMenu doesn't set one
- * @param array $prepend
- * @param array $append
- * @return int|false
+ * @param (?PxeSection)[] $prepend
+ * @param (?PxeSection)[] $append
+ * @return ?int ID of newly created menu, or null on error, e.g. if the menu is empty
*/
- public static function insertMenu($pxeMenu, $menuTitle, $defaultLabel, $defaultTimeoutSeconds, $prepend, $append)
+ public static function insertMenu(?PxeMenu $pxeMenu, string $menuTitle, ?string $defaultLabel, int $defaultTimeoutSeconds,
+ array $prepend, array $append): ?int
{
$timeoutMs = [];
$menuEntries = $prepend;
- settype($menuEntries, 'array');
- if (!empty($pxeMenu)) {
- $pxe =& $pxeMenu;
- if (!empty($pxe->title)) {
- $menuTitle = $pxe->title;
+ if ($pxeMenu !== null) {
+ if (!empty($pxeMenu->title)) {
+ $menuTitle = $pxeMenu->title;
}
- if ($pxe->timeoutLabel !== null && $pxe->hasLabel($pxe->timeoutLabel)) {
- $defaultLabel = $pxe->timeoutLabel;
- } elseif ($pxe->hasLabel($pxe->default)) {
- $defaultLabel = $pxe->default;
+ if ($pxeMenu->timeoutLabel !== null && $pxeMenu->hasLabel($pxeMenu->timeoutLabel)) {
+ $defaultLabel = $pxeMenu->timeoutLabel;
+ } elseif ($pxeMenu->hasLabel($pxeMenu->default)) {
+ $defaultLabel = $pxeMenu->default;
}
- $timeoutMs[] = $pxe->timeoutMs;
- $timeoutMs[] = $pxe->totalTimeoutMs;
- self::importPxeMenuEntries($pxe, $menuEntries);
+ $timeoutMs[] = $pxeMenu->timeoutMs;
+ $timeoutMs[] = $pxeMenu->totalTimeoutMs;
+ self::importPxeMenuEntries($pxeMenu, $menuEntries);
}
- if (is_array($append)) {
+ if (!empty($append)) {
$menuEntries += $append;
}
if (empty($menuEntries))
- return false;
+ return null;
// Make menu
$timeoutMs = array_filter($timeoutMs, function($x) { return is_int($x) && $x > 0; });
if (empty($timeoutMs)) {
@@ -195,25 +197,29 @@ class IPxe
// Figure out entryid for default label
// Fiddly diddly way of getting the mangled entryid for the wanted pxe menu label
$defaultEntryId = false;
+ $fallbackDefault = false;
foreach ($menuEntries as $entryId => $section) {
- if ($section instanceof PxeSection) {
- if ($section->isDefault) {
- $defaultEntryId = $entryId;
- break;
- }
- if ($section->label === $defaultLabel) {
- $defaultEntryId = $entryId;
- }
+ if ($section === null)
+ continue;
+ if ($section->isDefault) {
+ $defaultEntryId = $entryId;
+ break;
+ }
+ if ($section->label === $defaultLabel) {
+ $defaultEntryId = $entryId;
+ }
+ if ($fallbackDefault === false && !empty($entryId)) {
+ $fallbackDefault = $entryId;
}
}
if ($defaultEntryId === false) {
- $defaultEntryId = array_keys($menuEntries)[0];
+ $defaultEntryId = $fallbackDefault;
}
// Link boot entries to menu
$defaultMenuEntryId = null;
$order = 1000;
foreach ($menuEntries as $entryId => $entry) {
- if (is_string($entry)) {
+ if ($entry !== null && $entry->isTextOnly()) {
// Gap entry
Database::exec("INSERT INTO serversetup_menuentry
(menuid, entryid, hotkey, title, hidden, sortval, plainpass, md5pass)
@@ -221,7 +227,7 @@ class IPxe
'menuid' => $menuId,
'entryid' => null,
'hotkey' => '',
- 'title' => self::sanitizeIpxeString($entry),
+ 'title' => self::sanitizeIpxeString($entry->title),
'hidden' => 0,
'sortval' => $order += 100,
]);
@@ -232,7 +238,7 @@ class IPxe
continue;
$data['pass'] = '';
$data['hidden'] = 0;
- if ($entry instanceof PxeSection) {
+ if ($entry !== null) {
$data['hidden'] = (int)$entry->isHidden;
// Prefer explicit data from this imported menu over the defaults
$title = self::sanitizeIpxeString($entry->title);
@@ -243,7 +249,7 @@ class IPxe
$data['hotkey'] = $entry->hotkey;
}
if (!empty($entry->passwd)) {
- // Most likely it's a hash so we cannot recover; ask people to reset
+ // Most likely it's a hash, so we cannot recover; ask people to reset
$data['pass'] ='please_reset';
}
}
@@ -268,36 +274,28 @@ class IPxe
/**
* Import only the bootentries from the given PXELinux menu
- * @param PxeMenu $pxe
- * @param array $menuEntries Where to append the generated menu items to
+ *
+ * @param PxeSection[] $menuEntries Where to append the generated menu items to
*/
- public static function importPxeMenuEntries($pxe, &$menuEntries)
+ public static function importPxeMenuEntries(PxeMenu $pxe, array &$menuEntries): void
{
if (self::$allEntries === false) {
self::$allEntries = BootEntry::getAll();
}
foreach ($pxe->sections as $section) {
- if ($section->localBoot !== false || preg_match('/chain\.c32$/i', $section->kernel)) {
+ if ($section->isLocalboot()) {
$menuEntries['localboot'] = $section;
continue;
}
- if ($section->label === null) {
- if (!$section->isHidden && !empty($section->title)) {
- $menuEntries[] = $section->title;
- }
- continue;
- }
- if (empty($section->kernel)) {
- if (!$section->isHidden && !empty($section->title)) {
- $menuEntries[] = $section->title;
- }
+ if ($section->isTextOnly()) {
+ $menuEntries[] = $section;
continue;
}
$label = self::cleanLabelFixLocal($section);
$entry = self::pxe2BootEntry($section);
if ($entry === null)
continue; // Error? Ignore
- if ($label !== false || ($label = array_search($entry, self::$allEntries))) {
+ if ($label !== false || ($label = array_search($entry, self::$allEntries)) !== false) {
// Exact Duplicate, Do Nothing
error_log('Ignoring duplicate boot entry ' . $section->label . ' (' . $section->kernel . ')');
} else {
@@ -318,9 +316,10 @@ class IPxe
if (empty($title)) {
$title = $label;
}
- Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data)
- VALUES (:label, :hotkey, :title, 0, :data)', [
+ Database::exec('INSERT IGNORE INTO serversetup_bootentry (entryid, module, hotkey, title, builtin, data)
+ VALUES (:label, :module, :hotkey, :title, 0, :data)', [
'label' => $label,
+ 'module' => ($entry instanceof StandardBootEntry) ? '.exec' : '.script',
'hotkey' => $hotkey,
'title' => $title,
'data' => json_encode($data),
@@ -332,52 +331,43 @@ class IPxe
public static function createDefaultEntries()
{
- Database::exec( 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data)
- VALUES (:entryid, :hotkey, :title, 1, :data) ON DUPLICATE KEY UPDATE data = VALUES(data)',
+ $query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, module, data)
+ VALUES (:entryid, :hotkey, :title, 1, :module, :data)
+ ON DUPLICATE KEY UPDATE builtin = 1, module = VALUES(module), data = VALUES(data)';
+ Database::exec($query,
[
'entryid' => 'bwlp-default',
'hotkey' => 'B',
'title' => 'bwLehrpool-Umgebung starten',
+ 'module' => 'minilinux',
'data' => json_encode([
- 'script' => '
-imgfree ||
-set slxextra initrd=logo ||
-initrd /boot/default/initramfs-stage31 || goto fail
-initrd --name logo /tftp/bwlp.cpio || clear slxextra
-boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boot/default quiet splash loglevel=5 rd.systemd.show_status=auto intel_iommu=igfx_off ${ipappend1} ${ipappend2} || goto fail
-',
+ 'id' => 'default',
+ 'kcl-extra' => '',
+ 'debug' => false,
]),
]);
- $query = 'INSERT IGNORE INTO serversetup_bootentry (entryid, hotkey, title, builtin, data)
- VALUES (:entryid, :hotkey, :title, 1, :data) ON DUPLICATE KEY UPDATE data = VALUES(data)';
Database::exec($query,
[
'entryid' => 'bwlp-default-dbg',
'hotkey' => 'D',
'title' => 'bwLehrpool-Umgebung starten (nosplash, debug output)',
+ 'module' => 'minilinux',
'data' => json_encode([
- 'executable' => ['PCBIOS' => '/boot/default/kernel'],
- 'initRd' => ['PCBIOS' => ['/boot/default/initramfs-stage31']],
- 'commandLine' => ['PCBIOS' => 'slxbase=boot/default loglevel=7 intel_iommu=igfx_off ${ipappend1} ${ipappend2}'],
- 'replace' => true,
- 'autoUnload' => true,
- 'resetConsole' => true,
- 'arch' => 'agnostic',
+ 'id' => 'default',
+ 'kcl-extra' => '',
+ 'debug' => true,
]),
]);
Database::exec($query,
[
'entryid' => 'bwlp-default-sh',
- 'hotkey' => 'D',
+ 'hotkey' => 'S',
'title' => 'bwLehrpool-Umgebung starten (nosplash, !!! debug shell !!!)',
+ 'module' => 'minilinux',
'data' => json_encode([
- 'executable' => ['PCBIOS' => '/boot/default/kernel'],
- 'initRd' => ['PCBIOS' => ['/boot/default/initramfs-stage31']],
- 'commandLine' => ['PCBIOS' => 'slxbase=boot/default loglevel=7 debug=1 intel_iommu=igfx_off ${ipappend1} ${ipappend2}'],
- 'replace' => true,
- 'autoUnload' => true,
- 'resetConsole' => true,
- 'arch' => 'agnostic',
+ 'id' => 'default',
+ 'kcl-extra' => 'debug=1',
+ 'debug' => true,
]),
]);
Database::exec($query,
@@ -385,17 +375,17 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
'entryid' => 'localboot',
'hotkey' => 'L',
'title' => 'Lokales System starten',
- 'data' => json_encode([
- 'script' => 'goto slx_localboot || goto %fail% ||',
- ]),
+ 'module' => '.special',
+ 'data' => json_encode(['type' => 'localboot']),
]);
Database::exec($query,
[
'entryid' => 'poweroff',
'hotkey' => 'P',
'title' => 'Power off',
+ 'module' => '.script',
'data' => json_encode([
- 'script' => 'poweroff || goto %fail% ||',
+ 'script' => 'poweroff || goto fail ||',
]),
]);
Database::exec($query,
@@ -403,8 +393,9 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
'entryid' => 'reboot',
'hotkey' => 'R',
'title' => 'Reboot',
+ 'module' => '.script',
'data' => json_encode([
- 'script' => 'reboot || goto %fail% ||',
+ 'script' => 'reboot || goto fail ||',
]),
]);
}
@@ -415,10 +406,9 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
* Also it patches the entry if it's referencing the local bwlp install
* but with different options.
*
- * @param PxeSection $section
* @return string|false existing label if match, false otherwise
*/
- private static function cleanLabelFixLocal($section)
+ private static function cleanLabelFixLocal(PxeSection $section)
{
$myip = Property::getServerIp();
// Detect our "old" entry types
@@ -441,10 +431,9 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
}
/**
- * @param PxeSection $section
* @return BootEntry|null The according boot entry, null if it's unparsable
*/
- private static function pxe2BootEntry($section)
+ private static function pxe2BootEntry(PxeSection $section): ?BootEntry
{
if (preg_match('/(pxechain\.com|pxechn\.c32)$/i', $section->kernel)) {
// Chaining -- create script
@@ -473,7 +462,7 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
$script .= "set netX/{$opt}:{$type} {$args[$i]} || goto %fail%\n";
}
}
- } elseif ($arg{0} === '-') {
+ } elseif ($arg[0] === '-') {
continue;
} elseif ($file === false) {
$file = self::parseFile($arg);
@@ -503,11 +492,8 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
/**
* Parse PXELINUX file notion. Basically, turn
* server::file into tftp://server/file.
- *
- * @param string $file
- * @return string
*/
- private static function parseFile($file)
+ private static function parseFile(string $file): string
{
if (preg_match(',^([^:/]+)::(.*)$,', $file, $out)) {
return 'tftp://' . $out[1] . '/' . $out[2];
@@ -515,12 +501,12 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
return $file;
}
- public static function sanitizeIpxeString($string)
+ public static function sanitizeIpxeString(string $string): string
{
return str_replace(['&', '|', ';', '$', "\r", "\n"], ['+', '/', ':', 'S', ' ', ' '], $string);
}
- public static function makeMd5Pass($plainpass, $salt)
+ public static function makeMd5Pass(string $plainpass, string $salt): string
{
if (empty($plainpass))
return '';
@@ -535,15 +521,16 @@ boot -a -r /boot/default/kernel initrd=initramfs-stage31 ${slxextra} slxbase=boo
* remove any occurrence of either "option" or "option=something". If the argument starts with a
* '+', it will be added to the command line after removing the '+'. If the argument starts with any
* other character, it will also be added to the command line.
+ *
* @param string $cmdLine command line to modify
- * @param string $modifier modification string of space separated arguments
+ * @param string $modifier modification string of space separated arguments
* @return string the modified command line
*/
- public static function modifyCommandLine($cmdLine, $modifier)
+ public static function modifyCommandLine(string $cmdLine, string $modifier): string
{
$items = preg_split('/\s+/', $modifier, -1, PREG_SPLIT_NO_EMPTY);
foreach ($items as $item) {
- if ($item{0} === '-') {
+ if ($item[0] === '-') {
$item = preg_quote(substr($item, 1), '/');
$cmdLine = preg_replace('/(^|\s)' . $item . '(=\S*)?($|\s)/', ' ', $cmdLine);
} else {
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php
new file mode 100644
index 00000000..a2b25f55
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php
@@ -0,0 +1,80 @@
+<?php
+
+class IPxeBuilder
+{
+
+ const PROP_IPXE_BUILDSTRING = 'ipxe.compile-time';
+ const PROP_IPXE_HASH = 'ipxe.commit-hash';
+ const PROP_IPXE_COMPILE_TASKID = 'ipxe-task-id';
+ const VERSION_LIST_TASK = 'ipxe-version-list-id';
+ const PROP_VERSION_SELECT_TASKID = 'ipxe-version-select-id';
+
+ /**
+ * Checkout given commit/ref of ipxe repo. Returns the according task-id, or null on error
+ *
+ * @param string $version version ref (commit, tag, ...)
+ * @param ?string $parent parent task id, if any
+ */
+ public static function setIpxeVersion(string $version, ?string $parent = null): ?string
+ {
+ $task = Taskmanager::submit('IpxeVersion', [
+ 'action' => 'CHECKOUT',
+ 'ref' => $version,
+ 'parentTask' => $parent,
+ ]);
+ if (!Taskmanager::isTask($task))
+ return null;
+ TaskmanagerCallback::addCallback($task, 'ipxeVersionSet');
+ Property::set(IPxeBuilder::PROP_VERSION_SELECT_TASKID, $task['id'], 2);
+ return $task['id'];
+ }
+
+ public static function getVersionTaskResult(): ?array
+ {
+ $task = Taskmanager::status(IPxeBuilder::VERSION_LIST_TASK);
+ if (!Taskmanager::isTask($task) || Taskmanager::isFailed($task)) {
+ $task = Taskmanager::submit('IpxeVersion',
+ ['id' => IPxeBuilder::VERSION_LIST_TASK, 'action' => 'LIST']);
+ }
+ $task = Taskmanager::waitComplete($task);
+ if (Taskmanager::isFinished($task) && !Taskmanager::isFailed($task)) {
+ return $task['data'];
+ }
+ return null;
+ }
+
+ /**
+ * Callback when compile Taskmanager job finished
+ */
+ public static function compileCompleteCallback(array $task): void
+ {
+ if (!Taskmanager::isFinished($task) || Taskmanager::isFailed($task))
+ return;
+ $version = 'Unknown';
+ if (isset($task['data']['hash'])) {
+ $hash = $task['data']['hash'];
+ Property::set(IPxeBuilder::PROP_IPXE_HASH, $hash);
+ $version = $hash;
+ $list = IPxeBuilder::getVersionTaskResult();
+ if (isset($list['versions'])) {
+ foreach ($list['versions'] as $v) {
+ if ($v['hash'] === $version) {
+ // Do NOT change (see below)
+ $version = date('Y-m-d H:i', $v['date']) . ' (' . substr($version, 0, 7) . ')';
+ break;
+ }
+ }
+ }
+ }
+ // Do NOT change the format of this string -- we depend on it in ScriptBuilderIpxe::output()
+ $buildString = date('d.m.Y H:i') . ', Version: ' . $version;
+ Property::set(IPxeBuilder::PROP_IPXE_BUILDSTRING, $buildString);
+ }
+
+ public static function setIPxeVersionCallback(array $task): void
+ {
+ if (!Taskmanager::isFinished($task) || Taskmanager::isFailed($task) || empty($task['data']['ref']))
+ return;
+ Property::set(IPxeBuilder::PROP_IPXE_HASH, $task['data']['ref']);
+ }
+} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
index f87d15c2..3ffecba1 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
@@ -3,84 +3,71 @@
class IPxeMenu
{
+ /**
+ * @var int ID of this menu, from DB
+ */
protected $menuid;
- protected $timeoutMs;
- protected $title;
- protected $defaultEntryId;
+ /**
+ * @var int 0 = disabled, otherwise, launch default option after timeout
+ */
+ public $timeoutMs;
+ /**
+ * @var string title to display above menu
+ */
+ public $title;
+ /**
+ * @var int menu entry id from DB
+ */
+ public $defaultEntryId;
/**
* @var MenuEntry[]
*/
- protected $items = [];
+ public $items = [];
- public function __construct($menu)
+ public static function get(int $menuId, bool $emptyFallback = false): ?IPxeMenu
+ {
+ $menu = Database::queryFirst("SELECT menuid, timeoutms, title, defaultentryid FROM serversetup_menu
+ WHERE menuid = :menuid LIMIT 1", ['menuid' => $menuId]);
+ if ($menu !== false)
+ return new IPxeMenu($menu);
+ if (!$emptyFallback)
+ return null;
+ return new EmptyIPxeMenu();
+ }
+
+ /**
+ * IPxeMenu constructor.
+ *
+ * @param array $menu array for according menu row
+ */
+ public function __construct(array $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.refmenuid, e.hotkey, e.title, e.hidden, e.sortval, e.md5pass,
- b.module, b.data AS bootentry
+ $this->title = (string)$menu['title'];
+ $defaultEntryId = $menu['defaultentryid'];
+ $res = Database::simpleQuery("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title,
+ e.hidden, e.sortval, e.md5pass, b.module, b.data AS bootentry, b.title AS betitle
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)) {
+ foreach ($res as $row) {
$this->items[] = new MenuEntry($row);
}
// Make sure we have a default entry if the menu isn't empty
- if ($this->defaultEntryId === null && !empty($this->items)) {
- $this->defaultEntryId = $this->items[0]->menuEntryId();
- }
- }
-
- 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}";
+ if ($defaultEntryId === null && !empty($this->items)) {
+ $defaultEntryId = $this->items[0]->menuEntryId();
}
- $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;
+ $this->defaultEntryId = (int)$defaultEntryId;
}
- public function title()
+ public function title(): string
{
return $this->title;
}
- public function timeoutMs()
+ public function timeoutMs(): int
{
return $this->timeoutMs;
}
@@ -88,52 +75,89 @@ class IPxeMenu
/**
* @return int Number of items in this menu
*/
- public function itemCount()
+ public function itemCount(): int
{
return count($this->items);
}
/**
- * @return string|false Return script label of default entry, false if not set
+ * @return MenuEntry|null Return preselected menu entry
*/
- public function getDefaultScriptLabel()
+ public function defaultEntry(): ?MenuEntry
{
- if ($this->defaultEntryId !== null)
- return "m_{$this->menuid}_{$this->defaultEntryId}";
- return false;
+ foreach ($this->items as $item) {
+ if ($item->menuEntryId() === $this->defaultEntryId)
+ return $item;
+ }
+ return null;
+ }
+
+ private function maybeOverrideDefault(string $uuid)
+ {
+ $e = $this->defaultEntry();
+ // Shortcut - is already bwlp and timeout is reasonable (1-15s), do nothing
+ $defIsMl = $e !== null && substr($e->internalId(), 0, 3) === 'ml-';
+ $timeoutOk = $this->timeoutMs > 0 && $this->timeoutMs <= 15000;
+ if ($timeoutOk && $defIsMl)
+ return;
+ // No runmode module anyways
+ if (!Module::isAvailable('runmode'))
+ return;
+ $rm = RunMode::getRunMode($uuid);
+ // No runmode for this client, cannot be PVSmgr
+ if ($rm === false)
+ return;
+ // Is not pvsmgr
+ if ($rm['module'] !== 'roomplanner')
+ return;
+ // See if it's a dedicated station, if so make sure it boots into bwLehrpool
+ $data = json_decode($rm['modedata'], true);
+ if ($data['dedicatedmgr'] ?? false) {
+ if (!$defIsMl) {
+ $this->overrideDefaultToMinilinux();
+ }
+ if (!$timeoutOk) {
+ $this->timeoutMs = 5000;
+ }
+ }
}
/**
- * @return MenuEntry|null Return preselected menu entry
+ * Patch the menu to make sure bwLehrpool/"MiniLinux" is the default
+ * boot option, and set timeout to something reasonable. This is used
+ * for dedicated PVS managers, as they might not have a keyboard
+ * connected.
*/
- public function defaultEntry()
+ private function overrideDefaultToMinilinux()
{
foreach ($this->items as $item) {
- if ($item->menuEntryId() == $this->defaultEntryId)
- return $item;
+ if (substr($item->internalId(), 0, 3) === 'ml-') {
+ $this->defaultEntryId = $item->menuEntryId();
+ return;
+ }
}
- return null;
}
/*
*
*/
- public static function forLocation($locationId)
+ public static function forLocation(int $locationId): IPxeMenu
{
$chain = null;
if (Module::isAvailable('locations')) {
$chain = Location::getLocationRootChain($locationId);
}
if (!empty($chain)) {
- $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title, IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid
- FROM serversetup_menu m
- INNER JOIN serversetup_menu_location ml USING (menuid)
- WHERE ml.locationid IN (:chain)", ['chain' => $chain]);
+ $res = Database::simpleQuery("SELECT m.menuid, m.timeoutms, m.title,
+ IFNULL(ml.defaultentryid, m.defaultentryid) AS defaultentryid, ml.locationid
+ FROM serversetup_menu m
+ INNER JOIN serversetup_menu_location ml USING (menuid)
+ WHERE ml.locationid IN (:chain)", ['chain' => $chain]);
if ($res->rowCount() > 0) {
// Make the location id key, preserving order (closest location is first)
$chain = array_flip($chain);
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
// Overwrite the value (numeric ascending values, useless) with menu array of according location
$chain[(int)$row['locationid']] = $row;
}
@@ -156,13 +180,19 @@ class IPxeMenu
return new IPxeMenu($menu);
}
- public static function forClient($ip, $uuid)
+ public static function forClient(string $ip, ?string $uuid): IPxeMenu
{
$locationId = 0;
if (Module::isAvailable('locations')) {
$locationId = Location::getFromIpAndUuid($ip, $uuid);
}
- return self::forLocation($locationId);
+ $menu = self::forLocation($locationId);
+ if ($uuid !== null) {
+ // Super specialcase hackery: If this is a dedicated PVS, force the default to
+ // be bwlp/"minilinux"
+ $menu->maybeOverrideDefault($uuid);
+ }
+ return $menu;
}
}
@@ -170,11 +200,14 @@ class IPxeMenu
class EmptyIPxeMenu extends IPxeMenu
{
- /** @noinspection PhpMissingParentConstructorInspection */
public function __construct()
{
- $this->title = 'No menu defined';
- $this->menuid = -1;
+ parent::__construct([
+ 'menuid' => -1,
+ 'timeoutms' => 120,
+ 'defaultentryid' => null,
+ 'title' => 'No menu defined',
+ ]);
$this->items[] = new MenuEntry([
'title' => 'Please create a menu in Server-Setup first'
]);
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
index 4203f931..4d1a56c7 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php
@@ -7,38 +7,49 @@ class Localboot
const BOOT_METHODS = [
'PCBIOS' => [
- 'EXIT' => 'exit 1',
+ 'EXIT' => 'set slx_exit 1 ||
+exit 1',
'COMBOOT' => 'set netX/209:string localboot.cfg ||
set netX/210:string http://${serverip}/tftp/sl-bios/ ||
chain -ar /tftp/sl-bios/lpxelinux.0',
'SANBOOT' => 'sanboot --no-describe',
],
'EFI' => [
- 'EXIT' => 'exit 1',
- 'COMBOOT' => 'set netX/209:string localboot.cfg ||
-set netX/210:string http://${serverip}/tftp/sl-efi64/ ||
-chain -ar /tftp/sl-efi64/syslinux.efi',
+ 'EXIT' => 'set slx_exit 1 ||
+exit 1',
+ 'SANBOOT' => 'imgfree ||
+console ||
+set filename \EFI\Boot\bootx64.efi ||
+set i:int32 0 ||
+:blubber
+sanboot --no-describe --drive ${i} --filename ${filename} ||
+inc i
+iseq ${i} 10 || goto blubber',
+ 'GRUB' => 'chain /tftp/grub-boot.img',
],
];
- public static function getDefault()
+ /**
+ * @return array{PCBIOS: string, EFI: string}
+ */
+ public static function getDefault(): array
{
- $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,EXIT'));
+ $ret = explode(',', Property::get(self::PROPERTY_KEY, 'SANBOOT,GRUB'));
if (empty($ret)) {
- $ret = ['SANBOOT', 'EXIT'];
+ $ret = ['SANBOOT', 'GRUB'];
} elseif (count($ret) < 2) {
- $ret[] = 'EXIT';
+ $ret[] = 'SANBOOT';
}
- if (null === self::BOOT_METHODS['PCBIOS'][$ret[0]]) {
+ if (!isset(self::BOOT_METHODS['PCBIOS'][$ret[0]])) {
$ret[0] = 'SANBOOT';
}
- if (null === self::BOOT_METHODS['EFI'][$ret[1]]) {
- $ret[1] = 'EXIT';
+ if (!isset(self::BOOT_METHODS['EFI'][$ret[1]])) {
+ $ret[1] = 'GRUB';
}
return ['PCBIOS' => $ret[0], 'EFI' => $ret[1]];
}
- public static function setDefault($pcbios, $efi)
+ public static function setDefault(string $pcbios, string $efi)
{
Property::set(self::PROPERTY_KEY, "$pcbios,$efi");
}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
index a65e9f98..da94a16b 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
@@ -5,114 +5,98 @@ class MenuEntry
/**
* @var int id of entry, used for pw
*/
- private $menuentryid;
+ public $menuentryid;
/**
* @var false|string key code as expected by iPXE
*/
- private $hotkey;
+ public $hotkey;
/**
* @var string
*/
- private $title;
+ public $title;
/**
* @var bool
*/
- private $hidden;
+ public $hidden;
/**
* @var bool
*/
- private $gap;
+ public $gap;
/**
* @var int
*/
- private $sortval;
+ public $sortval;
/**
- * @var BootEntry
+ * @var ?BootEntry
*/
- private $bootEntry = null;
+ public $bootEntry = null;
- private $md5pass = null;
+ public $plainpass = null;
+
+ public $md5pass = null;
+
+ public static function get(int $menuEntryId): ?MenuEntry
+ {
+ $row = Database::queryFirst("SELECT e.menuentryid, e.entryid, e.refmenuid, e.hotkey, e.title,
+ e.hidden, e.sortval, e.plainpass, e.md5pass, b.module, b.data AS bootentry, b.title AS betitle
+ FROM serversetup_menuentry e
+ LEFT JOIN serversetup_bootentry b USING (entryid)
+ WHERE e.menuentryid = :id", ['id' => $menuEntryId]);
+ if ($row === false)
+ return null;
+ return new MenuEntry($row);
+ }
/**
* MenuEntry constructor.
*
* @param array $row row from database
*/
- public function __construct($row)
+ public function __construct(array $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['module'], $row['bootentry']);
- } elseif ($row['refmenuid'] !== null) {
- $this->bootEntry = BootEntry::forMenu($row['refmenuid']);
+ if (empty($row['title']) && !empty($row['betitle'])) {
+ $row['title'] = $row['betitle'];
+ }
+ foreach ($row as $key => $value) {
+ if (property_exists($this, $key)) {
+ $this->{$key} = $value;
}
- $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null);
}
+ $this->hotkey = self::getKeyCode($row['hotkey'] ?? '');
+ if (!empty($row['bootentry'])) {
+ $this->bootEntry = BootEntry::fromJson($row['module'], $row['bootentry']);
+ } elseif (isset($row['refmenuid'])) {
+ $this->bootEntry = BootEntry::forMenu($row['refmenuid']);
+ }
+ $this->gap = (array_key_exists('entryid', $row) && $row['entryid'] === null && $row['refmenuid'] === null);
settype($this->hidden, 'bool');
settype($this->gap, 'bool');
settype($this->sortval, 'int');
settype($this->menuentryid, 'int');
}
- public function getMenuItemScript($lblPrefix, $requestedDefaultId, $mode, $slxExtensions)
+ public function getBootEntryScript(ScriptBuilderBase $builder): string
{
- if ($this->bootEntry !== null && !$this->bootEntry->supportsMode($mode))
+ if ($this->bootEntry === null)
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";
+ return $this->bootEntry->toScript($builder);
}
- public function getBootEntryScript($lblPrefix, $failLabel, $mode)
+ public function menuEntryId(): int
{
- 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);
+ return $this->menuentryid;
}
- public function menuEntryId()
+ public function title(): string
{
- return $this->menuentryid;
+ return $this->title;
}
- public function title()
+ public function internalId(): string
{
- return $this->title;
+ if ($this->bootEntry === null)
+ return '';
+ return $this->bootEntry->internalId();
}
/*
@@ -154,7 +138,7 @@ class MenuEntry
*
* @return string[] list of known key names
*/
- public static function getKeyList()
+ public static function getKeyList(): array
{
return array_keys(self::getKeyArray());
}
@@ -163,10 +147,9 @@ class MenuEntry
* 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)
+ public static function getKeyCode(string $keyName)
{
$data = self::getKeyArray();
if (isset($data[$keyName]))
@@ -178,7 +161,7 @@ class MenuEntry
* @param string $keyName desired key name
* @return string $keyName if it's known, empty string otherwise
*/
- public static function filterKeyName($keyName)
+ public static function filterKeyName(string $keyName): string
{
$data = self::getKeyArray();
if (isset($data[$keyName]))
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php
index ff548c4c..24b099dc 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/pxelinux.inc.php
@@ -7,11 +7,14 @@ 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
+ * @return ?PxeMenu the parsed menu, or null if input is not a PXELinux menu
*/
- public static function parsePxeLinux($input, $isCp437)
+ public static function parsePxeLinux(string $input, bool $isCp437): ?PxeMenu
{
+ if (empty($input))
+ return null;
if ($isCp437) {
$input = iconv('IBM437', 'UTF8//TRANSLIT//IGNORE', $input);
}
@@ -76,12 +79,14 @@ class PxeLinux
}
$section->helpText = $text;
} elseif (self::handleKeyword($key, $val, $sectionPropMap, $section)) {
- continue;
+ //continue;
}
}
if ($section !== null) {
$menu->sections[] = $section;
}
+ if (empty($menu->sections))
+ return null; // Probably not a PXE menu but random text?
foreach ($menu->sections as $section) {
$section->mangle();
}
@@ -93,13 +98,14 @@ class PxeLinux
* 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
+ * @param PxeMenu|PxeSection $object 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 handleKeyword(string $key, string $val, array $map, $object): bool
{
if (!isset($map[$key]))
return false;
@@ -122,161 +128,3 @@ class PxeLinux
}
-/**
- * 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 = [];
- /**
- * @var string The DEFAULT entry of the menu. Usually refers either to a
- * LABEL, or a loadable module (like vesamenu.c32)
- */
- public $default;
-
- /**
- * Check if any of the sections has the given label.
- */
- public function hasLabel($label)
- {
- foreach ($this->sections as $section) {
- if ($section->label === $label)
- return true;
- }
- return false;
- }
-
-}
-
-/**
- * Class representing a parsed pxelinux menu entry. Members
- * will be set to their annotated type if present or
- * be null otherwise, except for present-only boolean
- * options, which will default to false.
- */
-class PxeSection
-{
-
- /**
- * @var string label used internally in PXEMENU definition to address this entry
- */
- public $label;
- /**
- * @var string MENU LABEL of PXEMENU - title of entry displayed to the user
- */
- public $title;
- /**
- * @var int Number of spaces to prefix the title with
- */
- public $indent;
- /**
- * @var string help text to display when the entry is highlighted
- */
- public $helpText;
- /**
- * @var string Kernel to load
- */
- public $kernel;
- /**
- * @var string|string[] initrd to load for the kernel.
- * If mangle() has been called this will be an array,
- * otherwise it's a comma separated list.
- */
- public $initrd;
- /**
- * @var string command line options to pass to the kernel
- */
- public $append;
- /**
- * @var int IPAPPEND from PXEMENU. Bitmask of valid options 1 and 2.
- */
- public $ipAppend;
- /**
- * @var string Password protecting the entry. This is most likely in crypted form.
- */
- public $passwd;
- /**
- * @var bool whether this section is marked as default (booted after timeout)
- */
- public $isDefault = false;
- /**
- * @var bool Menu entry is not visible (can only be triggered by timeout)
- */
- public $isHidden = false;
- /**
- * @var bool Disable this entry, making it unselectable
- */
- public $isDisabled = false;
- /**
- * @var int|false Value of the LOCALBOOT field, false if not set
- */
- public $localBoot = false;
- /**
- * @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-ipxe/inc/pxemenu.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php
new file mode 100644
index 00000000..7be57ef1
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/pxemenu.inc.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * 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 = [];
+ /**
+ * @var string The DEFAULT entry of the menu. Usually refers either to a
+ * LABEL, or a loadable module (like vesamenu.c32)
+ */
+ public $default;
+
+ /**
+ * Check if any of the sections has the given label.
+ */
+ public function hasLabel(string $label): bool
+ {
+ foreach ($this->sections as $section) {
+ if ($section->label === $label)
+ return true;
+ }
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php
new file mode 100644
index 00000000..2d9cd6ab
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/pxesection.inc.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * 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 encrypted 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|false Value of the LOCALBOOT field, false if not set
+ */
+ public $localBoot = false;
+ /**
+ * @var string hotkey to trigger item. Only valid after calling mangle()
+ */
+ public $hotkey;
+
+ public function __construct(?string $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 = [];
+ }
+ }
+
+ /**
+ * Does this appear to be an entry that triggers localboot?
+ */
+ public function isLocalboot(): bool
+ {
+ return $this->localBoot !== false || preg_match('/chain\.c32$/i', $this->kernel);
+ }
+
+ /**
+ * Is this (most likely) a separating entry only that cannot be selected?
+ */
+ public function isTextOnly(): bool
+ {
+ return ($this->label === null || empty($this->kernel)) && !$this->isHidden && !empty($this->title);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php
new file mode 100644
index 00000000..9cd07388
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php
@@ -0,0 +1,102 @@
+<?php
+
+abstract class ScriptBuilderBase
+{
+
+ private $lblId = 0;
+
+ protected $serverIp;
+
+ protected $platform = '';
+
+ /** @var string */
+ protected $clientIp;
+
+ /** @var ?string */
+ protected $uuid;
+
+ /**
+ * @var bool Running iPXE has slx-extensions
+ */
+ protected $hasExtension = false;
+
+ public function hasExtensions(): bool
+ {
+ return $this->hasExtension;
+ }
+
+ public function platform(): string
+ {
+ return $this->platform;
+ }
+
+ public function uuid(): ?string
+ {
+ return $this->uuid;
+ }
+
+ public function clientIp(): string
+ {
+ return $this->clientIp;
+ }
+
+ public function getLabel(): string
+ {
+ return 'b' . mt_rand(100, 999) . 'x' . (++$this->lblId);
+ }
+
+ public function __construct(?string $platform = null, ?string $serverIp = null, ?bool $slxExtensions = null)
+ {
+ $this->clientIp = (string)$_SERVER['REMOTE_ADDR'];
+ if (substr($this->clientIp, 0, 7) === '::ffff:') {
+ $this->clientIp = substr($this->clientIp, 7);
+ }
+ $this->serverIp = $serverIp ?? $_SERVER['SERVER_ADDR'] ?? Property::getServerIp();
+ $this->platform = $platform ?? Request::any('platform', null, 'string');
+ if ($this->platform !== null) {
+ $this->platform = strtoupper($this->platform);
+ }
+ if ($this->platform !== 'EFI' && $this->platform !== 'PCBIOS') {
+ $this->platform = '';
+ }
+ $this->hasExtension = $slxExtensions ?? (bool)Request::any('slx-extensions', false, 'int');
+ $uuid = Request::any('uuid', null, 'string');
+ if ($uuid !== null
+ && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $uuid)) {
+ $this->uuid = (string)$uuid;
+ }
+ }
+
+ /**
+ * Output given string (script) to client, in a suitable encoding, headers, etc.
+ */
+ public abstract function output(string $string): void;
+
+ public abstract function bootstrapLive();
+
+ public abstract function getMenu(IPxeMenu $menu, bool $bootstrap);
+
+ /**
+ * @param MenuEntry|null $menuEntry The according menu entry, or null if invalid.
+ * @param bool $honorPassword Whether we should generate a password dialog if protected, or skip
+ * @return string generated script/code/...
+ */
+ public abstract function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string;
+
+ /**
+ * @param BootEntry|null|false $bootEntry
+ */
+ public abstract function getBootEntry(?BootEntry $entry): string;
+
+ public abstract function getSpecial(string $special);
+
+ public abstract function menuToScript(IPxeMenu $menu): string;
+
+ /**
+ * Pass EITHER only $agnostic, OR $bios and/or $efi
+ * If $agnostic is given, it should be used unconditionally,
+ * and $bios/$efi should be ignored.
+ */
+ public abstract function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string;
+
+} \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php
new file mode 100644
index 00000000..d6b542ec
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php
@@ -0,0 +1,97 @@
+<?php
+
+class ScriptBuilderBash extends ScriptBuilderBase
+{
+
+ public function output(string $string): void
+ {
+ echo $string;
+ }
+
+ public function bootstrapLive(): bool { return false; }
+
+ public function getMenu(IPxeMenu $menu, bool $bootstrap): string
+ {
+ return $this->menuToScript($menu);
+ }
+
+ public function getBootEntry(?BootEntry $entry): string
+ {
+ if ($entry === null) {
+ return "echo 'Invalid boot entry id'\nread -n1 -r _\n";
+ }
+ return $entry->toScript($this);
+ }
+
+ public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string
+ {
+ if ($entry === null)
+ return "echo 'Invalid menu entry id - press any key to continue'\nread -n1 -r _\n";
+ return $entry->getBootEntryScript($this);
+ }
+
+ public function getSpecial(string $special): string
+ {
+ return ''; // We can't really do localboot here I guess
+ }
+
+ public function menuToScript(IPxeMenu $menu): string
+ {
+ $output = "declare -A items_name items_gap hotkey_item\ndeclare menu_default menu_timeout menu_title\n";
+ foreach ($menu->items as $entry) {
+ $id = $entry->menuentryid;
+ if ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform)))
+ continue;
+ if (!$entry->hidden) {
+ $output .= 'items_name[' . $id . ']=' . $this->bashString($entry->title) . "\n";
+ if ($entry->gap) {
+ $output .= 'items_gap[' . $id . "]=1\n";
+ }
+ }
+ if ($entry->hotkey !== false) {
+ $output .= 'hotkey_item[' . $entry->hotkey . ']=' . $id . "\n";
+ }
+ if ($id == $menu->defaultEntryId) {
+ $output .= "menu_default={$id}\n";
+ }
+ }
+ return $output . "menu_timeout=" . $menu->timeoutMs
+ . "\nmenu_title=" . $this->bashString($menu->title) . "\n";
+ }
+
+ public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string
+ {
+ if ($agnostic !== null)
+ return $this->execDataToScriptInternal($agnostic);
+ if ($bios !== null && $this->platform === BootEntry::BIOS)
+ return $this->execDataToScriptInternal($bios);
+ if ($efi !== null && $this->platform === BootEntry::EFI)
+ return $this->execDataToScriptInternal($efi);
+ return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData());
+ }
+
+ private function execDataToScriptInternal(ExecData $entry) : string
+ {
+ $entry->sanitize();
+ $script = "declare -a initrd\ndeclare kernel kcl\n";
+ if (!empty($entry->initRd)) {
+ foreach ($entry->initRd as $initrd) {
+ if (empty($initrd))
+ continue;
+ $script .= 'initrd+=( ' . $this->bashString($initrd) . " )\n";
+ }
+ }
+ $script .= 'kernel=' . $this->bashString($entry->executable) . "\n";
+ $script .= 'kcl="' . str_replace('"', '"\\""', $entry->commandLine) . "\"\n"; // Allow expansion
+ return $script;
+ }
+
+ private function bashString(string $string): string
+ {
+ if (strpos($string, "'") === false) {
+ return "'$string'";
+ }
+ return "'" . str_replace("'", "'\\''", $string) . "'";
+ }
+
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php
new file mode 100644
index 00000000..9dce5214
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php
@@ -0,0 +1,330 @@
+<?php
+
+class ScriptBuilderGrub extends ScriptBuilderBase
+{
+
+ /** @var bool */
+ private $confCodeEmited = false;
+
+ public function __construct(?string $platform = null, ?string $serverIp = null)
+ {
+ if (empty($platform)) {
+ $platform = Request::any('platform', null, 'string');
+ if ($platform === 'pc' || stripos($platform, 'bios') !== false) {
+ $platform = 'PCBIOS';
+ }
+ }
+ parent::__construct($platform, $serverIp, false);
+ }
+
+ private function getConfCode(): string
+ {
+ if ($this->confCodeEmited)
+ return '';
+ $this->confCodeEmited = true;
+ $str = '
+if ! [ "$uuid" ] ; then
+ smbios --type 1 --get-uuid 8 --set uuid
+fi
+set serverip="' . $this->getLocalIp() . '"
+';
+ foreach (['mac', 'ip', 'domain', 'hostname'] as $var) {
+ $str .= <<<EOF
+if ! [ "\$$var" ] ; then
+ set $var="\$net_default_$var"
+fi
+if ! [ "\$$var" ] ; then
+ set $var="\$net_efinet0_dhcp_$var"
+fi
+
+EOF;
+
+ }
+ return $str;
+ }
+
+ private function getLocalIp(): string
+ {
+ if (isset($_SERVER['SCRIPT_URI']) && preg_match('#^\w+://([^/]+)#', $_SERVER['SCRIPT_URI'], $out)) {
+ $host = $out[1];
+ } elseif (isset($_SERVER['SERVER_NAME'])) {
+ $host = $_SERVER['SERVER_NAME'];
+ } elseif (isset($_SERVER['SERVER_ADDR'])) {
+ $host = $_SERVER['SERVER_ADDR'];
+ } else {
+ $host = $this->serverIp;
+ }
+ return $host;
+ }
+
+ private function getGrubBase(): string
+ {
+ return '(http,' . $this->getLocalIp() . ')';
+ }
+
+ private function getUrlBase(): string
+ {
+ $host = $this->getGrubBase();
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $url = parse_url($_SERVER['REQUEST_URI']);
+ $path = $url['path'];
+ } else {
+ // Static fallback
+ $path = '/boot/ipxe';
+ }
+ return $host . $path;
+
+ }
+
+ private function getUrlFull(?string $key = null, ?string $value = null): string
+ {
+ $url = parse_url($_SERVER['REQUEST_URI']);
+ $urlbase = $this->getUrlBase();
+ if (empty($url['query'])) {
+ $fromQuery = [];
+ } else {
+ parse_str($url['query'], $fromQuery);
+ foreach ($fromQuery as &$v) {
+ $v = urlencode($v);
+ }
+ unset($v);
+ }
+ unset($fromQuery['entryid'], $fromQuery['special'], $fromQuery['redir']);
+ if ($key !== null) {
+ $fromQuery[$key] = $value;
+ }
+ $required = [
+ 'type' => 'grub',
+ 'uuid' => '$uuid',
+ 'mac' => '$mac',
+ 'platform' => '$grub_platform',
+ ];
+ $fullQuery = '?';
+ foreach ($required + $fromQuery as $k => $v) { // Loop instead of http_build_query since we don't want escaping for the varnames!
+ $fullQuery .= $k . '=' . $v . '&';
+ }
+ return $urlbase . $fullQuery;
+ }
+
+ /**
+ * Redirect to same URL, but add our extended params
+ */
+ private function redirect(string $key = null, string $value = null): string
+ {
+ // Redirect to self with added parameters
+ $urlfull = $this->getUrlFull($key, $value);
+ return $this->getConfCode() . <<<HERE
+
+set self="${urlfull}"
+echo "Chaining to \$self..."
+configfile \${self}redir=1
+
+HERE;
+ }
+
+ /**
+ * Called when we handle a real client request, and don't just generate static data
+ * for whatever use-case that might have. In the latter case, it wouldn't make much sense
+ * to generate a redirect code snippet.
+ *
+ * @return string
+ */
+ public function bootstrapLive()
+ {
+ // Check if required arguments are given; if not, spit out according script and chain to self
+ if ($this->uuid === null || $this->platform === '') {
+ // REQUIRED so we can hide incompatible entries
+ // but avoid redirect cycle
+ if (Request::any('redir', '', 'string') === '') {
+ return $this->redirect();
+ }
+ }
+ return false;
+ }
+
+ public function getBootEntry(?BootEntry $entry): string
+ {
+ if ($entry === null) {
+ return "echo Invalid boot entry id\nsleep --interruptible --verbose 10\n";
+ }
+ return $entry->toScript($this);
+ }
+
+ public function getMenu(IPxeMenu $menu, bool $bootstrap): string
+ {
+ $base = $this->getUrlFull();
+ return $this->getConfCode()
+ . "set self=\"{$base}\"\n"
+ . $this->menuToScript($menu);
+ }
+
+ public function menuToScript(IPxeMenu $menu): string
+ {
+ if ($menu->defaultEntryId === null) {
+ $output = <<<EOF
+set timeout=0
+
+EOF;
+
+ } else {
+ $secs = (int)($menu->timeoutMs / 1000);
+ $output = <<<EOF
+set timeout={$secs}
+set default="id-{$menu->defaultEntryId}"
+
+EOF;
+ }
+ $output .= $this->getConfCode();
+ foreach ($menu->items as $item) {
+ $output .= $this->getMenuItemScript($item);
+ }
+ return $output;
+ }
+
+ private function getMenuItemScript(MenuEntry $entry): string
+ {
+ $str = "menuentry '" . str_replace("'", '', $entry->title) . "' --id 'id-" . $entry->menuentryid . "' {\n";
+ if ($entry->gap) {
+ $str .= "true\n"; // AFAICT, not possible in GRUB
+ } elseif ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform))) {
+ $str .= "echo Type mismatch\n";
+ } elseif ($entry->hidden && $entry->hotkey === false) {
+ $str .= "echo Hidden entries without hotkey are illegal\n"; // Hidden entries without hotkey are illegal
+ } else {
+ if ($entry->hotkey !== false) {
+ // Not supported by grub...
+ }
+ if ($entry->bootEntry instanceof MenuBootEntry) {
+ // Link
+ $str .= "configfile \${self}entryid={$entry->menuentryid}\n";
+ } else {
+ // Embed directly
+ // TODO: Password. Use read etc.; might need hashsum.mod, in that case, don't embed entry directly but use configfile...
+ $str .= $this->getMenuEntry($entry, true);
+ }
+ }
+ return $str . "}\n";
+ }
+
+ public function getSpecial(string $special): string
+ {
+ if ($special === 'localboot') {
+ // Sync this with setup-scripts/grub_localboot occasionally...
+ $output = <<<'EOF'
+insmod chain
+if [ "$grub_platform" = "pc" ] ; then
+ chainloader (hd0)+1
+ chainloader (hd1)+1
+ chainloader (hd2)+1
+fi
+insmod fat
+insmod part_gpt
+echo "Scanning, first pass..."
+for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \
+ (*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do
+ regexp --set=1:efi_device '^\((.*)\)/' "${efi}"
+done
+
+echo "Scanning, second pass..."
+for efi in (*,gpt*)/efi/grub/grubx64.efi (*,gpt*)/efi/boot/bootx64.efi (*,gpt*)/efi/*/*/bootmgfw.efi (*,gpt*)/efi/*/*.efi \
+ (*,msdos*)/efi/grub/grubx64.efi (*,msdos*)/efi/boot/bootx64.efi (*,msdos*)/efi/*/*/bootmgfw.efi (*,msdos*)/efi/*/*.efi; do
+ if [ -e "${efi}" ]; then
+ #regexp --set=1:efi_device '^\((.*)\)/' "${efi}"
+ regexp --set=1:root '^(\(.*\))/' "${efi}"
+ regexp --set=1:efi_path '^\(.*\)(/.*)$' "${efi}"
+ echo " >> Found operating system! <<"
+ echo " Path: '${efi}' on '${root}'"
+ echo " Fallback '${efi_path}'"
+ chainloader "${efi}"
+ boot
+ echo " That failed..."
+ fi
+done
+
+echo "No EFI known OS found. Exiting."
+exit
+EOF;
+
+ } else {
+ $output = <<<EOF
+echo "Unknown special command: $special"
+sleep --interruptible --verbose 10
+EOF;
+ }
+ return $output;
+ }
+
+ public function output(string $string): void
+ {
+ Header('Content-Type: text/plain; charset=UTF-8');
+ echo $string;
+ }
+
+ public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string
+ {
+ if ($entry === null)
+ return "echo Invalid menu entry id\nsleep --interruptible --verbose 10\n";
+ // TODO: Check for password
+ if ($honorPassword && !empty($entry->md5pass)) {
+ return "echo TODO: Implement password check...\nsleep --interruptible --verbose 10\n";
+ }
+ $meid = $entry->menuEntryId();
+ $output = $this->getConfCode() . "set menuentryid=$meid\n";
+ // Output actual entry
+ $output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this));
+ return $output;
+ }
+
+ public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string
+ {
+ if ($agnostic !== null)
+ return $this->execDataToScriptInternal($agnostic);
+
+ if ($efi !== null && $this->platform === BootEntry::EFI)
+ return $this->execDataToScriptInternal($efi);
+ // Unknown or not EFI, should be BIOS at this point
+ return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData());
+ }
+
+ private function execDataToScriptInternal(ExecData $entry): string
+ {
+ $entry->sanitize();
+ $base = $this->getGrubBase();
+ $script = '';
+ // Overriding dhcpOpts probably not possible/necessary
+ $initrds = [];
+ if (!empty($entry->initRd)) {
+ foreach ($entry->initRd as $initrd) {
+ if (empty($initrd))
+ continue;
+ $initrds[] = $this->combineUrl($base, $initrd);
+ }
+ }
+ $file = $this->combineUrl($base, $entry->executable);
+ $script .= "linux $file {$entry->commandLine} slx.ipxe.id=\${menuentryid}\n";
+ if (!empty($initrds)) {
+ $script .= "initrd " . implode(' ', $initrds) . "\n";
+ }
+ return $script;
+ }
+
+ private function combineUrl(string $base, string $path): string
+ {
+ $url = parse_url($path);
+ if (isset($url['host'])) {
+ $scheme = $url['scheme'] ?? 'http';
+ $host = $url['host'];
+ $base = "($scheme,$host)";
+ $path = $url['path'] ?? '/';
+ if (isset($url['query'])) {
+ $path .= '?' . $url['query'];
+ }
+ } else {
+ if ($path[0] !== '/') {
+ $path = '/' . $path;
+ }
+ }
+ return $base . $path;
+ }
+
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
new file mode 100644
index 00000000..9421684f
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
@@ -0,0 +1,478 @@
+<?php
+
+class ScriptBuilderIpxe extends ScriptBuilderBase
+{
+
+ private function getUrlBase(): string
+ {
+ if (isset($_SERVER['REQUEST_URI'])) {
+ $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://' . $this->serverIp;
+ }
+ return $urlbase . $url['path'];
+ }
+ // Static fallback
+ return 'http://' . $this->serverIp . '/boot/ipxe';
+
+ }
+
+ private function getUrlFull(?bool &$hasExt = null, ?string $key = null, ?string $value = null): string
+ {
+ $url = parse_url($_SERVER['REQUEST_URI']);
+ $urlbase = $this->getUrlBase();
+ if (empty($url['query'])) {
+ $fromQuery = [];
+ } else {
+ parse_str($url['query'], $fromQuery);
+ foreach ($fromQuery as &$v) {
+ $v = urlencode($v);
+ }
+ unset($v);
+ }
+ unset($fromQuery['entryid'], $fromQuery['special']);
+ if ($key !== null) {
+ $fromQuery[$key] = $value;
+ }
+ $hasExt = isset($fromQuery['slx-extensions']);
+ $required = [
+ 'uuid' => '${uuid}',
+ 'mac' => '${mac}',
+ 'manuf' => '${manufacturer:uristring}',
+ 'product' => '${product:uristring}',
+ 'platform' => '${platform:uristring}',
+ ];
+ $fullQuery = '?';
+ foreach ($required + $fromQuery as $k => $v) { // Loop instead of http_build_query since we don't want escaping for the varnames!
+ $fullQuery .= $k . '=' . $v . '&';
+ }
+ return $urlbase . $fullQuery;
+ }
+
+ /**
+ * Redirect to same URL, but add our extended params
+ */
+ private function redirect(string $key = null, string $value = null): string
+ {
+ // Redirect to self with added parameters
+ $urlfull = $this->getUrlFull($hasExt, $key, $value);
+ if ($hasExt) {
+ $output = "#!ipxe\nset self {$urlfull} ||\n";
+ } else {
+ $output = <<<HERE
+#!ipxe
+set slxtest:string something ||
+iseq \${slxtest:md5} \${} && set slxext 0 || set slxext 1 ||
+clear slxtest ||
+set self {$urlfull}slx-extensions=\${slxext} ||
+
+HERE;
+ }
+ $output .= <<<HERE
+:retry
+echo Chaining to \${self}
+chain -ar \${self} ||
+echo Chaining to self failed with \${errno}, retrying in a bit...
+sleep 5
+goto retry
+
+HERE;
+ return $output;
+ }
+
+ /**
+ * Called when we handle a real client request, and don't just generate static data
+ * for whatever use-case that might have. In the latter case, it wouldn't make much sense
+ * to generate a redirect code snippet.
+ * @return string
+ */
+ public function bootstrapLive()
+ {
+ // Check if required arguments are given; if not, spit out according script and chain to self
+ if ($this->uuid === false || $this->platform === '') {
+ // REQUIRED so we can hide incompatible entries
+ return $this->redirect();
+ }
+ return false;
+ }
+
+ public function getBootEntry(?BootEntry $entry): string
+ {
+ if ($entry === null) {
+ return "#!ipxe\nprompt --timeout 5000 Invalid boot entry id\n";
+ }
+ return $entry->toScript($this);
+ }
+
+ public function getMenu(IPxeMenu $menu, bool $bootstrap): string
+ {
+ if ($bootstrap) {
+ return "#!ipxe\nimgfree ||\n" . $this->menuToScript($menu);
+ }
+ $base = $this->getUrlFull();
+ return "#!ipxe\nset self {$base} ||\n" . $this->menuToScript($menu);
+ }
+
+ public function menuToScript(IPxeMenu $menu): string
+ {
+ if ($this->hasExtension) {
+ $slxConsoleUpdate = '--update';
+ } else {
+ $slxConsoleUpdate = '';
+ }
+
+ $output = <<<HERE
+:start
+
+imgstat bg-menu || imgfetch --name bg-menu /tftp/pxe-menu.png ||
+console --left 55 --top 88 --right 63 --bottom 64 --keep --picture bg-menu ||
+
+colour --rgb 0xffffff 7 ||
+colour --rgb 0xcccccc 5 ||
+colour --rgb 0x000000 0 ||
+colour --rgb 0xdddddd 6 ||
+cpair --foreground 0 --background 4 1 ||
+cpair --foreground 0 --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 ||
+
+menu -- {$menu->title} || prompt --timeout 5000 Error creating menu ||
+
+HERE;
+ foreach ($menu->items as $item) {
+ $output .= $this->getMenuItemScript($menu->defaultEntryId, $item);
+ }
+ if ($menu->defaultEntryId === null) {
+ $default = "poweroff || exit 1 ||";
+ } else {
+ $default = "chain -a \${self}&entryid={$menu->defaultEntryId} ||";
+ }
+ $output .= "choose";
+ if ($menu->timeoutMs > 0) {
+ $output .= " --timeout {$menu->timeoutMs}";
+ }
+ $output .= " selection || goto default || goto fail\n";
+ $output .= <<<HERE
+console --left 60 --top 130 --right 67 --bottom 86 $slxConsoleUpdate ||
+set slx_exit \${} ||
+chain -a \${self}&entryid=\${selection} ||
+iseq \${slx_exit} \${} || console ||
+iseq \${slx_exit} \${} || echo Exiting with code \${slx_exit} ||
+iseq \${slx_exit} \${} || exit \${slx_exit}
+goto fail || goto start
+goto \${target} ||
+echo Could not find menu entry in script.
+prompt Press any key to continue.
+goto start
+:default
+$default
+:fail
+prompt Boot failed. Press any key to start.
+goto start
+
+HERE;
+ return $output;
+ }
+
+ private function getMenuItemScript(int $requestedDefaultId, MenuEntry $entry): string
+ {
+ $str = 'item ';
+ if ($entry->gap) {
+ $str .= '--gap -- ';
+ } else {
+ if ($entry->bootEntry === null || (!empty($this->platform) && !$entry->bootEntry->supportsMode($this->platform)))
+ return '';
+ if ($entry->hidden && $this->hasExtension) {
+ if ($entry->hotkey === false)
+ return ''; // Hidden entries without hotkey are illegal
+ $str .= '--hidden ';
+ }
+ if ($entry->hotkey !== false) {
+ $str .= '--key ' . $entry->hotkey . ' ';
+ }
+ if ($entry->menuentryid == $requestedDefaultId) {
+ $str .= '--default ';
+ }
+ $str .= "-- {$entry->menuentryid} ";
+ }
+ if (empty($entry->title)) {
+ $str .= '${}';
+ } else {
+ $str .= $entry->title;
+ }
+ return $str . " || prompt Could not create menu item for {$entry->menuentryid}\n";
+ }
+
+ public function getSpecial(string $special): string
+ {
+ if ($special === 'localboot') {
+ // Get preferred localboot method, depending on system model
+ // Check if required arguments are given; if not, spit out according script and chain to self
+ // Get platform - EFI or PCBIOS
+ $manuf = Request::any('manuf', false, 'string');
+ $product = Request::any('product', false, 'string');
+ if ($this->uuid === false && $manuf === false && $product === false) {
+ return $this->redirect('special', 'localboot');
+ }
+ $BOOT_METHODS = Localboot::BOOT_METHODS[$this->platform];
+ $localboot = false;
+ $model = false;
+ if ($this->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' => $this->uuid]);
+ if ($row !== false && !empty($row['systemmodel'])) {
+ $model = $row['systemmodel'];
+ }
+ }
+ if ($model === false) {
+ // Otherwise use what iPXE sent us
+ $manuf = $this->modfilt($manuf);
+ $product = $this->modfilt($product);
+ if (!empty($product)) {
+ $model = $product;
+ if (!empty($manuf)) {
+ $model .= " ($manuf)";
+ }
+ $model = Util::ansiToUtf8($model);
+ }
+ }
+ // Query
+ if ($model !== false) {
+ $e = strtolower($this->platform); // We made sure $this->platform is either PCBIOS or EFI, so no injection possible
+ $row = Database::queryFirst("SELECT $e AS 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 = Localboot::getDefault()[$this->platform];
+ if (!isset($BOOT_METHODS[$localboot])) {
+ $localboot = array_keys($BOOT_METHODS)[0];
+ }
+ }
+ // Convert to actual ipxe code
+ $localboot = $BOOT_METHODS[$localboot] ?? 'prompt Localboot not possible';
+ $output = <<<BLA
+imgfree ||
+console ||
+$localboot || goto fail
+
+BLA;
+ //
+ } else {
+ $output = "prompt --timeout 5000 Unknown special command '$special' ||\nchain -ar \${self}\n";
+ }
+ return $output;
+ }
+
+ public function output(string $string): void
+ {
+ // iPXE introduced UTF-8 support at some point in 2022, and now expects all text/script files to be
+ // encoded as such. Since we still offer to use older versions, we need to detect that here and handle
+ // all non-ASCII chars differently.
+ // Use 'ipxe.compile-time' instead of const from IpxeBuilder to avoid pulling in another include
+ if (!preg_match('/Version: (\d{4})-\d{2}-\d{2}\b/', Property::get('ipxe.compile-time'), $out)
+ || (int)$out[1] >= 2022) {
+ Header('Content-Type: text/plain; charset=UTF-8');
+ echo $string;
+ } else {
+ if ($this->platform === 'EFI') {
+ $cs = 'ASCII';
+ } else {
+ $cs = 'IBM437';
+ }
+ Header('Content-Type: text/plain; charset=' . $cs);
+
+ setlocale(LC_ALL, 'de_DE.UTF-8', 'de_DE.utf-8', 'de_DE.utf8', 'de_DE', 'de', 'German', 'ge', 'en_US.UTF-8', 'en_US.utf-8');
+ echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $string);
+ }
+ }
+
+ public function modfilt($str)
+ {
+ if (empty($str) || preg_match('/product\s+name|be\s+filled|unknown|default\s+string|system\s+model|manufacturer/i', $str))
+ return false;
+ return trim(preg_replace('/\s+/', ' ', $str));
+ }
+
+ const PROP_PW_SALT = 'ipxe.salt.';
+
+ private function passwordDialog(MenuEntry $entry): string
+ {
+ if ($this->hasExtension) {
+ $salt = dechex(mt_rand(0x100000, 0xFFFFFF));
+ Property::addToList(self::PROP_PW_SALT . $this->clientIp, $salt, 5);
+ return <<<HERE
+set password \${} ||
+login --nouser ||
+set password \${password:md5}-{$entry->menuentryid}
+set password \${password:md5}$salt
+params
+param pwhash \${password:md5}
+chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail ||
+
+HERE;
+ }
+ return <<<HERE
+set username PASSWORD ONLY ||
+login ||
+params
+param pwplain \${password}
+chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail ||
+
+HERE;
+ }
+
+ public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string
+ {
+ if ($entry === null)
+ return "#!ipxe\nprompt --timeout 10000 Invalid menu entry id\n";
+ $base = $this->getUrlBase();
+ $meid = $entry->menuEntryId();
+ // Make sure legacy variables are set; they might get used
+ $output = <<<HERE
+#!ipxe
+set ipappend1 ip=\${ip}:{$this->serverIp}:\${gateway}:\${netmask}
+set ipappend2 BOOTIF=01-\${mac:hexhyp}
+set serverip {$this->serverIp} ||
+iseq \${idx} \${} && set idx:string X ||
+iseq \${self} \${} && set self {$base}? ||
+set menuentryid $meid ||
+
+HERE;
+ // Check for password
+ if ($honorPassword && !empty($entry->md5pass)) {
+ $pwh = Request::post('pwhash', false, 'string');
+ $pwp = Request::post('pwplain', false, 'string');
+ if ($pwh === false && $pwp === false) {
+ return $output . $this->passwordDialog($entry);
+ }
+ $ok = false;
+ if ($pwh !== false) {
+ $list = Property::getList(self::PROP_PW_SALT . $this->clientIp);
+ foreach ($list as $salt) {
+ if ($pwh === md5($entry->md5pass . $salt)) {
+ $ok = true;
+ break;
+ }
+ }
+ }
+ if (!$ok && $pwp !== false && !empty($entry->plainpass)) {
+ $ok = ($pwp === $entry->plainpass);
+ }
+ if (!$ok) {
+ return $output . "prompt --timeout 10000 Wrong password ||\n";
+ }
+ }
+ // Output actual entry
+ $output .= str_replace('%fail%', 'fail', $entry->getBootEntryScript($this));
+ $output .= <<<HERE
+
+goto end
+:fail
+prompt --timeout 5000 Error launching selected boot entry ||
+:end
+
+HERE;
+ return $output;
+ }
+
+ public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi) : string
+ {
+ if ($agnostic !== null)
+ return $this->execDataToScriptInternal($agnostic) . "\ngoto fail\n";
+
+ if (empty($this->platform)) {
+ // output dynamic code that decides client-side
+ $biosLabel = $this->getLabel();
+ $output = 'iseq ${platform} efi || goto ' . $biosLabel . "\n";
+ // EFI
+ if ($efi !== null) {
+ $output .= $this->execDataToScriptInternal($efi) . "\n";
+ } else {
+ $output .= "echo EFI not supported\n";
+ }
+ $output .= "goto fail\n"
+ . ':' . $biosLabel . "\n";
+ if ($bios !== null) {
+ $output .= $this->execDataToScriptInternal($bios) . "\n";
+ } else {
+ $output .= "echo BIOS not supported\n";
+ }
+ return $output . "goto fail\n";
+ }
+ // static, we know in advance
+ if ($efi !== null && $this->platform === BootEntry::EFI)
+ return $this->execDataToScriptInternal($efi) . "\ngoto fail\n";
+ // Should be BIOS at this point
+ return $this->execDataToScriptInternal($bios ?? $efi ?? new ExecData()) . "\ngoto fail\n";
+ }
+
+ private function execDataToScriptInternal(ExecData $entry) : string
+ {
+ $entry->sanitize();
+ $script = '';
+ if ($entry->resetConsole) {
+ $script .= "console ||\n";
+ }
+ if ($entry->imageFree) {
+ $script .= "imgfree ||\n";
+ }
+ foreach ($entry->dhcpOptions as $opt) {
+ if (empty($opt['value'])) {
+ $val = '${}';
+ } else {
+ if (empty($opt['hex'])) {
+ $val = bin2hex($opt['value']);
+ } else {
+ $val = $opt['value'];
+ }
+ preg_match_all('/[0-9a-f]{2}/', $val, $out);
+ $val = implode(':', $out[0]);
+ }
+ $script .= 'set net${idx}/' . $opt['opt'] . ':hex ' . $val
+ . ' || prompt Cannot override DHCP server option ' . $opt['opt'] . ". Press any key to continue anyways.\n";
+ }
+ $initrds = [];
+ if (!empty($entry->initRd)) {
+ foreach (array_values($entry->initRd) as $i => $initrd) {
+ if (empty($initrd))
+ continue;
+ $script .= "initrd --name initrd$i $initrd || goto fail\n";
+ $initrds[] = "initrd$i";
+ }
+ }
+ $script .= "boot ";
+ if ($entry->autoUnload) {
+ $script .= "-a ";
+ }
+ if ($entry->replace) {
+ $script .= "-r ";
+ }
+ $script .= $entry->executable;
+ if (!empty($initrds)) {
+ foreach ($initrds as $initrd) {
+ $script .= " initrd=$initrd";
+ }
+ }
+ if (!empty($entry->commandLine)) {
+ $script .= ' ' . $entry->commandLine . ' slx.ipxe.id=${menuentryid}';
+ }
+ $script .= " || goto fail\n";
+ if ($entry->resetConsole) {
+ $script .= "goto start ||\n";
+ }
+ return $script;
+ }
+
+}