PL
games/xencenter-xenadmin-xcp-jumphp / src / Feature / Pools / HostLinks.php · commit fb1b7c9
<?phpnamespace App\Feature\Pools;use App\Model\Connection;use App\Xapi\Fake\FakeHostTransport;use App\Xapi\Rrd;use App\Xapi\StreamTransport;use App\Xapi\TlsStream;use App\Xapi\Transport;use App\Xapi\XapiClient;use JumPHP\Kernel\Engine;/** * The roads of one connection to the hosts of its pool other than the master. Each XAPI host only has the performance figures of * itself and of the VMs it runs (/rrd_updates?host=true), so the Performance and Search tabs ask each host, at the address * HostAddresses::reachable() gives, by the connection's own method (HTTP, HTTPS, an SSH tunnel through the master: `ssh -W * <host>:80`, or the demo pool). The pool's session is valid on every member. One transport per host, closed on disconnect. */final class HostLinks{    /** @var array<string, array{address: string, transport: Transport}> host ref => its road */    private array $links = [];    /** @var array<string, Rrd> host ref => its last figures */    private array $rrd = [];    /** @var array<string, bool> a request running */    private array $busy = [];    private ?Rrd $master = null;    /** Asks every other host of the pool for its figures; $changed runs when an answer arrived. */    public function fetch(Connection $c, \Closure $changed): void    {        $session = $c->client?->session;        $master = (string)$c->cache->master();        if ($session === null || !$c->isConnected()) {            return;        }        foreach (array_keys($c->cache->hosts()) as $host) {            if ($host === $master || !empty($this->busy[$host])) {                continue;            }            try {                $transport = $this->transport($c, $host);            } catch (\Throwable) {                continue;            }            $client = new XapiClient($transport);            $client->session = $session;            $this->busy[$host] = true;            $client->rrdUpdates(time() - 600, true)->then(function (Rrd $rrd) use ($host, $changed): void {                $this->busy[$host] = false;                $this->rrd[$host] = $rrd;                $changed();            }, function (\Throwable $e) use ($host): void {                $this->busy[$host] = false;            });        }    }    /** The master's figures (when given) and every host's, in one: the columns hold uuids, they do not overlap. */    public function merged(?Rrd $master = null): Rrd    {        if ($master !== null) {            $this->master = $master;        }        $m = $this->master;        $all = $m === null ? new Rrd() : new Rrd($m->columns, $m->end, $m->step);        foreach ($this->rrd as $rrd) {            $all->columns += $rrd->columns;            $all->end = max($all->end, $rrd->end);        }        return $all;    }    /** @return array<string, string> host ref => the address its figures come from (tests, the status) */    public function addresses(): array    {        return array_map(fn (array $l) => $l['address'], $this->links);    }    /** The road to a host: made once, again when its address changed. */    public function transport(Connection $c, string $host): Transport    {        $address = HostAddresses::reachable($c, $host);        if (isset($this->links[$host]) && $this->links[$host]['address'] === $address) {            return $this->links[$host]['transport'];        }        if (isset($this->links[$host])) {            $this->links[$host]['transport']->close();        }        $p = $c->profile;        $engine = Engine::get();        $transport = match ($p->method) {            'demo' => new FakeHostTransport($c->fake() ?? throw new \RuntimeException('No demo pool.'), (string)$c->cache->field($host, 'uuid', ''), $address),            'https' => TlsStream::available() ? StreamTransport::tls($address, $p->port()) : throw new \RuntimeException('No TLS here.'),            'ssh' => StreamTransport::tcp($engine->ssh()->via(array_filter(['sshHost' => $p->host, 'sshPort' => $p->sshPort, 'sshUser' => $p->sshUser,                'sshKey' => $p->sshKey !== '' ? $p->sshKey : null, 'sshPassword' => $p->sshKey === '' ? $p->password : null,                'sshKeys' => $engine->sshKeyring()->all() ?: null], fn ($v) => $v !== null && $v !== '')), $address, $p->port(), "SSH {$p->host} → $address"),            default => StreamTransport::tcp($engine->tcp(), $address, $p->port(), "HTTP $address:{$p->port()}"),        };        $this->links[$host] = ['address' => $address, 'transport' => $transport];        return $transport;    }    public function poll(float $dt): void    {        foreach ($this->links as $link) {            $link['transport']->poll($dt);        }    }    /** The roads close (disconnect, addresses changed); the figures go with them. */    public function close(): void    {        foreach ($this->links as $link) {            $link['transport']->close();        }        $this->links = [];        $this->rrd = [];        $this->busy = [];        $this->master = null;    }}