ES
games/xencenter-xenadmin-xcp-jumphp / src / Xapi / XapiClient.php · commit fb1b7c9
<?phpnamespace App\Xapi;use JumPHP\Async\Task;/** * The XAPI client: JSON-RPC 2.0 on POST /jsonrpc (what XenCenter does with XML-RPC), a session made by * session.login_with_password, inserted as the first parameter of every other call. * *   $client->login('root', $password)->then(fn (string $session) => ...); *   $client->call('VM.get_all_records')->then(fn (array $records) => ...); *   $client->call('Async.VM.start', $vmRef, false, false)->then(fn (string $taskRef) => ...); *   $client->events(['vm', 'host'], $token, 30.0)->then(fn (array $batch) => ...);     // event.from: what changed since $token *   $client->rrdUpdates(time() - 600, true, null)->then(fn (Rrd $rrd) => ...); */final class XapiClient{    public const API_VERSION = '2.21';    public const ORIGINATOR = 'XenCenter-JumPHP';    public ?string $session = null;    private int $id = 0;    public function __construct(public readonly Transport $transport)    {    }    public function login(string $user, string $password): Task    {        $task = new Task();        $this->raw('session.login_with_password', [$user, $password, self::API_VERSION, self::ORIGINATOR])->then(            function (mixed $session) use ($task): void {                $this->session = (string)$session;                $task->settle($this->session);            },            fn (\Throwable $e) => $task->settle(null, $e),        );        return $task;    }    public function logout(): void    {        if ($this->session !== null) {            $this->raw('session.logout', [$this->session]);            $this->session = null;        }    }    /** A call with the session first: call('VM.clean_shutdown', $ref). */    public function call(string $method, mixed ...$params): Task    {        if ($this->session === null) {            $task = new Task();            $task->settle(null, new XapiError(['SESSION_INVALID']));            return $task;        }        return $this->raw($method, [$this->session, ...$params]);    }    /** @param list<string> $classes */    public function events(array $classes, string $token, float $timeout): Task    {        return $this->raw('event.from', [(string)$this->session, $classes, $token, $timeout], 'events');    }    /** /rrd_updates: the performance figures of the last seconds (host and its VMs, or one VM). */    public function rrdUpdates(int $start, bool $host, ?string $vmUuid = null, int $interval = 5): Task    {        $task = new Task();        $query = http_build_query(array_filter(['session_id' => $this->session, 'start' => $start, 'cf' => 'AVERAGE', 'interval' => $interval,            'host' => $host ? 'true' : null, 'vm_uuid' => $vmUuid], fn ($v) => $v !== null));        $this->transport->request('GET', '/rrd_updates?' . $query, '', [], 'rrd')->then(            fn (HttpReply $reply) => $reply->ok()                ? $task->settle(Rrd::parse($reply->body))                : $task->settle(null, XapiError::transport("rrd_updates answered HTTP {$reply->status}")),            fn (\Throwable $e) => $task->settle(null, $e),        );        return $task;    }    /** @param list<mixed> $params */    public function raw(string $method, array $params, string $channel = 'calls'): Task    {        $task = new Task();        $body = json_encode(['jsonrpc' => '2.0', 'method' => $method, 'params' => $params, 'id' => ++$this->id],            JSON_UNESCAPED_SLASHES);        $this->transport->request('POST', '/jsonrpc', (string)$body, ['Content-Type' => 'application/json'], $channel)->then(            function (HttpReply $reply) use ($task, $method): void {                $task->settle(...self::decode($reply, $method));            },            fn (\Throwable $e) => $task->settle(null, $e instanceof XapiError ? $e : XapiError::transport($e->getMessage())),        );        return $task;    }    /** @return array{0: mixed, 1: ?\Throwable} */    public static function decode(HttpReply $reply, string $method): array    {        if ($reply->status === 404 || $reply->status === 405) {            return [null, XapiError::transport("This server has no JSON-RPC API (HTTP {$reply->status}): XAPI 1.290 / XenServer 7.3 or newer is needed.")];        }        $data = json_decode($reply->body, true);        if (!is_array($data)) {            return [null, XapiError::transport("Unexpected answer to $method (HTTP {$reply->status}).")];        }        if (isset($data['error'])) {            $error = $data['error'];            $failure = is_array($error['data'] ?? null) ? array_map('strval', array_values($error['data']))                : [(string)($error['message'] ?? 'INTERNAL_ERROR')];            return [null, new XapiError($failure)];        }        return [$data['result'] ?? null, null];    }}