<?phpnamespace App\Feature\Pools;use App\Form\HostAddressesForm;use App\Form\MainForm;use App\Form\NewPoolForm;use App\Model\Chain;use App\Model\Connection;use App\Model\LogEntry;use App\Model\Session;use JumPHP\Async\Task;use JumPHP\Kernel\Engine;/** * What the pool commands do, with XAPI's calls as XenCenter makes them: * - New Pool: pool.set_name_label (and _description) on the chosen master's own pool, then each other server joins it; * - a join: Async.pool.join(master_address, user, password) called ON THE JOINING SERVER, through its own connection; once done, * that connection is left (the server answers HOST_IS_SLAVE now: it shows under the pool); * - Remove Server from Pool: Async.pool.eject(host) on the pool; Make this server the pool master: Async.pool.designate_new_master. */final class PoolActions{ /** A command of PoolCommands: a dialog to open ([class, params]), or null when it ran. @return array{0: class-string, 1: array<string, mixed>}|null */ public static function command(MainForm $form, string $id, ?Connection $tc, string $target): ?array { $session = $form->session; if ($id === 'pool.new') { if (PoolCommands::standalone($session) === []) { $session->log('New Pool', 'XenCenter', null, LogEntry::FAILED, 'Connect a server that is in no pool first.'); return null; } return [NewPoolForm::class, NewPoolForm::paramsForNew($session)]; } if ($tc === null || !$tc->isConnected()) { return null; } $name = $tc->cache->name($target); switch (true) { case $id === 'pool.addServer': return [NewPoolForm::class, NewPoolForm::paramsForAdd($session, $tc)]; case $id === 'pool.addresses': return [HostAddressesForm::class, HostAddressesForm::paramsFor($tc)]; case str_starts_with($id, 'pool.joinTo:'): $pool = $session->connections[(int)substr($id, 12)] ?? null; if ($pool !== null) { self::join($form, $pool, $tc)->then(fn () => null, fn () => null); } return null; case $id === 'host.removeFromPool': $tc->run("Removing server '$name' from pool '" . $tc->name() . "'", 'pool.eject', [$target], $target)->then(fn () => null, fn () => null); return null; case $id === 'host.designateMaster': $tc->run("Making '$name' the master of pool '" . $tc->name() . "'", 'pool.designate_new_master', [$target], (string)$tc->cache->poolRef())->then(function () use ($form, $tc): void { if ($tc->profile->method !== 'demo') { // the old master answers HOST_IS_SLAVE now: connecting again follows it to the new one Engine::get()->gameLoop->addTimer(8.0, fn () => $form->connect($tc)); } }, fn () => null); return null; } return null; } /** What NewPoolForm and HostAddressesForm answered: true when it was theirs. */ public static function resume(MainForm $form, array $result): bool { $session = $form->session; if (isset($result['newPool'])) { $p = (array)$result['newPool']; $master = self::byKey($session, (string)$p['master']); if ($master !== null) { self::create($form, $master, (string)$p['name'], (string)($p['description'] ?? ''), self::members($session, (array)$p['members'])); } return true; } if (isset($result['joinPool'])) { $p = (array)$result['joinPool']; $pool = self::byKey($session, (string)$p['pool']); if ($pool !== null) { Chain::run(array_map(fn (Connection $s) => fn () => self::join($form, $pool, $s), self::members($session, (array)$p['members'])))->then(fn () => null, fn () => null); } return true; } if (isset($result['hostAddresses'])) { $p = (array)$result['hostAddresses']; $c = self::byKey($session, (string)$p['connection']); if ($c !== null) { $c->profile->hostAddresses = HostAddresses::clean((array)$p['addresses']); $c->hostLinks()->close(); // the roads to the hosts are made again at the new addresses $session->save(); $session->log('Connection addresses of ' . $c->name() . ' saved', $c->name()); $form->refreshFigures(); } return true; } return false; } /** New Pool: the master's pool gets its name, then the other servers join it one after the other. @param list<Connection> $members */ public static function create(MainForm $form, Connection $master, string $name, string $description, array $members): Task { $pool = (string)$master->cache->poolRef(); $steps = [fn () => $master->call('pool.set_name_label', $pool, $name)]; if (trim($description) !== '') { $steps[] = fn () => $master->call('pool.set_name_description', $pool, $description); } $steps[] = function () use ($form, $name): void { $form->session->log("Created pool '$name'", $name); }; foreach ($members as $s) { $steps[] = fn () => self::join($form, $master, $s, $name); } return Chain::run($steps)->then(fn () => null, fn () => null); } /** A server joins a pool: Async.pool.join on the server's own connection, given the master's address and credentials. */ public static function join(MainForm $form, Connection $pool, Connection $server, ?string $poolName = null): Task { $poolName ??= $pool->name(); $host = (string)$server->cache->master(); $serverName = $server->name(); $done = new Task(); $server->run("Adding server '$serverName' to pool '$poolName'", 'pool.join', [$pool->profile->host, $pool->profile->user, $pool->profile->password], $host)->then( function () use ($form, $server, $pool, $serverName, $poolName, $done): void { $form->session->log("Server '$serverName' joined pool '$poolName'", $poolName); // it is a pool member now: the pool's connection shows it, its own connection goes (as in XenCenter) $form->forgetConnection($server); $form->navigate((string)$pool->cache->poolRef()); $done->settle(true); }, fn (\Throwable $e) => $done->settle(null, $e), ); return $done; } private static function byKey(Session $session, string $key): ?Connection { foreach ($session->connections as $c) { if ($c->profile->key() === $key) { return $c; } } return null; } /** @param list<mixed> $keys @return list<Connection> */ private static function members(Session $session, array $keys): array { return array_values(array_filter(array_map(fn ($k) => self::byKey($session, (string)$k), $keys))); }}