summaryrefslogtreecommitdiffstats
path: root/modules-available/minilinux/inc/minilinux.inc.php
diff options
context:
space:
mode:
Diffstat (limited to 'modules-available/minilinux/inc/minilinux.inc.php')
-rw-r--r--modules-available/minilinux/inc/minilinux.inc.php324
1 files changed, 267 insertions, 57 deletions
diff --git a/modules-available/minilinux/inc/minilinux.inc.php b/modules-available/minilinux/inc/minilinux.inc.php
index 005b81fa..cbc797f2 100644
--- a/modules-available/minilinux/inc/minilinux.inc.php
+++ b/modules-available/minilinux/inc/minilinux.inc.php
@@ -11,21 +11,27 @@ class MiniLinux
const INVALID = 'invalid';
+ const INSTALL_MISSING = 0;
+
+ const INSTALL_OK = 1;
+
+ const INSTALL_BROKEN = 2;
+
/*
* Update of available versions by querying sources
*/
/**
- * Query all known sources for meta data
+ * Query all known sources for metadata
* @return int number of sources query was just initialized for
*/
- public static function updateList()
+ public static function updateList(): int
{
$stamp = time();
$last = Property::get(self::PROPERTY_KEY_FETCHTIME);
- if ($last !== false && $last + 10 > $stamp)
+ if ($last !== false && $last + 3 > $stamp)
return 0; // In progress...
- Property::set(self::PROPERTY_KEY_FETCHTIME, $stamp, 1);
+ Property::set(self::PROPERTY_KEY_FETCHTIME, $stamp, 10);
Database::exec('LOCK TABLES callback WRITE,
minilinux_source WRITE, minilinux_branch WRITE, minilinux_version WRITE');
Database::exec('UPDATE minilinux_source SET taskid = UUID()');
@@ -33,7 +39,8 @@ class MiniLinux
Database::exec("UPDATE minilinux_version
INNER JOIN minilinux_branch USING (branchid)
INNER JOIN minilinux_source USING (sourceid)
- SET orphan = orphan + 1 WHERE minilinux_source.lastupdate < $cutoff");
+ SET orphan = orphan + 1
+ WHERE minilinux_source.lastupdate < $cutoff AND orphan < 100");
$list = Database::queryAll('SELECT sourceid, url, taskid FROM minilinux_source');
foreach ($list as $source) {
Taskmanager::submit('DownloadText', array(
@@ -48,20 +55,22 @@ class MiniLinux
/**
* Called when downloading metadata from a specific update source is finished
- * @param mixed $task task structure
+ *
+ * @param array $task task structure
* @param string $sourceid see minilinux_source table
*/
- public static function listDownloadCallback($task, $sourceid)
+ public static function listDownloadCallback(array $task, string $sourceid): void
{
- if ($task['statusCode'] !== 'TASK_FINISHED')
+ if (!Taskmanager::isFinished($task))
return;
$taskId = $task['id'];
- $data = json_decode($task['data']['content'], true);
- if (!is_array($data)) {
- EventLog::warning('Cannot download Linux version meta data for ' . $sourceid);
+ $data = json_decode($task['data']['content'] ?? '', true);
+ if (!is_array($data) || empty($data['systems'])) {
+ EventLog::warning('Cannot download Linux version meta data for ' . $sourceid,
+ ($task['data']['error'] ?? '') . "\n\nContent:\n" . ($task['data']['content'] ?? ''));
$lastupdate = 'lastupdate';
} else {
- if (@is_array($data['systems'])) {
+ if (is_array($data['systems'])) {
self::addBranches($sourceid, $data['systems']);
}
$lastupdate = 'UNIX_TIMESTAMP()';
@@ -70,7 +79,8 @@ class MiniLinux
WHERE sourceid = :sourceid AND taskid = :taskid",
['sourceid' => $sourceid, 'taskid' => $taskId]);
// Clean up -- delete orphaned versions that are not installed
- Database::exec('DELETE FROM minilinux_version WHERE orphan > 4 AND installed = 0');
+ Database::exec('DELETE FROM minilinux_version WHERE orphan > 4 AND installed = :missing',
+ ['missing' => self::INSTALL_MISSING]);
// FKC makes sure we only delete orphaned ones
Database::exec('DELETE IGNORE FROM minilinux_branch WHERE 1', [], true);
}
@@ -81,18 +91,31 @@ class MiniLinux
if (!self::isValidIdPart($system['id']))
continue;
$branchid = $sourceid . '/' . $system['id'];
- $title = empty($system['title']) ? $branchid : $system['title'];
- $description = empty($system['description']) ? '' : $system['description'];
- Database::exec('INSERT INTO minilinux_branch (branchid, sourceid, title, description)
- VALUES (:branchid, :sourceid, :title, :description)
- ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description)', [
- 'branchid' => $branchid,
- 'sourceid' => $sourceid,
- 'title' => $title,
- 'description' => $description,
- ]);
- if (@is_array($system['versions'])) {
+ $title = mb_substr(empty($system['title']) ? $branchid : $system['title'], 0, 150);
+ $description = $system['description'] ?? '';
+ $color = $system['color'] ?? '';
+ if (!empty($system['versions']) && is_array($system['versions'])) {
+ Database::exec('INSERT INTO minilinux_branch (branchid, sourceid, title, color, description)
+ VALUES (:branchid, :sourceid, :title, :color, :description)
+ ON DUPLICATE KEY UPDATE title = VALUES(title), color = VALUES(color), description = VALUES(description)', [
+ 'branchid' => $branchid,
+ 'sourceid' => $sourceid,
+ 'title' => $title,
+ 'color' => $color,
+ 'description' => $description,
+ ]);
self::addVersions($branchid, $system['versions']);
+ } else {
+ // Empty branch - only update metadata if branch exists locally
+ Database::exec('UPDATE minilinux_branch
+ SET title = :title, color = :color, description = :description
+ WHERE sourceid = :sourceid AND branchid = :branchid', [
+ 'branchid' => $branchid,
+ 'sourceid' => $sourceid,
+ 'title' => $title,
+ 'color' => $color,
+ 'description' => $description,
+ ]);
}
}
}
@@ -115,7 +138,8 @@ class MiniLinux
return;
}
$versionid = $branchid . '/' . $version['version'];
- $title = empty($version['title']) ? '' : $version['title'];
+ $title = $version['title'] ?? '';
+ $description = $version['description'] ?? '';
$dateline = empty($version['releasedate']) ? time() : (int)$version['releasedate'];
unset($version['version'], $version['title'], $version['releasedate']);
// Sanitize files array
@@ -148,18 +172,20 @@ class MiniLinux
$version['files'] = array_values($version['files']);
}
$data = json_encode($version);
- Database::exec('INSERT INTO minilinux_version (versionid, branchid, title, dateline, data, orphan)
- VALUES (:versionid, :branchid, :title, :dateline, :data, 0)
- ON DUPLICATE KEY UPDATE title = VALUES(title), data = VALUES(data), orphan = 0', [
+ Database::exec('INSERT INTO minilinux_version (versionid, branchid, title, description, dateline, data, orphan)
+ VALUES (:versionid, :branchid, :title, :description, :dateline, :data, 0)
+ ON DUPLICATE KEY UPDATE title = VALUES(title), description = VALUES(description),
+ dateline = VALUES(dateline), data = VALUES(data), orphan = 0', [
'versionid' => $versionid,
'branchid' => $branchid,
- 'title' => $title,
+ 'title' => mb_substr($title, 0, 150),
+ 'description' => $description,
'dateline' => $dateline,
'data' => $data,
]);
}
- private static function isValidIdPart($str)
+ private static function isValidIdPart(string $str): bool
{
return preg_match('/^[a-z0-9_\-]+$/', $str) > 0;
}
@@ -168,10 +194,10 @@ class MiniLinux
* Download of specific version
*/
- public static function validateDownloadTask($versionid, $taskid)
+ public static function validateDownloadTask(string $versionid, ?string $taskid): ?string
{
if ($taskid === null)
- return false;
+ return null;
$task = Taskmanager::status($taskid);
if (Taskmanager::isTask($task) && !Taskmanager::isFailed($task)
&& (is_dir(CONFIG_HTTP_DIR . '/' . $versionid) || !Taskmanager::isFinished($task)))
@@ -179,15 +205,13 @@ class MiniLinux
Database::exec('UPDATE minilinux_version SET taskid = NULL
WHERE versionid = :versionid AND taskid = :taskid',
['versionid' => $versionid, 'taskid' => $taskid]);
- return false;
+ return null;
}
/**
* Download the files for the given version id
- * @param $versionid
- * @return bool
*/
- public static function downloadVersion($versionid)
+ public static function downloadVersion(string $versionid): ?string
{
$ver = Database::queryFirst('SELECT s.url, s.pubkey, v.versionid, v.taskid, v.data FROM minilinux_version v
INNER JOIN minilinux_branch b USING (branchid)
@@ -195,17 +219,17 @@ class MiniLinux
WHERE versionid = :versionid',
['versionid' => $versionid]);
if ($ver === false)
- return false;
+ return null;
$taskid = self::validateDownloadTask($versionid, $ver['taskid']);
- if ($taskid !== false)
+ if ($taskid !== null)
return $taskid;
$data = json_decode($ver['data'], true);
if (!is_array($data)) {
EventLog::warning("Cannot download Linux '$versionid': Corrupted meta data.", $ver['data']);
- return false;
+ return null;
}
if (empty($data['files']))
- return false;
+ return null;
$list = [];
$legacyDir = preg_replace(',^[^/]*/,', '', $versionid);
foreach ($data['files'] as $file) {
@@ -224,6 +248,7 @@ class MiniLinux
Database::exec('LOCK TABLES minilinux_version WRITE');
$aff = Database::exec('UPDATE minilinux_version SET taskid = :taskid WHERE versionid = :versionid AND taskid IS NULL',
['taskid' => $uuid, 'versionid' => $versionid]);
+ $task = false;
if ($aff > 0) {
$task = Taskmanager::submit('DownloadFiles', [
'id' => $uuid,
@@ -234,22 +259,22 @@ class MiniLinux
if (Taskmanager::isFailed($task)) {
$task = false;
} else {
- $task = $task['id'];
+ $task = (string)$task['id'];
}
- } else {
- $task = false;
}
Database::exec('UNLOCK TABLES');
if ($task !== false) {
// Callback for db column
TaskmanagerCallback::addCallback($task, 'mlGotLinux', $versionid);
+ self::checkStage4($data);
}
+ // Race - someone else wrote a taskid to DB, just call self again to get that one
if ($aff === 0)
return self::downloadVersion($versionid);
return $task;
}
- public static function fileToId($versionid, $fileName)
+ public static function fileToId(string $versionid, string $fileName): string
{
return 'x' . substr(md5($fileName . $versionid), 0, 8);
}
@@ -259,10 +284,10 @@ class MiniLinux
*/
/**
- * Geenrate messages regarding setup und update availability.
+ * Generate messages regarding setup und update availability.
* @return bool true if severe problems were found, false otherwise
*/
- public static function generateUpdateNotice()
+ public static function generateUpdateNotice(): bool
{
// Messages in here are with module name, as required by the
// main-warning hook.
@@ -305,7 +330,7 @@ class MiniLinux
* actually installed locally.
* @return bool true if installed locally, false otherwise
*/
- public static function updateCurrentBootSetting()
+ public static function updateCurrentBootSetting(): bool
{
$default = Property::get(self::PROPERTY_DEFAULT_BOOT);
if ($default === false)
@@ -318,7 +343,7 @@ class MiniLinux
} elseif ($slashes === 1) {
// Latest from branch
$ver = Database::queryFirst('SELECT versionid, installed FROM minilinux_version
- WHERE branchid = :branchid AND installed = 1 ORDER BY dateline DESC', ['branchid' => $default]);
+ WHERE branchid = :branchid AND installed = :ok ORDER BY dateline DESC', ['branchid' => $default, 'ok' => self::INSTALL_OK]);
} else {
// Unknown
return false;
@@ -329,33 +354,218 @@ class MiniLinux
return false;
}
Property::set(self::PROPERTY_DEFAULT_BOOT_EFFECTIVE, $ver['versionid']);
- return $ver['installed'] != 0;
+ return $ver['installed'] != self::INSTALL_MISSING;
}
public static function linuxDownloadCallback($task, $versionid)
{
- self::setInstalledState($versionid, $task['statusCode'] === 'TASK_FINISHED');
+ self::setInstalledState($versionid, $task['statusCode'] === 'TASK_FINISHED' ? self::INSTALL_OK : self::INSTALL_BROKEN);
}
- public static function setInstalledState($versionid, $installed)
+ public static function setInstalledState($versionid, int $installed): void
{
- settype($installed, 'int');
- error_log("Setting $versionid to $installed");
Database::exec('UPDATE minilinux_version SET installed = :installed WHERE versionid = :versionid', [
'versionid' => $versionid,
'installed' => $installed,
]);
+ if ($installed === self::INSTALL_OK) {
+ $res = Database::queryFirst('SELECT Count(*) AS cnt FROM minilinux_version WHERE installed = :ok',
+ ['ok' => self::INSTALL_OK]);
+ if ($res['cnt'] == 1) {
+ self::setDefaultVersion($versionid);
+ }
+ }
}
- public static function queryAllVersionsByBranch()
+ public static function queryAllVersionsByBranch(): array
{
$list = [];
- $res = Database::simpleQuery('SELECT branchid, versionid, title, dateline, orphan, taskid, installed
+ $res = Database::simpleQuery('SELECT branchid, versionid, title, Length(description) AS desclen,
+ dateline, orphan, taskid, installed
FROM minilinux_version ORDER BY branchid, dateline, versionid');
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ foreach ($res as $row) {
$list[$row['branchid']][$row['versionid']] = $row;
}
return $list;
}
-} \ No newline at end of file
+ public static function setDefaultVersion($versionId)
+ {
+ Property::set(MiniLinux::PROPERTY_DEFAULT_BOOT, $versionId);
+ self::updateCurrentBootSetting();
+ // Legacy PXELINUX boot menu (TODO: Remove this when we get rid of PXELINUX support)
+ $task = Taskmanager::submit('Symlink', [
+ 'target' => $versionId,
+ 'linkname' => CONFIG_HTTP_DIR . '/default',
+ ]);
+ if ($task !== false) {
+ Taskmanager::release($task);
+ }
+ }
+
+ /**
+ * Check whether an optionally required stage4 is available.
+ * Return true if there is no stage4, otherwise check filesystem,
+ * or try to request from local dnbd3-server.
+ *
+ * @param array $data decoded data column from minilinux_version
+ * @param string[] $errors in array of error messages if not available
+ * @return bool true if stage4 is available or none required
+ */
+ public static function checkStage4(array $data, &$errors = false): bool
+ {
+ $errors = [];
+ $image = false;
+ $rid = 0;
+ foreach (['agnostic', 'efi', 'bios'] as $type) {
+ if (!isset($data[$type]) || !isset($data[$type]['commandLine']))
+ continue;
+ if (!preg_match('/\bslx\.stage4\.path=(\S+)/', $data[$type]['commandLine'], $out))
+ continue;
+ $image = $out[1];
+ if (preg_match('/\bslx\.stage4\.rid=(\d+)/', $data[$type]['commandLine'], $out)) {
+ $rid = (int)$out[1];
+ }
+ break;
+ }
+ if ($image === false)
+ return true; // No stage4
+ if ($rid === 0) {
+ // Get latest local revision
+ foreach (glob(CONFIG_VMSTORE_DIR . '/' . $image . '.r*', GLOB_NOSORT) as $file) {
+ if (preg_match('/\.r(\d+)$/', $file, $out)) {
+ $cmp = (int)$out[1];
+ if ($cmp > $rid) {
+ $rid = $cmp;
+ }
+ }
+ }
+ }
+ if ($rid > 0 && file_exists(CONFIG_VMSTORE_DIR . '/' . $image . '.r' . $rid)
+ && !file_exists(CONFIG_VMSTORE_DIR . '/' . $image . '.r' . $rid . '.map')) {
+ // Accept if image exists locally and no map file (map file would mean incomplete)
+ return true;
+ }
+ // Not found locally -- try to replicate
+ $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+ if ($sock === false) {
+ $errors[] = 'Error creatring socket to connect to dnbd3-server';
+ return false;
+ }
+ socket_set_option($sock, SOL_SOCKET, SO_SNDTIMEO, array('sec' => 1, 'usec' => 0));
+ socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 5, 'usec' => 0));
+ if (@socket_connect($sock, '127.0.0.1', 5003) === false) {
+ $errors[] = 'Could not connect to local dnbd3-server';
+ socket_close($sock);
+ return false;
+ }
+ // proto-version(16), image name\0, rid(16), flags(8)
+ $payload = pack('vA*xvC', 3, $image, $rid, 0);
+ // magic(16), cmd(16), payload-len(32), offset(64), handle(64) XXX 32Bit compat
+ $packet = pack('A*vVVVVV', 'sr', 2, strlen($payload), 0, 0, 1234, 0) . $payload;
+ if (!socket_send($sock, $packet, strlen($packet), 0)) {
+ $errors[] = 'Cannot send request to dnbd3-server';
+ socket_close($sock);
+ return false;
+ }
+ $len = socket_recv($sock, $reply, 16, MSG_WAITALL);
+ if ($len === 0) {
+ $errors[] = 'Local dnbd3-server cannot replicate required stage4 from master-server';
+ socket_close($sock);
+ return false;
+ }
+ if ($len !== 16) {
+ $errors[] = 'Incomplete reply received from local dnbd3-server. Stage4 might not replicate!';
+ socket_close($sock);
+ return false;
+ }
+ socket_close($sock);
+ // Try to decode header
+ $reply = unpack('A2magic/vcmd/Vsize/Vhandlelow/Vhandlehigh', $reply);
+ if ($reply['magic'] !== 'sr') {
+ $errors[] = 'Reply has wrong magic';
+ }
+ if ($reply['cmd'] !== 2) {
+ $errors[] = 'Reply is not CMD_IMAGE_REPLY';
+ }
+ return empty($errors);
+ }
+
+ /**
+ * Determine by which menus/locations each MiniLinux version is being used.
+ */
+ public static function getBootMenuUsage(): array
+ {
+ if (!Module::isAvailable('serversetup') || !class_exists('BootEntryHook'))
+ return [];
+ $res = Database::simpleQuery("SELECT be.entryid, be.data,
+ GROUP_CONCAT(DISTINCT me.menuid) AS menus,
+ GROUP_CONCAT(DISTINCT ml.locationid) AS locations
+ FROM serversetup_bootentry be
+ LEFT JOIN serversetup_menuentry me USING (entryid)
+ LEFT JOIN serversetup_menu_location ml USING (menuid)
+ WHERE module = 'minilinux'
+ GROUP BY be.data");
+ $return = [];
+ $usedMenuIds = [];
+ foreach ($res as $row) {
+ $data = json_decode($row['data'], true);
+ if (!isset($data['id']))
+ continue;
+ $id = self::resolveEntryId($data['id']);
+ $new = [
+ 'entryids' => [$row['entryid']],
+ 'menus' => explode(',', $row['menus'] ?? ''),
+ 'locations' => explode(',', $row['locations'] ?? ''),
+ ];
+ $usedMenuIds = array_merge($usedMenuIds, $new['menus']);
+ if (isset($return[$id])) {
+ $return[$id] = array_merge_recursive($return[$id], $new);
+ } else {
+ $return[$id] = $new;
+ }
+ }
+ // Build id => title map for menus
+ $res = Database::simpleQuery("SELECT menuid, title FROM serversetup_menu m
+ WHERE menuid IN (:menuid)", ['menuid' => array_unique($usedMenuIds)]);
+ $menus = [];
+ foreach ($res as $row) {
+ $menus[$row['menuid']] = $row['title'];
+ }
+ // Build output array
+ foreach ($return as &$item) {
+ $item['locations'] = array_map(function ($i) {
+ return ['locationid' => $i, 'locationname' => Location::getName($i)];
+ }, array_unique(array_filter($item['locations'], 'is_numeric')));
+ $item['menus'] = array_map(function ($i) use ($menus) {
+ return ['menuid' => $i, 'menuname' => $menus[$i]];
+ }, array_unique(array_filter($item['menus'], 'is_numeric')));
+ $item['locationCount'] = count($item['locations']);
+ $item['menuCount'] = count($item['menus']);
+ $item['entryCount'] = count($item['entryids']);
+ }
+ return $return;
+ }
+
+ /**
+ * Take a configured versionid from a bootentry (serversetup module) and translate
+ * it, in case it's "default" or just a branch name.
+ */
+ private static function resolveEntryId(string $id): string
+ {
+ if ($id === 'default') { // Special case
+ $id = Property::get(MiniLinux::PROPERTY_DEFAULT_BOOT_EFFECTIVE);
+ }
+ if (substr_count($id, '/') < 2) {
+ // Maybe this is a branchid, which means latest from according branch (installed only)
+ $res = Database::queryFirst('SELECT versionid FROM minilinux_version WHERE branchid = :id AND installed = :ok
+ ORDER BY dateline DESC LIMIT 1',
+ ['id' => $id, 'ok' => self::INSTALL_OK]);
+ if ($res !== false) {
+ $id = $res['versionid'];
+ }
+ }
+ return $id;
+ }
+
+}