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.php127
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php57
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/execdata.inc.php7
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php154
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxebuilder.inc.php80
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php113
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/localboot.inc.php37
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php58
-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.php49
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php20
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuildergrub.inc.php330
-rw-r--r--modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php137
15 files changed, 985 insertions, 534 deletions
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
index 614f5ee4..5812c0cd 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentry.inc.php
@@ -12,17 +12,28 @@ abstract class BootEntry
/** Supports both via distinct entry */
const BOTH = 'PCBIOS-EFI';
- public abstract function supportsMode($mode);
-
/**
- * @param ScriptBuilderBase $builder
- * @return string
+ * @var string Internal ID - set to your liking, e.g. the MiniLinux version identifier
*/
- public abstract function toScript($builder);
+ protected $internalId;
+
+ public function __construct(string $internalId)
+ {
+ $this->internalId = $internalId;
+ }
+
+ public abstract function supportsMode(string $mode): bool;
- public abstract function toArray();
+ public abstract function toScript(ScriptBuilderBase $builder): string;
- public abstract function addFormFields(&$array);
+ public abstract function toArray(): array;
+
+ public abstract function addFormFields(array &$array): void;
+
+ public function internalId(): string
+ {
+ return $this->internalId;
+ }
/*
*
@@ -32,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;
}
@@ -49,9 +60,9 @@ 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);
}
@@ -64,14 +75,14 @@ abstract class BootEntry
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;
@@ -89,7 +100,7 @@ abstract class BootEntry
return $ret;
}
- public static function newCustomBootEntry($initData)
+ public static function newCustomBootEntry($initData): ?CustomBootEntry
{
if (!is_array($initData) || empty($initData))
return null;
@@ -99,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']);
}
@@ -117,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;
@@ -143,14 +153,15 @@ class StandardBootEntry extends BootEntry
*/
protected $efi;
/**
- * @var string BootEntry Constants above
+ * @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) {
@@ -223,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;
@@ -250,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;
@@ -266,7 +274,7 @@ class StandardBootEntry extends BootEntry
return false;
}
- public function toScript($builder)
+ public function toScript(ScriptBuilderBase $builder): string
{
if ($this->arch === BootEntry::AGNOSTIC) // Same as below, could construct fall-through but this is more clear
return $builder->execDataToScript($this->pcbios, null, null);
@@ -275,7 +283,7 @@ class StandardBootEntry extends BootEntry
$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);
@@ -283,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(),
@@ -298,7 +309,7 @@ class CustomBootEntry extends BootEntry
/**
* @var string iPXE
*/
- protected $ipxe;
+ protected $ipxe = '';
protected $bash;
@@ -306,6 +317,7 @@ class CustomBootEntry extends BootEntry
public function __construct($data)
{
+ parent::__construct('custom');
if (is_array($data)) {
$this->ipxe = $data['script'] ?? ''; // LEGACY
foreach (['bash', 'grub'] as $key) {
@@ -314,21 +326,24 @@ class CustomBootEntry extends BootEntry
}
}
- public function supportsMode($mode)
+ public function supportsMode(string $mode): bool
{
return true;
}
- public function toScript($builder)
+ public function toScript(ScriptBuilderBase $builder): string
{
+ // 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->ipxe,
@@ -336,7 +351,10 @@ class CustomBootEntry extends BootEntry
$array['script_checked'] = 'checked';
}
- public function toArray()
+ /**
+ * @return array{script: string}
+ */
+ public function toArray(): array
{
return ['script' => $this->ipxe];
}
@@ -344,30 +362,32 @@ class CustomBootEntry extends BootEntry
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($builder)
+ public function toScript(ScriptBuilderBase $builder): string
{
- $menu = IPxeMenu::get($this->menuId);
+ $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
{
}
}
@@ -380,23 +400,24 @@ class SpecialBootEntry extends BootEntry
public function __construct($type)
{
$this->type = $type['type'] ?? $type;
+ parent::__construct('special-' . $this->type);
}
- public function supportsMode($mode)
+ public function supportsMode(string $mode): bool
{
return true;
}
- public function toScript($builder)
+ public function toScript(ScriptBuilderBase $builder): string
{
return $builder->getSpecial($this->type);
}
- public function toArray()
+ public function toArray(): array
{
return [];
}
- public function addFormFields(&$array) { }
+ public function addFormFields(array &$array): void { }
-} \ No newline at end of file
+}
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php
index 73611b0a..ab55c888 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/bootentryhook.inc.php
@@ -6,41 +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;
- /**
- * @param string $id
- * @return bool
- */
- 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) {
@@ -54,16 +50,13 @@ 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);
}
@@ -71,7 +64,7 @@ abstract class BootEntryHook
* @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($mixed)
+ public function setSelected(string $mixed): void
{
$json = @json_decode($mixed, true);
if (is_array($json)) {
@@ -86,16 +79,19 @@ abstract class BootEntryHook
/**
* @return string ID of entry that was marked as selected by setSelected()
*/
- public function getSelected()
+ public function getSelected(): string
{
return $this->selectedId;
}
- public function renderExtraFields()
+ /**
+ * @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;
@@ -144,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;
@@ -182,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;
@@ -203,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 1f6fa265..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 = [];
diff --git a/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/ipxe.inc.php
index 29885588..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 {
@@ -321,7 +319,7 @@ class IPxe
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',
+ 'module' => ($entry instanceof StandardBootEntry) ? '.exec' : '.script',
'hotkey' => $hotkey,
'title' => $title,
'data' => json_encode($data),
@@ -408,10 +406,9 @@ class IPxe
* 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
@@ -434,10 +431,9 @@ class IPxe
}
/**
- * @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
@@ -466,7 +462,7 @@ class IPxe
$script .= "set netX/{$opt}:{$type} {$args[$i]} || goto %fail%\n";
}
}
- } elseif ($arg{0} === '-') {
+ } elseif ($arg[0] === '-') {
continue;
} elseif ($file === false) {
$file = self::parseFile($arg);
@@ -496,11 +492,8 @@ class IPxe
/**
* 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];
@@ -508,12 +501,12 @@ class IPxe
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 '';
@@ -528,15 +521,16 @@ class IPxe
* 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 b1e13e87..3ffecba1 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/ipxemenu.inc.php
@@ -3,19 +3,28 @@
class IPxeMenu
{
+ /**
+ * @var int ID of this menu, from DB
+ */
protected $menuid;
+ /**
+ * @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[]
*/
public $items = [];
- /**
- * @param int $menuId
- */
- public static function get($menuId, $emptyFallback = false)
+ 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]);
@@ -31,33 +40,34 @@ class IPxeMenu
*
* @param array $menu array for according menu row
*/
- public function __construct($menu)
+ public function __construct(array $menu)
{
$this->menuid = (int)$menu['menuid'];
$this->timeoutMs = (int)$menu['timeoutms'];
- $this->title = $menu['title'];
- $this->defaultEntryId = $menu['defaultentryid'];
+ $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();
+ if ($defaultEntryId === null && !empty($this->items)) {
+ $defaultEntryId = $this->items[0]->menuEntryId();
}
+ $this->defaultEntryId = (int)$defaultEntryId;
}
- public function title()
+ public function title(): string
{
return $this->title;
}
- public function timeoutMs()
+ public function timeoutMs(): int
{
return $this->timeoutMs;
}
@@ -65,36 +75,74 @@ class IPxeMenu
/**
* @return int Number of items in this menu
*/
- public function itemCount()
+ public function itemCount(): int
{
return count($this->items);
}
/**
- * @return string|null Return script label of default entry, null if not set
+ * @return MenuEntry|null Return preselected menu entry
*/
- public function getDefaultEntryId()
+ public function defaultEntry(): ?MenuEntry
{
- return $this->defaultEntryId;
+ 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) : IPxeMenu
+ public static function forLocation(int $locationId): IPxeMenu
{
$chain = null;
if (Module::isAvailable('locations')) {
@@ -109,7 +157,7 @@ class IPxeMenu
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;
}
@@ -132,13 +180,19 @@ class IPxeMenu
return new IPxeMenu($menu);
}
- public static function forClient($ip, $uuid) : IPxeMenu
+ 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;
}
}
@@ -146,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 eb4a98de..da94a16b 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/menuentry.inc.php
@@ -27,7 +27,7 @@ class MenuEntry
*/
public $sortval;
/**
- * @var BootEntry
+ * @var ?BootEntry
*/
public $bootEntry = null;
@@ -35,11 +35,7 @@ class MenuEntry
public $md5pass = null;
- /**
- * @param int $menuEntryId
- * @return MenuEntry|null
- */
- public static function get($menuEntryId)
+ 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
@@ -56,48 +52,53 @@ class MenuEntry
*
* @param array $row row from database
*/
- public function __construct($row)
+ public function __construct(array $row)
{
- if (is_array($row)) {
- 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->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']);
+ 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 getBootEntryScript($builder)
+ public function getBootEntryScript(ScriptBuilderBase $builder): string
{
if ($this->bootEntry === null)
return '';
return $this->bootEntry->toScript($builder);
}
- public function menuEntryId()
+ public function menuEntryId(): int
{
return $this->menuentryid;
}
- public function title()
+ public function title(): string
{
return $this->title;
}
+ public function internalId(): string
+ {
+ if ($this->bootEntry === null)
+ return '';
+ return $this->bootEntry->internalId();
+ }
+
/*
*
*/
@@ -137,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());
}
@@ -146,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]))
@@ -161,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 3f406767..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 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($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
index 84cfd7db..9cd07388 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php
@@ -9,8 +9,10 @@ abstract class ScriptBuilderBase
protected $platform = '';
+ /** @var string */
protected $clientIp;
+ /** @var ?string */
protected $uuid;
/**
@@ -18,57 +20,57 @@ abstract class ScriptBuilderBase
*/
protected $hasExtension = false;
- public function hasExtensions()
+ public function hasExtensions(): bool
{
return $this->hasExtension;
}
- public function platform()
+ public function platform(): string
{
return $this->platform;
}
- public function uuid()
+ public function uuid(): ?string
{
return $this->uuid;
}
- public function clientIp()
+ public function clientIp(): string
{
return $this->clientIp;
}
- public function getLabel()
+ public function getLabel(): string
{
return 'b' . mt_rand(100, 999) . 'x' . (++$this->lblId);
}
- public function __construct($platform = null, $serverIp = null, $slxExtensions = null)
+ public function __construct(?string $platform = null, ?string $serverIp = null, ?bool $slxExtensions = null)
{
- $this->clientIp = $_SERVER['REMOTE_ADDR'];
+ $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', false, 'string');
- if ($this->platform !== false) {
+ $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');
- $this->uuid = Request::any('uuid', false, 'string');
- if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $this->uuid)) {
- $this->uuid = false;
+ $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.
- * @param string $string
*/
- public abstract function output($string);
+ public abstract function output(string $string): void;
public abstract function bootstrapLive();
@@ -79,31 +81,22 @@ abstract class ScriptBuilderBase
* @param bool $honorPassword Whether we should generate a password dialog if protected, or skip
* @return string generated script/code/...
*/
- public abstract function getMenuEntry($menuEntry, $honorPassword = true);
+ public abstract function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string;
/**
* @param BootEntry|null|false $bootEntry
- * @return string
*/
- public abstract function getBootEntry($bootEntry);
+ public abstract function getBootEntry(?BootEntry $entry): string;
- public abstract function getSpecial($special);
+ public abstract function getSpecial(string $special);
- /**
- * @param IPxeMenu|null $menu
- * @return string
- */
- public abstract function menuToScript($menu);
+ 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.
- * @param ExecData $agnostic
- * @param ExecData $bios
- * @param ExecData $efi
- * @return string
*/
- public abstract function execDataToScript($agnostic, $bios, $efi);
+ 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
index 86b2931f..d6b542ec 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbash.inc.php
@@ -3,39 +3,39 @@
class ScriptBuilderBash extends ScriptBuilderBase
{
- public function output($string)
+ public function output(string $string): void
{
echo $string;
}
- public function bootstrapLive() { return false; }
+ public function bootstrapLive(): bool { return false; }
- public function getMenu(IPxeMenu $menu, bool $bootstrap)
+ public function getMenu(IPxeMenu $menu, bool $bootstrap): string
{
return $this->menuToScript($menu);
}
- public function getBootEntry($entry)
+ public function getBootEntry(?BootEntry $entry): string
{
- if (!$entry) {
+ if ($entry === null) {
return "echo 'Invalid boot entry id'\nread -n1 -r _\n";
}
return $entry->toScript($this);
}
- public function getMenuEntry($entry, $honorPassword = true)
+ 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($special)
+ public function getSpecial(string $special): string
{
return ''; // We can't really do localboot here I guess
}
- public function menuToScript($menu)
+ 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) {
@@ -59,7 +59,7 @@ class ScriptBuilderBash extends ScriptBuilderBase
. "\nmenu_title=" . $this->bashString($menu->title) . "\n";
}
- public function execDataToScript($agnostic, $bios, $efi) : string
+ public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi): string
{
if ($agnostic !== null)
return $this->execDataToScriptInternal($agnostic);
@@ -86,7 +86,7 @@ class ScriptBuilderBash extends ScriptBuilderBase
return $script;
}
- private function bashString($string)
+ private function bashString(string $string): string
{
if (strpos($string, "'") === false) {
return "'$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
index 385cd15f..9421684f 100644
--- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
+++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderipxe.inc.php
@@ -3,7 +3,7 @@
class ScriptBuilderIpxe extends ScriptBuilderBase
{
- private function getUrlBase()
+ private function getUrlBase(): string
{
if (isset($_SERVER['REQUEST_URI'])) {
$url = parse_url($_SERVER['REQUEST_URI']);
@@ -23,7 +23,7 @@ class ScriptBuilderIpxe extends ScriptBuilderBase
}
- private function getUrlFull(&$hasExt, $key = null, $value = null)
+ private function getUrlFull(?bool &$hasExt = null, ?string $key = null, ?string $value = null): string
{
$url = parse_url($_SERVER['REQUEST_URI']);
$urlbase = $this->getUrlBase();
@@ -58,7 +58,7 @@ class ScriptBuilderIpxe extends ScriptBuilderBase
/**
* Redirect to same URL, but add our extended params
*/
- private function redirect($key = null, $value = null)
+ private function redirect(string $key = null, string $value = null): string
{
// Redirect to self with added parameters
$urlfull = $this->getUrlFull($hasExt, $key, $value);
@@ -102,27 +102,24 @@ HERE;
return false;
}
- public function getBootEntry($entry)
+ public function getBootEntry(?BootEntry $entry): string
{
- if (!$entry) {
+ if ($entry === null) {
return "#!ipxe\nprompt --timeout 5000 Invalid boot entry id\n";
}
return $entry->toScript($this);
}
- public function getMenu(IPxeMenu $menu, bool $bootstrap)
+ public function getMenu(IPxeMenu $menu, bool $bootstrap): string
{
if ($bootstrap) {
return "#!ipxe\nimgfree ||\n" . $this->menuToScript($menu);
}
- $base = $this->getUrlFull($he);
+ $base = $this->getUrlFull();
return "#!ipxe\nset self {$base} ||\n" . $this->menuToScript($menu);
}
- /**
- * @param IPxeMenu $menu
- */
- public function menuToScript($menu)
+ public function menuToScript(IPxeMenu $menu): string
{
if ($this->hasExtension) {
$slxConsoleUpdate = '--update';
@@ -136,13 +133,13 @@ HERE;
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
+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
@@ -166,7 +163,11 @@ HERE;
$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.
@@ -179,49 +180,10 @@ prompt Boot failed. Press any key to start.
goto start
HERE;
-
- /*
-
- :i5
- chain -a /tftp/memtest.0 passes=1 onepass || goto membad
- prompt Memory OK. Press a key.
- goto init
-
- :i8
- set x:int32 0
- :again
- console --left 60 --top 130 --right 67 --bottom 96 --picture bg-load --keep ||
- console --left 55 --top 88 --right 63 --bottom 64 --picture bg-menu --keep ||
- inc x
- iseq \${x} 20 || goto again
- prompt DONE. Press dein Knie.
- goto slx_menu
-
- :membad
- iseq \${errno} 0x1 || goto memaborted
- params
- param scrot \${vram}
- imgfetch -a http://132.230.8.113/screen.php##params ||
- prompt Memory is bad. Press a key.
- goto init
-
- :memaborted
- params
- param scrot \${vram}
- imgfetch -a http://132.230.8.113/screen.php##params ||
- prompt Memory test aborted. Press a key.
- goto init
-
- */
return $output;
}
- /**
- * @param $requestedDefaultId
- * @param MenuEntry $entry
- * @return string
- */
- private function getMenuItemScript($requestedDefaultId, $entry)
+ private function getMenuItemScript(int $requestedDefaultId, MenuEntry $entry): string
{
$str = 'item ';
if ($entry->gap) {
@@ -250,7 +212,7 @@ HERE;
return $str . " || prompt Could not create menu item for {$entry->menuentryid}\n";
}
- public function getSpecial($special)
+ public function getSpecial(string $special): string
{
if ($special === 'localboot') {
// Get preferred localboot method, depending on system model
@@ -299,11 +261,7 @@ HERE;
}
}
// Convert to actual ipxe code
- if (isset($BOOT_METHODS[$localboot])) {
- $localboot = $BOOT_METHODS[$localboot];
- } else {
- $localboot = 'prompt Localboot not possible';
- }
+ $localboot = $BOOT_METHODS[$localboot] ?? 'prompt Localboot not possible';
$output = <<<BLA
imgfree ||
console ||
@@ -317,17 +275,27 @@ BLA;
return $output;
}
- public function output($string)
+ public function output(string $string): void
{
- 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');
- if ($this->platform === 'EFI') {
- $cs = 'ASCII';
+ // 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 {
- $cs = 'IBM437';
- }
- Header('Content-Type: text/plain; charset=' . $cs);
+ if ($this->platform === 'EFI') {
+ $cs = 'ASCII';
+ } else {
+ $cs = 'IBM437';
+ }
+ Header('Content-Type: text/plain; charset=' . $cs);
- echo iconv('UTF-8', $cs . '//TRANSLIT//IGNORE', $string);
+ 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)
@@ -337,28 +305,9 @@ BLA;
return trim(preg_replace('/\s+/', ' ', $str));
}
- /**
- * @param IPxeMenu $menu
- */
- private function menuCheckAutostart($menu)
- {
- // If this is a menu with a single item, treat a timeout of 0 as "boot immediately" instead of "infinite"
- if ($menu->itemCount() === 1 && $menu->timeoutMs() === 0 && ($tmp = $menu->getDefaultEntryId()) !== null) {
- if (empty($menu->items[0]->md5pass)) {
- return $menu->items[0]->getBootEntryScript($this);
- } else {
- return $this->passwordDialog($menu->items[0]);
- }
- }
- return '';
- }
-
const PROP_PW_SALT = 'ipxe.salt.';
- /**
- * @param MenuEntry $menuEntryId
- */
- private function passwordDialog($entry)
+ private function passwordDialog(MenuEntry $entry): string
{
if ($this->hasExtension) {
$salt = dechex(mt_rand(0x100000, 0xFFFFFF));
@@ -384,7 +333,7 @@ chain -a \${self}&entryid={$entry->menuentryid}##params || goto fail ||
HERE;
}
- public function getMenuEntry($entry, $honorPassword = true)
+ public function getMenuEntry(?MenuEntry $entry, bool $honorPassword = true): string
{
if ($entry === null)
return "#!ipxe\nprompt --timeout 10000 Invalid menu entry id\n";
@@ -438,7 +387,7 @@ HERE;
return $output;
}
- public function execDataToScript($agnostic, $bios, $efi) : string
+ public function execDataToScript(?ExecData $agnostic, ?ExecData $bios, ?ExecData $efi) : string
{
if ($agnostic !== null)
return $this->execDataToScriptInternal($agnostic) . "\ngoto fail\n";