Anmelden
DE
<?phpnamespace App\Feature\Updates;use App\Model\Chain;use App\Model\Connection;use JumPHP\Async\Task;/** * XCP-ng's updates, per server: XCP-ng 8.x gives yum's updates through the host plugin updater.py, *   host.call_plugin($host, 'updater.py', 'check_update', {})  -> a JSON list of packages {name, version, release, description, changelog…} *   host.call_plugin($host, 'updater.py', 'update', {})        -> installs them (long: Async.host.call_plugin, a task in Events) * After a new kernel, Xen or XAPI the server needs a reboot: "Reboot required" until its boot_time changes. What was found is kept * here (per connection, per host), the Updates tab and the tree show it. */final class Updates{    /** packages whose update only takes effect after a reboot of the server */    public const REBOOT_PACKAGES = '/^(kernel|xen-|xapi|microcode|linux-firmware)/';    /** @var \WeakMap<Connection, array<string, HostUpdates>>|null */    private static ?\WeakMap $state = null;    public static function of(Connection $c, string $host): HostUpdates    {        self::$state ??= new \WeakMap();        $hosts = self::$state[$c] ?? [];        $hosts[$host] ??= new HostUpdates();        self::$state[$c] = $hosts;        return $hosts[$host];    }    /** Asks every server of the connection ($host: one), one after the other (a failure of one does not stop the others). */    public static function check(Connection $c, ?string $host = null): Task    {        $steps = [];        foreach ($host !== null ? [$host] : array_keys($c->cache->hosts()) as $h) {            $state = self::of($c, $h);            $state->checking = true;            $state->error = '';            $steps[] = function () use ($c, $h): Task {                $out = new Task();                self::checkOne($c, $h)->then(fn () => $out->settle(true), fn () => $out->settle(true));                return $out;            };        }        return Chain::run($steps);    }    private static function checkOne(Connection $c, string $host): Task    {        $done = new Task();        $state = self::of($c, $host);        $state->checking = true;        $c->run("Checking for updates on '" . $c->cache->name($host) . "'", 'host.call_plugin', [$host, 'updater.py', 'check_update', []], $host)->then(            function (mixed $result) use ($state, $done, $c): void {                $state->checking = false;                $state->packages = self::parse((string)$result);                $state->checkedAt = $c->cache->version;                $done->settle(count($state->packages));            },            function (\Throwable $e) use ($state, $done): void {                $state->checking = false;                $state->error = str_contains($e->getMessage(), 'XENAPI_MISSING_PLUGIN')                    ? 'This server has no updater.py plugin (it is not XCP-ng 8.x): update it with its own tools.' : $e->getMessage();                $done->settle(null, $e);            },        );        return $done;    }    /** Installs the updates of one server; Task of true when it now needs a reboot. */    public static function install(Connection $c, string $host): Task    {        $done = new Task();        $state = self::of($c, $host);        $packages = $state->packages ?? [];        $state->installing = true;        $c->run("Installing " . count($packages) . " updates on '" . $c->cache->name($host) . "'", 'host.call_plugin', [$host, 'updater.py', 'update', []], $host)->then(            function () use ($c, $host, $state, $packages, $done): void {                $state->installing = false;                $reboot = self::needsReboot($packages);                if ($reboot) {                    $state->rebootBoot = (string)($c->cache->field($host, 'other_config', [])['boot_time'] ?? '0');                    $state->rebootFor = array_values(array_filter(array_column($packages, 'name'), fn ($n) => preg_match(self::REBOOT_PACKAGES, (string)$n)));                }                $state->packages = null;                self::checkOne($c, $host)->then(fn () => null, fn () => null);        // what is left (usually nothing)                $done->settle($reboot);            },            function (\Throwable $e) use ($state, $done): void {                $state->installing = false;                $state->error = $e->getMessage();                $done->settle(null, $e);            },        );        return $done;    }    /** @param list<array<string, mixed>> $packages */    public static function needsReboot(array $packages): bool    {        foreach ($packages as $p) {            if (preg_match(self::REBOOT_PACKAGES, (string)($p['name'] ?? ''))) {                return true;            }        }        return false;    }    /**     * The answer of check_update: a JSON list (or a map name => package, as some versions give). As a task's result it comes wrapped     * as an XML-RPC value (<value>…</value>, XML-escaped).     *     * @return list<array<string, mixed>>     */    public static function parse(string $result): array    {        $text = trim($result);        if (preg_match('#^<value>(?:<string>)?(.*?)(?:</string>)?</value>$#s', $text, $m)) {            $text = html_entity_decode($m[1], ENT_QUOTES | ENT_XML1);        }        $data = json_decode($text, true);        if (!is_array($data)) {            return [];        }        $out = [];        foreach ($data as $key => $p) {            if (!is_array($p)) {                continue;            }            $p['name'] = (string)($p['name'] ?? (is_string($key) ? $key : ''));            if ($p['name'] !== '') {                $out[] = $p;            }        }        usort($out, fn ($a, $b) => strcmp($a['name'], $b['name']));        return $out;    }    /** "1.249.32-1.1.xcpng8.3": version and release. @param array<string, mixed> $p */    public static function version(array $p, string $prefix = ''): string    {        $v = (string)($p[$prefix . 'version'] ?? '');        $r = (string)($p[$prefix . 'release'] ?? '');        return $v === '' ? '' : $v . ($r !== '' ? '-' . $r : '');    }    /** The packages to update on a server (0: none, or never asked). */    public static function pending(Connection $c, string $host): int    {        return count(self::of($c, $host)->packages ?? []);    }    /** Servers with updates waiting or a reboot to do get a word after their name in the tree. @param list<array<string, mixed>> $nodes */    public static function markTree(array $nodes, \App\Model\Session $session): array    {        $marks = [];        foreach ($session->connections as $c) {            foreach (self::$state[$c] ?? [] as $host => $state) {                if ($state->rebootRequired($c, $host)) {                    $marks[$host] = ' (reboot required)';                } elseif (($n = count($state->packages ?? [])) > 0) {                    $marks[$host] = " ($n " . ($n === 1 ? 'update' : 'updates') . ')';                }            }        }        if ($marks === []) {            return $nodes;        }        $walk = function (array $nodes) use (&$walk, $marks): array {            foreach ($nodes as &$node) {                if (isset($marks[$node['id'] ?? ''])) {                    $node['suffix'] = ($node['suffix'] ?? '') . $marks[$node['id']];                }                if (!empty($node['children'])) {                    $node['children'] = $walk($node['children']);                }            }            return $nodes;        };        return $walk($nodes);    }    /** Forgets everything (tests). */    public static function reset(): void    {        self::$state = null;    }}