PL
games/xencenter-xenadmin-xcp-jumphp / src / Xapi / FakeXapi.php · commit fb1b7c9
<?phpnamespace App\Xapi;use JumPHP\Async\Task;/** * A XAPI server in memory: a demo pool of XCP-ng 8.3 (two hosts, VMs, templates, local and shared storage, networks), answering * JSON-RPC like the real one (records, Async.* tasks that progress, event.from, /rrd_updates). The "Demo pool" of the Add Server * dialog, the tests and the screenshots use it: no server needed. Answers come in the next poll() (never at once, as on a network). */final class FakeXapi implements Transport{    use Fake\PoolOps;          // pools: other demo servers, pool.join / eject / designate_new_master    // XCP-ng updates (updater.py), XVA export / import (HTTP handlers), perfmon alarms: src/Xapi/Fake    use Fake\UpdatesOps;    use Fake\XvaOps;    use Fake\AlertsOps;    // storage, networks, cross-pool migration and the second demo pool: src/Xapi/Fake/    use Fake\StorageOps, Fake\NetworkOps, Fake\MigrationOps;    public const USER = 'root';    public const PASSWORD = 'demo';    /** @var array<string, array<string, array<string, mixed>>> class (lower case) => ref => record */    public array $db = [];    /** @var list<array{id: int, class: string, operation: string, ref: string}> */    private array $log = [];    private int $eventId = 0;    private int $nextRef = 1;    /** @var list<array{0: float, 1: Task, 2: HttpReply}> answers to give at a time */    private array $outbox = [];    /** @var list<array{task: Task, token: int, classes: list<string>, until: float, id: mixed}> */    private array $waiting = [];    /** @var list<array{task: string, left: float, total: float, apply: \Closure}> running Async operations */    private array $jobs = [];    private float $clock = 0;    private float $metricsIn = 5;    private int $seed = 7;    private int $now;    /** @var list<string> calls received (tests) */    public array $calls = [];    /** @var array<string, bool> sessions */    private array $sessions = [];    public float $latency = 0.0;    /** an operation fails with this failure (tests): method => failure */    public array $failures = [];    public function __construct(public string $address = 'xcp-demo.local')    {        $this->now = time();        $this->buildFor();          // the demo pool, "Demo pool 2" or a standalone host (PoolOps, MigrationOps)        $this->buildUpdates();        $this->buildAlerts();    }    public function describe(): string    {        return 'Demo pool (in memory)';    }    public function close(): void    {        foreach ($this->waiting as $wait) {            $wait['task']->settle(null, XapiError::transport('Disconnected.'));        }        $this->waiting = [];    }    // transport -------------------------------------------------------------------------------------------------------    public function request(string $method, string $path, string $body = '', array $headers = [], string $channel = 'calls'): Task    {        $task = new Task();        if ($method === 'GET' && str_starts_with($path, '/rrd_updates')) {            parse_str((string)parse_url($path, PHP_URL_QUERY), $query);            $ok = isset($this->sessions[(string)($query['session_id'] ?? '')]);            $this->outbox[] = [$this->clock + $this->latency, $task, $ok                ? new HttpReply(200, ['content-type' => 'text/xml'], $this->rrdXml((int)($query['start'] ?? 0), !empty($query['host']), $query['vm_uuid'] ?? null, $query['xc_host'] ?? null))                : new HttpReply(401, [], 'unauthorised')];            return $task;        }        $data = json_decode($body, true);        if ($path !== '/jsonrpc' || !is_array($data)) {            $this->outbox[] = [$this->clock, $task, new HttpReply(404, [], '')];            return $task;        }        $id = $data['id'] ?? null;        $name = (string)($data['method'] ?? '');        $params = (array)($data['params'] ?? []);        $this->calls[] = $name;        if ($name === 'event.from') {            if (!isset($this->sessions[(string)($params[0] ?? '')])) {                $this->reply($task, $id, null, ['SESSION_INVALID', (string)($params[0] ?? '')]);                return $task;            }            $this->waiting[] = ['task' => $task, 'token' => (int)($params[2] ?? 0) ?: -1, 'classes' => array_map('strtolower', (array)($params[1] ?? [])),                'until' => $this->clock + min(30.0, (float)($params[3] ?? 30)), 'id' => $id];            if ((string)($params[2] ?? '') === '') {                $this->waiting[array_key_last($this->waiting)]['token'] = -1;            }            return $task;        }        try {            $result = $this->dispatch($name, $params);            $this->reply($task, $id, $result, null);        } catch (XapiError $e) {            $this->reply($task, $id, null, $e->failure);        }        return $task;    }    private function reply(Task $task, mixed $id, mixed $result, ?array $failure): void    {        $body = $failure === null ? ['jsonrpc' => '2.0', 'result' => $result, 'id' => $id]            : ['jsonrpc' => '2.0', 'error' => ['code' => 1, 'message' => $failure[0], 'data' => $failure], 'id' => $id];        $this->outbox[] = [$this->clock + $this->latency, $task, new HttpReply(200, ['content-type' => 'application/json'], (string)json_encode($body))];    }    public function poll(float $dt): void    {        $this->clock += $dt;        $this->tickPool($dt);        foreach ($this->vncs as $i => $vnc) {            $vnc->poll($dt);            if ($vnc->isClosed()) {                unset($this->vncs[$i]);            }        }        $this->vncs = array_values($this->vncs);        $this->pollXva($dt);        $this->pollAlerts($dt);        foreach ($this->jobs as $i => &$job) {            $job['left'] -= $dt;            $progress = max(0.0, min(0.99, 1 - $job['left'] / $job['total']));            if ($job['left'] > 0) {                if (abs(($this->db['task'][$job['task']]['progress'] ?? 0) - $progress) > 0.09) {                    $this->set('task', $job['task'], ['progress' => round($progress, 2)]);                }                continue;            }            unset($this->jobs[$i]);            try {                $result = ($job['apply'])();                $this->set('task', $job['task'], ['status' => 'success', 'progress' => 1.0, 'finished' => $this->date(), 'result' => is_string($result) ? $result : '']);            } catch (XapiError $e) {                $this->set('task', $job['task'], ['status' => 'failure', 'progress' => 1.0, 'finished' => $this->date(), 'error_info' => $e->failure]);            }        }        unset($job);        $this->jobs = array_values($this->jobs);        $this->metricsIn -= $dt;        if ($this->metricsIn <= 0) {            $this->metricsIn = 5;            $this->tickMetrics();        }        foreach ($this->waiting as $i => $wait) {            $events = $this->eventsSince($wait['token'], $wait['classes']);            if ($events !== [] || $wait['token'] === -1 || $this->clock >= $wait['until']) {                unset($this->waiting[$i]);                $this->reply($wait['task'], $wait['id'], ['events' => $events, 'valid_ref_counts' => [], 'token' => (string)$this->eventId], null);            }        }        $this->waiting = array_values($this->waiting);        $due = [];        foreach ($this->outbox as $i => $out) {            if ($out[0] <= $this->clock) {                $due[] = $out;                unset($this->outbox[$i]);            }        }        $this->outbox = array_values($this->outbox);        foreach ($due as [, $task, $reply]) {            $task->settle($reply);        }    }    /** @param list<string> $classes */    private function eventsSince(int $token, array $classes): array    {        $all = in_array('*', $classes, true);        $events = [];        if ($token === -1) {            // the first call: every object, as "add"            foreach ($this->db as $class => $records) {                if ($all || in_array($class, $classes, true)) {                    foreach ($records as $ref => $record) {                        $events[] = ['id' => $this->eventId, 'class' => $class, 'operation' => 'add', 'ref' => $ref, 'snapshot' => $record];                    }                }            }            return $events;        }        $seen = [];        foreach ($this->log as $entry) {            if ($entry['id'] <= $token || !($all || in_array($entry['class'], $classes, true))) {                continue;            }            $key = $entry['class'] . $entry['ref'];            $snapshot = $this->db[$entry['class']][$entry['ref']] ?? null;            $seen[$key] = ['id' => $entry['id'], 'class' => $entry['class'], 'operation' => $snapshot === null ? 'del' : $entry['operation'],                'ref' => $entry['ref'], 'snapshot' => $snapshot ?? []];        }        return array_values($seen);    }    // the database ------------------------------------------------------------------------------------------------------    private function ref(): string    {        return sprintf('OpaqueRef:%08x-0000-4000-8000-%012x', $this->nextRef, $this->nextRef++);    }    /**     * (seed * 1103515245 + 12345) mod 2^31 without leaving 32-bit integers (the browser's PHP: the product became a float and     * every uuid warned "not representable as an int"). Halves of 16 bits; the sums stay below 2^53, exact as floats.     */    private static function lcg(int $seed): int    {        $lo = $seed & 0xffff;        $hi = ($seed >> 16) & 0x7fff;        $cross = fmod((float)$hi * 0x4e6d + (float)$lo * 0x41c6, 32768.0);    // 1103515245 = 0x41c64e6d        return (int)fmod($cross * 65536.0 + (float)$lo * 0x4e6d + 12345.0, 2147483648.0);    }    private function uuid(): string    {        $a = $this->seed = self::lcg($this->seed);        $this->seed = self::lcg($this->seed);        return sprintf('%08x-%04x-4%03x-a%03x-%08x%04x', $a, $this->seed & 0xffff, $a & 0xfff, $this->seed & 0xfff, $this->seed, $a & 0xffff);    }    private function date(int $offset = 0): string    {        return gmdate('Ymd\TH:i:s\Z', $this->now + (int)$this->clock + $offset);    }    /** @param array<string, mixed> $record */    private function add(string $class, array $record, ?string $ref = null): string    {        $ref ??= $this->ref();        $record += ['uuid' => $this->uuid(), 'other_config' => [], 'tags' => []];        $this->db[$class][$ref] = $record;        $this->log[] = ['id' => ++$this->eventId, 'class' => $class, 'operation' => 'add', 'ref' => $ref];        return $ref;    }    /** @param array<string, mixed> $fields */    private function set(string $class, string $ref, array $fields): void    {        if (!isset($this->db[$class][$ref])) {            return;        }        $this->db[$class][$ref] = $fields + $this->db[$class][$ref];        $this->log[] = ['id' => ++$this->eventId, 'class' => $class, 'operation' => 'mod', 'ref' => $ref];        if (count($this->log) > 4000) {            $this->log = array_slice($this->log, -2000);        }    }    private function delete(string $class, string $ref): void    {        unset($this->db[$class][$ref]);        $this->log[] = ['id' => ++$this->eventId, 'class' => $class, 'operation' => 'del', 'ref' => $ref];    }    private function append(string $class, string $ref, string $field, string $value): void    {        $list = (array)($this->db[$class][$ref][$field] ?? []);        $list[] = $value;        $this->set($class, $ref, [$field => $list]);    }    private function removeFrom(string $class, string $ref, string $field, string $value): void    {        $list = array_values(array_filter((array)($this->db[$class][$ref][$field] ?? []), fn ($v) => $v !== $value));        $this->set($class, $ref, [$field => $list]);    }    /** @return array<string, mixed> */    private function record(string $class, string $ref): array    {        if (!isset($this->db[$class][$ref])) {            throw new XapiError(['HANDLE_INVALID', strtoupper($class), $ref]);        }        return $this->db[$class][$ref];    }    private const GIB = 1073741824;    private const MIB = 1048576;    private function build(): void    {        $pool = $this->ref();        $hosts = [];        $networks = [];        $net0 = $this->add('network', ['name_label' => 'Pool-wide network associated with eth0', 'name_description' => '', 'bridge' => 'xenbr0', 'MTU' => 1500,            'PIFs' => [], 'VIFs' => [], 'other_config' => []]);        $net1 = $this->add('network', ['name_label' => 'Storage (eth1)', 'name_description' => 'iSCSI / NFS traffic', 'bridge' => 'xenbr1', 'MTU' => 9000, 'PIFs' => [], 'VIFs' => []]);        $vlan = $this->add('network', ['name_label' => 'DMZ (VLAN 20)', 'name_description' => 'Public services', 'bridge' => 'xapi1', 'MTU' => 1500, 'PIFs' => [], 'VIFs' => []]);        $internal = $this->add('network', ['name_label' => 'Host internal management network', 'name_description' => 'Network on which guests will be assigned a private link-local IP address',            'bridge' => 'xenapi', 'MTU' => 1500, 'PIFs' => [], 'VIFs' => [], 'other_config' => ['is_host_internal_management_network' => 'true']]);        $networks = [$net0, $net1, $vlan];        $nfs = $this->add('sr', ['name_label' => 'NFS storage', 'name_description' => 'NFS SR [192.168.10.20:/export/xcp]', 'type' => 'nfs', 'content_type' => 'user',            'shared' => true, 'physical_size' => 4000 * self::GIB, 'physical_utilisation' => 1310 * self::GIB, 'virtual_allocation' => 1900 * self::GIB, 'PBDs' => [], 'VDIs' => []]);        $iso = $this->add('sr', ['name_label' => 'ISO library', 'name_description' => 'CIFS ISO library [\\\\nas\\iso]', 'type' => 'iso', 'content_type' => 'iso',            'shared' => true, 'physical_size' => 500 * self::GIB, 'physical_utilisation' => 96 * self::GIB, 'virtual_allocation' => 96 * self::GIB, 'PBDs' => [], 'VDIs' => []]);        $tools = $this->add('sr', ['name_label' => 'XCP-ng Tools', 'name_description' => 'XCP-ng Tools ISOs', 'type' => 'udev', 'content_type' => 'iso',            'shared' => true, 'physical_size' => 0, 'physical_utilisation' => 0, 'virtual_allocation' => 0, 'PBDs' => [], 'VDIs' => [], 'other_config' => ['xenserver_tools_sr' => 'true']]);        $local = [];        foreach ([['xcp-ng-01', '192.168.1.11', 24, 128], ['xcp-ng-02', '192.168.1.12', 16, 96]] as $n => [$name, $ip, $cpus, $mem]) {            $metrics = $this->add('host_metrics', ['memory_total' => $mem * self::GIB, 'memory_free' => floor($mem * 0.55 * self::GIB), 'live' => true, 'last_updated' => $this->date()]);            $host = $this->add('host', ['name_label' => $name, 'name_description' => $n === 0 ? 'Rack A, unit 12' : 'Rack A, unit 14', 'hostname' => $name, 'address' => $ip, 'enabled' => true,                'metrics' => $metrics, 'resident_VMs' => [], 'PIFs' => [], 'PBDs' => [], 'memory_overhead' => 412 * self::MIB, 'API_version_major' => 2, 'API_version_minor' => 21,                'cpu_info' => ['cpu_count' => (string)$cpus, 'socket_count' => '2', 'vendor' => 'GenuineIntel', 'speed' => '2394.454', 'modelname' => 'Intel(R) Xeon(R) Silver 4214R CPU @ 2.40GHz', 'family' => '6', 'model' => '85', 'stepping' => '7', 'flags' => 'fpu vme de pse tsc msr pae mce cx8 apic sep mtrr', 'features' => '', 'features_pv' => '', 'features_hvm' => ''],                'software_version' => ['product_brand' => 'XCP-ng', 'product_version' => '8.3.0', 'product_version_text' => '8.3', 'platform_name' => 'XCP', 'platform_version' => '3.4.0', 'build_number' => 'release/yangtze/master/58', 'xapi' => '24.19', 'xen' => '4.17.4-3', 'linux' => '4.19.0+1', 'hostname' => $name, 'date' => '2024-09-30'],                'bios_strings' => ['bios-vendor' => 'Dell Inc.', 'system-manufacturer' => 'Dell Inc.', 'system-product-name' => 'PowerEdge R640', 'system-serial-number' => 'CN7' . (4120 + $n)],                'other_config' => ['iscsi_iqn' => "iqn.2024-01.local.demo:$name", 'agent_start_time' => (string)($this->now - 86400 * (12 + $n)), 'boot_time' => (string)($this->now - 86400 * (41 + $n * 9))],                'iscsi_iqn' => "iqn.2024-01.local.demo:$name", 'license_params' => ['sku_type' => 'free'], 'edition' => 'xcp-ng', 'logging' => [], 'suspend_image_sr' => $nfs, 'crash_dump_sr' => $nfs,                'external_auth_type' => '', 'ssl_legacy' => false, 'tls_verification_enabled' => true, 'chipset_info' => ['iommu' => 'true'], 'control_domain' => '']);            $hosts[] = $host;            foreach ([[$net0, 'eth0', true, $ip, '00:1b:21:3a:0' . $n . ':10', 10000, -1], [$net1, 'eth1', false, '10.0.10.1' . (1 + $n), '00:1b:21:3a:0' . $n . ':11', 10000, -1],                [$vlan, 'eth0', false, '', '00:1b:21:3a:0' . $n . ':10', 10000, 20]] as [$net, $dev, $mgmt, $addr, $mac, $speed, $vlanTag]) {                $pm = $this->add('pif_metrics', ['speed' => $speed, 'duplex' => true, 'carrier' => true, 'vendor_name' => 'Intel Corporation', 'device_name' => 'Ethernet Controller X710 for 10GbE SFP+', 'pci_bus_path' => '0000:3b:00.' . ($dev === 'eth0' ? 0 : 1)]);                $pif = $this->add('pif', ['device' => $dev, 'host' => $host, 'network' => $net, 'MAC' => $mac, 'MTU' => $net === $net1 ? 9000 : 1500, 'VLAN' => $vlanTag, 'IP' => $addr,                    'netmask' => $addr === '' ? '' : '255.255.255.0', 'gateway' => $mgmt ? '192.168.1.1' : '', 'DNS' => $mgmt ? '192.168.1.1' : '', 'ip_configuration_mode' => $addr === '' ? 'None' : 'Static',                    'management' => $mgmt, 'currently_attached' => true, 'physical' => $vlanTag === -1, 'metrics' => $pm, 'bond_master_of' => [], 'bond_slave_of' => 'OpaqueRef:NULL']);                $this->append('host', $host, 'PIFs', $pif);                $this->append('network', $net, 'PIFs', $pif);            }            $lsr = $this->add('sr', ['name_label' => 'Local storage', 'name_description' => "Local storage on $name", 'type' => 'lvm', 'content_type' => 'user',                'shared' => false, 'physical_size' => 1786 * self::GIB, 'physical_utilisation' => (420 + 300 * $n) * self::GIB, 'virtual_allocation' => (600 + 300 * $n) * self::GIB,                'PBDs' => [], 'VDIs' => [], 'other_config' => ['i18n-key' => 'local-storage']]);            $local[] = $lsr;            foreach ([$lsr, $nfs, $iso, $tools] as $sr) {                $pbd = $this->add('pbd', ['host' => $host, 'SR' => $sr, 'currently_attached' => true, 'device_config' => $sr === $nfs ? ['server' => '192.168.10.20', 'serverpath' => '/export/xcp'] : ($sr === $lsr ? ['device' => '/dev/sda3'] : [])]);                $this->append('sr', $sr, 'PBDs', $pbd);                $this->append('host', $host, 'PBDs', $pbd);            }            // dom0            $this->addVm(['name_label' => "Control domain on host: $name", 'is_control_domain' => true, 'power_state' => 'Running', 'resident_on' => $host,                'memory' => 4 * self::GIB, 'vcpus' => 16, 'os' => 'XCP-ng 8.3']);        }        $this->add('pool', ['name_label' => 'Demo pool', 'name_description' => 'XCP-ng 8.3 lab', 'master' => $hosts[0], 'default_SR' => $nfs, 'suspend_image_SR' => $nfs,            'crash_dump_SR' => $nfs, 'ha_enabled' => false, 'ha_host_failures_to_tolerate' => 0, 'other_config' => [], 'tags' => [], 'wlb_enabled' => false,            'gui_config' => [], 'cpu_info' => ['cpu_count' => '40', 'socket_count' => '4', 'vendor' => 'GenuineIntel']], $pool);        foreach (['ubuntu-24.04-desktop-amd64.iso', 'debian-12.7.0-amd64-netinst.iso', 'Win11_24H2_English_x64.iso', 'AlmaLinux-9.4-x86_64-minimal.iso'] as $isoName) {            $this->addVdi($iso, $isoName, self::GIB * (2 + strlen($isoName) % 5), 'user', true);        }        $this->addVdi($tools, 'guest-tools.iso', 90 * self::MIB, 'user', true);        // templates        foreach (['Debian Bookworm 12', 'Ubuntu Noble Numbat 24.04', 'AlmaLinux 9', 'Rocky Linux 9', 'Windows Server 2022 (64-bit)', 'Windows 11', 'Other install media', 'CentOS Stream 9'] as $t) {            $windows = str_starts_with($t, 'Windows');            $this->addVm(['name_label' => $t, 'is_a_template' => true, 'default_template' => true, 'power_state' => 'Halted',                'memory' => ($windows ? 4 : 2) * self::GIB, 'vcpus' => 2, 'description' => $windows ? 'Clones of this template will automatically provision their storage when first booted.' : "Template for $t", 'disk' => $windows ? 64 : 10]);        }        $vms = [            ['web-01', 'Nginx front end', 'Running', 0, 4, 2, 'Debian GNU/Linux 12 (bookworm)', 40, [$net0, $vlan], true],            ['web-02', 'Nginx front end', 'Running', 1, 4, 2, 'Debian GNU/Linux 12 (bookworm)', 40, [$net0, $vlan], true],            ['db-primary', 'PostgreSQL 16 primary', 'Running', 0, 32, 8, 'Ubuntu 24.04.1 LTS', 500, [$net0, $net1], true],            ['db-replica', 'PostgreSQL 16 streaming replica', 'Running', 1, 32, 8, 'Ubuntu 24.04.1 LTS', 500, [$net0, $net1], false],            ['ad-dc01', 'Active Directory domain controller', 'Running', 0, 8, 4, 'Microsoft Windows Server 2022 Standard', 80, [$net0], true],            ['backup-srv', 'Backup proxy', 'Halted', 1, 8, 4, 'Rocky Linux 9.4 (Blue Onyx)', 120, [$net0, $net1], false],            ['gitlab', 'GitLab CE + runners', 'Running', 1, 16, 6, 'AlmaLinux 9.4 (Seafoam Ocelot)', 250, [$net0], true],            ['monitoring', 'Prometheus + Grafana', 'Suspended', 0, 6, 2, 'Debian GNU/Linux 12 (bookworm)', 60, [$net0], false],            ['test-win11', 'Test desktop', 'Halted', 0, 8, 4, '', 64, [$net0], false],            ['mail-gw', 'Postfix relay', 'Paused', 1, 2, 1, 'Debian GNU/Linux 12 (bookworm)', 20, [$vlan], false],        ];        foreach ($vms as $i => [$name, $desc, $state, $h, $memG, $vcpus, $os, $disk, $nets, $snap]) {            $vm = $this->addVm(['name_label' => $name, 'description' => $desc, 'power_state' => $state, 'resident_on' => $state === 'Halted' ? 'OpaqueRef:NULL' : $hosts[$h],                'affinity' => $hosts[$h], 'memory' => $memG * self::GIB, 'vcpus' => $vcpus, 'os' => $os, 'disk' => $disk, 'sr' => $i % 3 === 2 ? $local[$h] : $nfs, 'networks' => $nets, 'index' => $i,                'tags' => $i % 2 ? ['production'] : ['production', 'linux']]);            if ($snap) {                $this->snapshot($vm, 'before upgrade', -86400 * (3 + $i));            }        }        $this->organizeDemo($pool);        // a few alerts        foreach ([['HA_POOL_OVERCOMMITTED', 3, 'pool', 'The pool is overcommitted: not enough free memory to restart every protected VM.'],            ['ALARM', 3, 'host', 'Disk usage of /var/log on xcp-ng-01 is at 82%.'],            // what XAPI really writes for a performance alarm: several lines, the config as XML            ['ALARM', 2, 'host', "value: 0.953000\nconfig:\n<variable>\n\t<name value=\"cpu_usage\"/>\n\t<alarm_trigger_level value=\"0.9\"/>\n\t<alarm_trigger_period value=\"60\"/>\n\t<alarm_auto_inhibit_period value=\"3600\"/>\n</variable>\n"], ['UPDATES_FEATURE_EXPIRED', 4, 'pool', 'New updates are available for XCP-ng 8.3.']] as $k => [$mname, $prio, $cls, $text]) {            $obj = $cls === 'pool' ? $this->db['pool'][$pool]['uuid'] : $this->db['host'][$hosts[0]]['uuid'];            $this->add('message', ['name' => $mname, 'priority' => $prio, 'cls' => ucfirst($cls), 'obj_uuid' => $obj, 'timestamp' => $this->date(-3600 * (5 + $k * 7)), 'body' => $text]);        }        $this->log = [];        $this->eventId = 100;    }    /** @param array<string, mixed> $o */    private function addVm(array $o): string    {        $running = in_array($o['power_state'], ['Running', 'Paused'], true);        $metrics = $this->add('vm_metrics', ['memory_actual' => $running ? $o['memory'] : 0, 'VCPUs_number' => $running ? $o['vcpus'] : 0,            'start_time' => $running ? $this->date(-3600 * (20 + 7 * ($o['index'] ?? 1))) : '19700101T00:00:00Z', 'install_time' => $this->date(-86400 * 120), 'last_updated' => $this->date()]);        $guest = 'OpaqueRef:NULL';        if ($running && !empty($o['os']) && empty($o['is_control_domain'])) {            $guest = $this->add('vm_guest_metrics', ['os_version' => ['name' => $o['os'], 'distro' => strtolower(explode(' ', $o['os'])[0]), 'uname' => '6.1.0-25-amd64'],                'PV_drivers_version' => ['major' => '8', 'minor' => '4', 'micro' => '0', 'build' => '1'], 'PV_drivers_detected' => true, 'PV_drivers_up_to_date' => true,                'networks' => ['0/ip' => '192.168.1.' . (100 + ($o['index'] ?? 0)), '0/ipv4/0' => '192.168.1.' . (100 + ($o['index'] ?? 0)), '0/ipv6/0' => 'fe80::4c1a:' . dechex(40 + ($o['index'] ?? 0))],                'disks' => [], 'other' => ['feature-shutdown' => '1', 'feature-suspend' => '1'], 'live' => true, 'can_use_hotplug_vbd' => 'yes', 'can_use_hotplug_vif' => 'yes', 'last_updated' => $this->date()]);        }        $vm = $this->add('vm', ['name_label' => $o['name_label'], 'name_description' => $o['description'] ?? '', 'power_state' => $o['power_state'],            'is_a_template' => !empty($o['is_a_template']), 'is_default_template' => !empty($o['default_template']), 'is_a_snapshot' => false, 'is_control_domain' => !empty($o['is_control_domain']),            'resident_on' => $o['resident_on'] ?? 'OpaqueRef:NULL', 'affinity' => $o['affinity'] ?? 'OpaqueRef:NULL',            'memory_static_min' => 512 * self::MIB, 'memory_static_max' => $o['memory'], 'memory_dynamic_min' => $o['memory'], 'memory_dynamic_max' => $o['memory'], 'memory_target' => $o['memory'],            'VCPUs_max' => $o['vcpus'], 'VCPUs_at_startup' => $o['vcpus'], 'VCPUs_params' => [], 'metrics' => $metrics, 'guest_metrics' => $guest,            'VBDs' => [], 'VIFs' => [], 'snapshots' => [], 'snapshot_of' => 'OpaqueRef:NULL', 'snapshot_time' => '19700101T00:00:00Z', 'parent' => 'OpaqueRef:NULL',            'allowed_operations' => [], 'current_operations' => [], 'blocked_operations' => [], 'domain_type' => str_contains((string)($o['os'] ?? ''), 'Windows') || str_contains($o['name_label'], 'Windows') ? 'hvm' : 'pvh',            'HVM_boot_params' => ['order' => 'cd', 'firmware' => 'uefi'], 'HVM_boot_policy' => 'BIOS order', 'platform' => ['viridian' => 'true', 'secureboot' => 'false', 'device-model' => 'qemu-upstream-uefi'],            'other_config' => !empty($o['is_a_template']) ? ['default_template' => 'true', 'install-methods' => 'cdrom,nfs,http,ftp'] : ['base_template_name' => 'Debian Bookworm 12'],            'tags' => $o['tags'] ?? [], 'ha_restart_priority' => '', 'order' => 0, 'start_delay' => 0, 'shutdown_delay' => 0, 'user_version' => 1, 'domid' => $running ? 10 + ($o['index'] ?? 0) : -1,            'actions_after_crash' => 'restart', 'actions_after_shutdown' => 'destroy', 'actions_after_reboot' => 'restart', 'has_vendor_device' => false, 'reference_label' => '']);        if (!empty($o['resident_on']) && $running) {            $this->append('host', $o['resident_on'], 'resident_VMs', $vm);        }        if (empty($o['is_a_template'])) {            $this->addConsole($vm);        }        if (!empty($o['disk']) && empty($o['is_a_template']) && empty($o['is_control_domain'])) {            $vdi = $this->addVdi($o['sr'], $o['name_label'] . ' 0', $o['disk'] * self::GIB, 'system', false);            $this->addVbd($vm, $vdi, '0', 'Disk', 'xvda', $running);            $this->addVbd($vm, 'OpaqueRef:NULL', '3', 'CD', 'xvdd', $running);            foreach ($o['networks'] ?? [] as $k => $net) {                $this->addVif($vm, $net, (string)$k, $running, (int)($o['index'] ?? 0));            }        }        return $vm;    }    /** The VNC console of a VM: XAPI's location is the address of the host it runs on. */    private function addConsole(string $vm): void    {        $host = (string)($this->db['vm'][$vm]['resident_on'] ?? '');        $address = (string)($this->db['host'][$host]['address'] ?? $this->address);        $console = $this->add('console', ['protocol' => 'rfb', 'location' => "https://$address/console?uuid=" . $this->uuid(), 'VM' => $vm, 'other_config' => []]);        $this->set('vm', $vm, ['consoles' => [$console]]);    }    /** @var list<\App\Vnc\FakeVnc> */    private array $vncs = [];    /** A connection to a console (what CONNECT /console?ref=... opens on a real host). */    public function openConsole(string $consoleRef): \App\Vnc\FakeVnc    {        $vm = (string)($this->db['console'][$consoleRef]['VM'] ?? '');        $record = $this->db['vm'][$vm] ?? [];        $guest = $this->db['vm_guest_metrics'][(string)($record['guest_metrics'] ?? '')] ?? [];        $os = (string)($guest['os_version']['name'] ?? 'Debian GNU/Linux 12');        $vnc = new \App\Vnc\FakeVnc((string)($record['name_label'] ?? 'vm'), $os, !in_array($record['power_state'] ?? '', ['Running', 'Paused'], true));        $this->vncs[] = $vnc;        return $vnc;    }    private function addVdi(string $sr, string $name, float $size, string $type, bool $readOnly): string    {        $vdi = $this->add('vdi', ['name_label' => $name, 'name_description' => $readOnly ? '' : 'Created by XenCenter', 'SR' => $sr, 'virtual_size' => $size,            'physical_utilisation' => floor($size * ($readOnly ? 1 : 0.42)), 'type' => $type, 'read_only' => $readOnly, 'sharable' => false, 'VBDs' => [], 'is_a_snapshot' => false, 'snapshot_of' => 'OpaqueRef:NULL', 'managed' => true]);        $this->append('sr', $sr, 'VDIs', $vdi);        return $vdi;    }    private function addVbd(string $vm, string $vdi, string $user, string $type, string $device, bool $attached): string    {        $vbd = $this->add('vbd', ['VM' => $vm, 'VDI' => $vdi, 'userdevice' => $user, 'device' => $device, 'type' => $type, 'mode' => $type === 'CD' ? 'RO' : 'RW',            'bootable' => $type === 'Disk', 'empty' => $vdi === 'OpaqueRef:NULL', 'currently_attached' => $attached, 'unpluggable' => $type === 'CD', 'qos_algorithm_type' => '', 'qos_algorithm_params' => []]);        $this->append('vm', $vm, 'VBDs', $vbd);        if ($vdi !== 'OpaqueRef:NULL') {            $this->append('vdi', $vdi, 'VBDs', $vbd);        }        return $vbd;    }    private function addVif(string $vm, string $net, string $device, bool $attached, int $index): string    {        $vif = $this->add('vif', ['VM' => $vm, 'network' => $net, 'device' => $device, 'MAC' => sprintf('c2:4f:%02x:%02x:%02x:%02x', 0x10 + $index, (int)$device, $index * 7 % 256, (crc32($vm . $device) & 0xff)),            'MTU' => 1500, 'currently_attached' => $attached, 'qos_algorithm_type' => '', 'qos_algorithm_params' => [], 'locking_mode' => 'network_default', 'ipv4_allowed' => [], 'ipv6_allowed' => []]);        $this->append('vm', $vm, 'VIFs', $vif);        $this->append('network', $net, 'VIFs', $vif);        return $vif;    }    private function snapshot(string $vm, string $name, int $when = 0): string    {        $source = $this->record('vm', $vm);        $metrics = $this->add('vm_metrics', ['memory_actual' => 0, 'VCPUs_number' => 0, 'start_time' => '19700101T00:00:00Z', 'install_time' => $this->date($when), 'last_updated' => $this->date($when)]);        $snap = $this->add('vm', ['name_label' => $name, 'is_a_snapshot' => true, 'snapshot_of' => $vm, 'snapshot_time' => $this->date($when), 'power_state' => 'Halted',            'resident_on' => 'OpaqueRef:NULL', 'metrics' => $metrics, 'guest_metrics' => 'OpaqueRef:NULL', 'VBDs' => [], 'VIFs' => [], 'snapshots' => [], 'allowed_operations' => [], 'current_operations' => [],            'tags' => []] + $source);        $this->append('vm', $vm, 'snapshots', $snap);        return $snap;    }    private function tickMetrics(): void    {        $t = $this->clock;        foreach ($this->db['host_metrics'] ?? [] as $ref => $m) {            $total = (float)$m['memory_total'];            $used = 0;            foreach ($this->db['host'] as $h) {                if ($h['metrics'] === $ref) {                    foreach ($h['resident_VMs'] as $vm) {                        $used += (float)($this->db['vm'][$vm]['memory_dynamic_max'] ?? 0);                    }                }            }            $this->set('host_metrics', $ref, ['memory_free' => max(0, floor($total - $used - (2 * self::GIB + sin($t / 30) * 200 * self::MIB))), 'last_updated' => $this->date()]);        }    }    /** Folders (other_config "folder") and tags, as XenCenter's Organization views keep them; an empty folder kept in the pool. */    private function organizeDemo(string $pool): void    {        $folders = ['web-01' => '/Production/Web', 'web-02' => '/Production/Web', 'db-primary' => '/Production/Databases', 'db-replica' => '/Production/Databases',            'test-win11' => '/Lab', 'backup-srv' => '/Lab', 'NFS storage' => '/Production'];        $tags = ['ad-dc01' => ['windows'], 'test-win11' => ['windows', 'test'], 'backup-srv' => ['backup'], 'NFS storage' => ['shared-storage']];        foreach (['vm', 'sr'] as $class) {            foreach ($this->db[$class] as $ref => $r) {                $name = (string)$r['name_label'];                if (isset($folders[$name])) {                    $this->db[$class][$ref]['other_config']['folder'] = $folders[$name];                }                if (isset($tags[$name])) {                    $this->db[$class][$ref]['tags'] = array_values(array_unique([...(array)$r['tags'], ...$tags[$name]]));                }            }        }        $this->db['pool'][$pool]['other_config']['folders'] = '["/Archive"]';    }    // the API --------------------------------------------------------------------------------------------------------------    /** @param list<mixed> $p */    private function dispatch(string $name, array $p): mixed    {        if (isset($this->failures[$name])) {            throw new XapiError($this->failures[$name]);        }        if ($name === 'session.login_with_password') {            $this->refuseIfSlave();            if ((string)($p[0] ?? '') !== self::USER || (string)($p[1] ?? '') !== self::PASSWORD) {                throw new XapiError(['SESSION_AUTHENTICATION_FAILED', (string)($p[0] ?? ''), 'Authentication failure']);            }            $session = $this->ref();            $this->sessions[$session] = true;            return $session;        }        if (!isset($this->sessions[(string)($p[0] ?? '')])) {            throw new XapiError(['SESSION_INVALID', (string)($p[0] ?? '')]);        }        array_shift($p);        if ($name === 'session.logout') {            return '';        }        $async = str_starts_with($name, 'Async.');        $call = $async ? substr($name, 6) : $name;        [$class, $method] = explode('.', $call, 2) + [1 => ''];        $class = strtolower($class);        if ($method === 'get_all_records') {            return $this->db[$class] ?? [];        }        if ($method === 'get_all') {            return array_keys($this->db[$class] ?? []);        }        if ($method === 'get_record') {            return $this->record($class, (string)$p[0]);        }        if (str_starts_with($method, 'get_') && isset($p[0])) {            return $this->record($class, (string)$p[0])[substr($method, 4)] ?? null;        }        if (preg_match('/^set_(name_label|name_description|tags|other_config|ha_restart_priority|affinity)$/', $method, $m)) {            $this->record($class, (string)$p[0]);            $this->set($class, (string)$p[0], [$m[1] => $p[1]]);            return '';        }        // XAPI's own names: add_to_other_config / remove_from_other_config for maps, add_tags / remove_tags for the tags set        if (preg_match('/^(add_to|remove_from|add|remove)_(other_config|platform|tags|xenstore_data|blocked_operations)$/', $method, $m) && isset($p[0])) {            $record = $this->record($class, (string)$p[0]);            $value = (array)($record[$m[2]] ?? []);            if ($m[2] === 'tags') {                $value = str_starts_with($m[1], 'add') ? array_values(array_unique([...$value, (string)$p[1]])) : array_values(array_diff($value, [(string)$p[1]]));            } elseif ($m[1] === 'add_to') {                $value[(string)$p[1]] = (string)$p[2];            } else {                unset($value[(string)$p[1]]);            }            $this->set($class, (string)$p[0], [$m[2] => $value]);            return '';        }        if ($class === 'task' && $method === 'destroy') {            $this->delete('task', (string)$p[0]);            return '';        }        if ($class === 'task' && $method === 'cancel') {            $this->cancelHttp((string)$p[0]);            foreach ($this->jobs as $i => $job) {                if ($job['task'] === $p[0]) {                    unset($this->jobs[$i]);                    $this->set('task', (string)$p[0], ['status' => 'cancelled', 'finished' => $this->date()]);                }            }            $this->jobs = array_values($this->jobs);            return '';        }        if ($class === 'message' && $method === 'destroy') {            $this->delete('message', (string)$p[0]);            return '';        }        $work = $this->operation($class, $method, $p);        if ($work === null) {            throw new XapiError(['MESSAGE_METHOD_UNKNOWN', $name]);        }        [$seconds, $apply, $label, $target] = $work;        if (!$async) {            return $apply();        }        $task = $this->add('task', ['name_label' => $name, 'name_description' => $label, 'status' => 'pending', 'progress' => 0.0, 'created' => $this->date(), 'finished' => '19700101T00:00:00Z',            'result' => '', 'error_info' => [], 'resident_on' => array_key_first($this->db['host']), 'allowed_operations' => ['cancel'], 'other_config' => ['applies_to' => $target]]);        $this->jobs[] = ['task' => $task, 'left' => $seconds, 'total' => $seconds, 'apply' => $apply];        return $task;    }    /** @return array{0: float, 1: \Closure, 2: string, 3: string}|null seconds, what it does, label, the object */    private function operation(string $class, string $method, array $p): ?array    {        if (($own = $this->storageOperation($class, $method, $p) ?? $this->networkOperation($class, $method, $p) ?? $this->migrationOperation($class, $method, $p)) !== null) {            return $own;        }        $ref = is_scalar($p[0] ?? '') ? (string)($p[0] ?? '') : '';        if (($feature = $this->updatesOperation($class, $method, $p) ?? $this->xvaOperation($class, $method, $p)) !== null) {            return $feature;        }        if ($class === 'pool') {            return $this->poolOperation($method, $p);        }        if ($class === 'vm') {            $vm = $this->record('vm', $ref);            $state = $vm['power_state'];            $need = function (string ...$states) use ($ref, $state): void {                $now = $this->db['vm'][$ref]['power_state'] ?? $state;                if (!in_array($now, $states, true)) {                    throw new XapiError(['VM_BAD_POWER_STATE', $ref, strtolower($states[0]), strtolower($now)]);                }            };            return match ($method) {                'start', 'start_on' => [2.5, function () use ($ref, $need, $method, $p): string {                    $need('Halted');                    $host = $method === 'start_on' ? (string)$p[1] : $this->pickHost($ref);                    $this->power($ref, 'Running', $host);                    return '';                }, 'Starting', $ref],                'clean_shutdown', 'hard_shutdown' => [$method === 'clean_shutdown' ? 3.0 : 1.0, function () use ($ref, $need): string {                    $need('Running', 'Paused');                    $this->power($ref, 'Halted', null);                    return '';                }, 'Shutting down', $ref],                'clean_reboot', 'hard_reboot' => [3.0, function () use ($ref, $need): string {                    $need('Running');                    $this->set('vm_metrics', $this->db['vm'][$ref]['metrics'], ['start_time' => $this->date()]);                    return '';                }, 'Rebooting', $ref],                'suspend' => [2.0, function () use ($ref, $need): string {                    $need('Running');                    $this->power($ref, 'Suspended', null);                    return '';                }, 'Suspending', $ref],                'resume', 'resume_on' => [2.0, function () use ($ref, $need, $method, $p): string {                    $need('Suspended');                    $this->power($ref, 'Running', $method === 'resume_on' ? (string)$p[1] : $this->pickHost($ref));                    return '';                }, 'Resuming', $ref],                'pause' => [0.5, function () use ($ref, $need): string {                    $need('Running');                    $this->set('vm', $ref, ['power_state' => 'Paused']);                    return '';                }, 'Pausing', $ref],                'unpause' => [0.5, function () use ($ref, $need): string {                    $need('Paused');                    $this->set('vm', $ref, ['power_state' => 'Running']);                    return '';                }, 'Unpausing', $ref],                'pool_migrate' => [4.0, function () use ($ref, $need, $p): string {                    $need('Running');                    $from = (string)$this->db['vm'][$ref]['resident_on'];                    $this->removeFrom('host', $from, 'resident_VMs', $ref);                    $this->append('host', (string)$p[1], 'resident_VMs', $ref);                    $this->set('vm', $ref, ['resident_on' => (string)$p[1]]);                    return '';                }, 'Migrating', $ref],                'snapshot' => [1.5, fn (): string => $this->snapshot($ref, (string)($p[1] ?? 'snapshot')), 'Taking a snapshot', $ref],                'revert' => [2.0, function () use ($ref): string {                    $of = (string)$this->db['vm'][$ref]['snapshot_of'];                    if (isset($this->db['vm'][$of]) && $this->db['vm'][$of]['power_state'] !== 'Halted') {                        $this->power($of, 'Halted', null);                    }                    return '';                }, 'Reverting to snapshot', $ref],                'clone', 'copy' => [2.0, function () use ($ref, $p): string {                    $src = $this->db['vm'][$ref];                    $disk = 0;                    foreach ($src['VBDs'] as $vbd) {                        $vdi = $this->db['vbd'][$vbd]['VDI'] ?? '';                        $disk = max($disk, (int)(($this->db['vdi'][$vdi]['virtual_size'] ?? 0) / self::GIB));                    }                    $new = $this->addVm(['name_label' => (string)$p[1], 'description' => $src['is_a_template'] ? '' : $src['name_description'], 'power_state' => 'Halted',                        'memory' => (float)$src['memory_static_max'], 'vcpus' => (int)$src['VCPUs_max'], 'disk' => $src['is_a_template'] ? 0 : max(10, $disk),                        'sr' => $this->db['pool'][array_key_first($this->db['pool'])]['default_SR'], 'networks' => [], 'index' => count($this->db['vm'])]);                    $this->set('vm', $new, ['other_config' => ['base_template_name' => $src['name_label']], 'domain_type' => $src['domain_type']]);                    $this->db['vm'][$new]['template_disk'] = $src['is_a_template'] ? (int)(($src['name_label'] === 'Windows 11' || str_starts_with($src['name_label'], 'Windows')) ? 64 : 10) : 0;                    return $new;                }, 'Cloning', $ref],                'provision' => [1.5, function () use ($ref): string {                    $vm = $this->db['vm'][$ref];                    if ($vm['VBDs'] === []) {                        $vdi = $this->addVdi($this->db['pool'][array_key_first($this->db['pool'])]['default_SR'], $vm['name_label'] . ' 0', max(10, (int)($vm['template_disk'] ?? 10)) * self::GIB, 'system', false);                        $this->addVbd($ref, $vdi, '0', 'Disk', 'xvda', false);                        $this->addVbd($ref, 'OpaqueRef:NULL', '3', 'CD', 'xvdd', false);                    }                    return '';                }, 'Provisioning', $ref],                'destroy' => [1.0, function () use ($ref): string {                    $vm = $this->db['vm'][$ref];                    if (!in_array($vm['power_state'], ['Halted'], true) && !$vm['is_a_snapshot']) {                        throw new XapiError(['VM_BAD_POWER_STATE', $ref, 'halted', strtolower($vm['power_state'])]);                    }                    foreach ($vm['VBDs'] as $vbd) {                        // as XAPI: the disks stay (XenCenter destroys them after), their VBDs go                        $vdi = (string)($this->db['vbd'][$vbd]['VDI'] ?? '');                        if (isset($this->db['vdi'][$vdi])) {                            $this->removeFrom('vdi', $vdi, 'VBDs', $vbd);                        }                        $this->delete('vbd', $vbd);                    }                    foreach ($vm['VIFs'] as $vif) {                        $this->removeFrom('network', (string)$this->db['vif'][$vif]['network'], 'VIFs', $vif);                        $this->delete('vif', $vif);                    }                    if ($vm['is_a_snapshot'] && isset($this->db['vm'][$vm['snapshot_of']])) {                        $this->removeFrom('vm', (string)$vm['snapshot_of'], 'snapshots', $ref);                    }                    $this->delete('vm', $ref);                    return '';                }, 'Deleting', $ref],                'set_memory_limits', 'set_memory' => [0.3, function () use ($ref, $p, $method): string {                    $max = (float)($method === 'set_memory' ? $p[1] : $p[2]);                    $this->set('vm', $ref, ['memory_static_max' => $max, 'memory_dynamic_max' => $max, 'memory_dynamic_min' => $max, 'memory_target' => $max]);                    return '';                }, 'Changing memory', $ref],                'set_VCPUs_max' => [0.2, function () use ($ref, $p): string {                    $this->set('vm', $ref, ['VCPUs_max' => (int)$p[1]]);                    return '';                }, 'Changing vCPUs', $ref],                'set_VCPUs_at_startup' => [0.2, function () use ($ref, $p): string {                    $this->set('vm', $ref, ['VCPUs_at_startup' => (int)$p[1]]);                    return '';                }, 'Changing vCPUs', $ref],                'set_is_a_template' => [0.2, function () use ($ref, $p): string {                    $this->set('vm', $ref, ['is_a_template' => (bool)$p[1]]);                    return '';                }, 'Converting to template', $ref],                default => null,            };        }        if ($class === 'vbd' && ($method === 'insert' || $method === 'eject')) {            $this->record('vbd', $ref);            return [0.8, function () use ($ref, $method, $p): string {                $old = (string)$this->db['vbd'][$ref]['VDI'];                if (isset($this->db['vdi'][$old])) {                    $this->removeFrom('vdi', $old, 'VBDs', $ref);                }                $vdi = $method === 'insert' ? (string)$p[1] : 'OpaqueRef:NULL';                $this->set('vbd', $ref, ['VDI' => $vdi, 'empty' => $method === 'eject']);                if ($method === 'insert') {                    $this->append('vdi', $vdi, 'VBDs', $ref);                }                return '';            }, $method === 'insert' ? 'Inserting a disc' : 'Ejecting a disc', (string)$this->db['vbd'][$ref]['VM']];        }        if ($class === 'vdi' && $method === 'destroy') {            $vdi = $this->record('vdi', $ref);            return [0.5, function () use ($ref, $vdi): string {                if ($vdi['VBDs'] !== []) {                    throw new XapiError(['VDI_IN_USE', $ref, 'destroy']);                }                $this->removeFrom('sr', (string)$vdi['SR'], 'VDIs', $ref);                $this->delete('vdi', $ref);                return '';            }, 'Deleting a virtual disk', $ref];        }        if ($class === 'vif' && $method === 'create') {            $r = (array)$p[0];            return [0.3, fn (): string => $this->addVif((string)$r['VM'], (string)$r['network'], (string)$r['device'], false, 1), 'Adding a network interface', (string)$r['VM']];        }        if ($class === 'host') {            $this->record('host', $ref);            return match ($method) {                'disable' => [0.5, function () use ($ref): string { $this->set('host', $ref, ['enabled' => false]); return ''; }, 'Entering maintenance mode', $ref],                'enable' => [0.5, function () use ($ref): string { $this->set('host', $ref, ['enabled' => true]); return ''; }, 'Exiting maintenance mode', $ref],                'evacuate' => [4.0, function () use ($ref): string {                    $other = null;                    foreach ($this->db['host'] as $h => $rec) {                        if ($h !== $ref && $rec['enabled']) {                            $other = $h;                        }                    }                    foreach ($this->db['host'][$ref]['resident_VMs'] as $vm) {                        if ($other !== null && empty($this->db['vm'][$vm]['is_control_domain'])) {                            $this->removeFrom('host', $ref, 'resident_VMs', $vm);                            $this->append('host', $other, 'resident_VMs', $vm);                            $this->set('vm', $vm, ['resident_on' => $other]);                        }                    }                    return '';                }, 'Migrating VMs away', $ref],                'reboot', 'shutdown' => [3.0, function () use ($ref, $method): string {                    foreach ($this->db['host'][$ref]['resident_VMs'] as $vm) {                        if (empty($this->db['vm'][$vm]['is_control_domain'])) {                            throw new XapiError(['HOST_IN_USE', $ref, 'VM', $vm]);                        }                    }                    if ($method === 'reboot') {                        $this->set('host', $ref, ['other_config' => ['boot_time' => (string)($this->now + (int)$this->clock)] + $this->db['host'][$ref]['other_config']]);                    }                    return '';                }, $method === 'reboot' ? 'Rebooting' : 'Shutting down', $ref],                default => null,            };        }        if ($class === 'sr' && $method === 'scan') {            $this->record('sr', $ref);            return [1.5, fn (): string => '', 'Rescanning', $ref];        }        return null;    }    private function pickHost(string $vm): string    {        $affinity = (string)($this->db['vm'][$vm]['affinity'] ?? '');        if (isset($this->db['host'][$affinity]) && $this->db['host'][$affinity]['enabled']) {            return $affinity;        }        foreach ($this->db['host'] as $ref => $host) {            if ($host['enabled']) {                return $ref;            }        }        throw new XapiError(['NO_HOSTS_AVAILABLE']);    }    private function power(string $vm, string $state, ?string $host): void    {        $record = $this->db['vm'][$vm];        $from = (string)$record['resident_on'];        if (isset($this->db['host'][$from]) && $state !== 'Running') {            $this->removeFrom('host', $from, 'resident_VMs', $vm);        }        $running = $state === 'Running';        if ($running && $host !== null) {            if (!in_array($vm, $this->db['host'][$host]['resident_VMs'], true)) {                $this->append('host', $host, 'resident_VMs', $vm);            }        }        $this->set('vm', $vm, ['power_state' => $state, 'resident_on' => $running ? (string)$host : 'OpaqueRef:NULL', 'domid' => $running ? random_int(20, 90) : -1]);        $this->set('vm_metrics', (string)$record['metrics'], ['memory_actual' => $running ? (float)$record['memory_dynamic_max'] : 0,            'VCPUs_number' => $running ? (int)$record['VCPUs_at_startup'] : 0, 'start_time' => $running ? $this->date() : '19700101T00:00:00Z']);        foreach ([...$record['VBDs'], ...$record['VIFs']] as $device) {            $kind = isset($this->db['vbd'][$device]) ? 'vbd' : 'vif';            $this->set($kind, $device, ['currently_attached' => $running || $state === 'Paused']);        }        if (!$running && $record['guest_metrics'] !== 'OpaqueRef:NULL' && $state === 'Halted') {            $this->set('vm_guest_metrics', (string)$record['guest_metrics'], ['live' => false]);        } elseif ($running && $record['guest_metrics'] === 'OpaqueRef:NULL' && !$record['is_control_domain']) {            $gm = $this->add('vm_guest_metrics', ['os_version' => ['name' => $record['other_config']['base_template_name'] ?? 'Linux'], 'PV_drivers_version' => ['major' => '8', 'minor' => '4', 'micro' => '0'],                'PV_drivers_up_to_date' => true, 'PV_drivers_detected' => true, 'networks' => ['0/ip' => '192.168.1.' . random_int(150, 220)], 'other' => [], 'live' => true, 'disks' => [], 'last_updated' => $this->date()]);            $this->set('vm', $vm, ['guest_metrics' => $gm]);        } elseif ($running && $record['guest_metrics'] !== 'OpaqueRef:NULL') {            $this->set('vm_guest_metrics', (string)$record['guest_metrics'], ['live' => true]);        }    }    // performance figures ---------------------------------------------------------------------------------------------------    private function rrdXml(int $start, bool $hosts, ?string $vmUuid, ?string $onHost = null): string    {        $step = 5;        $end = $this->now + (int)$this->clock;        $end -= $end % $step;        $from = max($start, $end - 600);        $from -= $from % $step;        $columns = [];        $wave = fn (float $seed, int $t, float $base, float $amp) => max(0.0, $base + $amp * sin($t / 47 + $seed) + $amp * 0.45 * sin($t / 13 + $seed * 3) + $amp * 0.2 * sin($t / 5.3 + $seed * 7));        foreach ($this->db['host'] as $host) {            if ($vmUuid === null && !$this->rrdAnswersFor($host, $onHost)) {                continue;            }            $seed = (float)(crc32($host['uuid']) % 100);            if ($hosts && $vmUuid === null) {                $cpus = (int)$host['cpu_info']['cpu_count'];                for ($c = 0; $c < $cpus; $c++) {                    $columns["AVERAGE:host:{$host['uuid']}:cpu$c"] = fn (int $t) => min(1.0, $wave($seed + $c, $t, 0.22, 0.12));                }                $metrics = $this->db['host_metrics'][$host['metrics']];                $columns["AVERAGE:host:{$host['uuid']}:memory_total_kib"] = fn (int $t) => $metrics['memory_total'] / 1024;                $columns["AVERAGE:host:{$host['uuid']}:memory_free_kib"] = fn (int $t) => $metrics['memory_free'] / 1024 + 200000 * sin($t / 60 + $seed);                $columns["AVERAGE:host:{$host['uuid']}:pif_eth0_rx"] = fn (int $t) => $wave($seed, $t, 2.4e6, 1.6e6);                $columns["AVERAGE:host:{$host['uuid']}:pif_eth0_tx"] = fn (int $t) => $wave($seed + 2, $t, 1.1e6, 0.8e6);                $columns["AVERAGE:host:{$host['uuid']}:loadavg"] = fn (int $t) => $wave($seed, $t, 1.3, 0.7);            }            foreach ($host['resident_VMs'] as $vmRef) {                $vm = $this->db['vm'][$vmRef] ?? null;                if ($vm === null || $vm['power_state'] !== 'Running' || ($vmUuid !== null && $vm['uuid'] !== $vmUuid)) {                    continue;                }                $vs = (float)(crc32($vm['uuid']) % 100);                for ($c = 0; $c < (int)$vm['VCPUs_at_startup']; $c++) {                    $columns["AVERAGE:vm:{$vm['uuid']}:cpu$c"] = fn (int $t) => min(1.0, $wave($vs + $c, $t, 0.18 + ($vs % 5) / 20, 0.15));                }                $mem = (float)$vm['memory_dynamic_max'];                $columns["AVERAGE:vm:{$vm['uuid']}:memory"] = fn (int $t) => $mem;                $columns["AVERAGE:vm:{$vm['uuid']}:memory_internal_free"] = fn (int $t) => $mem / 1024 * (0.45 + 0.1 * sin($t / 80 + $vs));                $columns["AVERAGE:vm:{$vm['uuid']}:vif_0_rx"] = fn (int $t) => $wave($vs, $t, 4.0e5, 3.0e5);                $columns["AVERAGE:vm:{$vm['uuid']}:vif_0_tx"] = fn (int $t) => $wave($vs + 1, $t, 2.0e5, 1.5e5);                $columns["AVERAGE:vm:{$vm['uuid']}:vbd_xvda_read"] = fn (int $t) => $wave($vs + 4, $t, 1.5e6, 1.2e6);                $columns["AVERAGE:vm:{$vm['uuid']}:vbd_xvda_write"] = fn (int $t) => $wave($vs + 5, $t, 0.8e6, 0.7e6);            }        }        $xml = '<xport><meta><start>' . $from . '</start><step>' . $step . '</step><end>' . $end . '</end><rows>' . (int)(($end - $from) / $step) . '</rows><columns>'            . count($columns) . '</columns><legend>';        foreach (array_keys($columns) as $name) {            $xml .= "<entry>$name</entry>";        }        $xml .= '</legend></meta><data>';        for ($t = $end; $t > $from; $t -= $step) {            $xml .= "<row><t>$t</t>";            foreach ($columns as $fn) {                $xml .= '<v>' . sprintf('%.4F', $fn($t)) . '</v>';            }            $xml .= '</row>';        }        return $xml . '</data></xport>';    }}