>> class (lower case) => ref => record */ public array $db = []; /** @var list */ private array $log = []; private int $eventId = 0; private int $nextRef = 1; /** @var list answers to give at a time */ private array $outbox = []; /** @var list, until: float, id: mixed}> */ private array $waiting = []; /** @var list running Async operations */ private array $jobs = []; private float $clock = 0; private float $metricsIn = 5; private int $seed = 7; private int $now; /** @var list calls received (tests) */ public array $calls = []; /** @var array 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 $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 $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 $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 */ 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\n\t\n\t\n\t\n\t\n\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 $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 $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 = '' . $from . '' . $step . '' . $end . '' . (int)(($end - $from) / $step) . '' . count($columns) . ''; foreach (array_keys($columns) as $name) { $xml .= "$name"; } $xml .= ''; for ($t = $end; $t > $from; $t -= $step) { $xml .= "$t"; foreach ($columns as $fn) { $xml .= '' . sprintf('%.4F', $fn($t)) . ''; } $xml .= ''; } return $xml . ''; } }