Open the Second Demo Pool): 'xcp-lab.local' (any 'xcp-lab…' name); 'xcp-demo-N.local' are standalone hosts (PoolOps). */ public static function isSecondDemo(string $address): bool { return str_starts_with(strtolower($address), 'xcp-lab'); } /** The demo pool of an address, if one is open (the registry of PoolOps: one for both). */ public static function at(string $address): ?FakeXapi { return self::$fakes[strtolower($address)] ?? null; } /** The main demo pool plus two spare NICs per host (to make bonds). */ private function buildMainDemo(): void { $this->build(); $this->addSpareNics(); $this->log = []; $this->eventId = 100; } /** "Demo pool 2": refs and uuids of its own (the window finds an object's connection by its ref). */ private function buildSecondDemo(): void { $this->nextRef = 0x100000 * (1 + (crc32(strtolower($this->address)) & 0xff)); $this->seed = 1000 + (crc32($this->address) & 0xffff); $this->buildSecondPool(); $this->log = []; $this->eventId = 100; } /** eth2 and eth3 on each host, 1 Gbit/s, each with its pool-wide network (as XAPI makes them): NICs free to bond. */ private function addSpareNics(): void { foreach (['eth2', 'eth3'] as $k => $device) { $net = $this->add('network', ['name_label' => "Pool-wide network associated with $device", 'name_description' => '', 'bridge' => 'xenbr' . (2 + $k), 'MTU' => 1500, 'PIFs' => [], 'VIFs' => []]); $n = 0; foreach (array_keys($this->db['host']) as $host) { $metrics = $this->add('pif_metrics', ['speed' => 1000, 'duplex' => true, 'carrier' => true, 'vendor_name' => 'Broadcom Inc.', 'device_name' => 'NetXtreme BCM5720 Gigabit Ethernet', 'pci_bus_path' => '0000:04:00.' . $k]); $this->addPif((string)$host, $net, $device, sprintf('00:1b:21:4c:%02x:%02x', $n++, 0x20 + $k), $metrics, -1, true); } } } private function buildSecondPool(): void { $pool = $this->ref(); $net0 = $this->add('network', ['name_label' => 'Pool-wide network associated with eth0', 'name_description' => '', 'bridge' => 'xenbr0', 'MTU' => 1500, 'PIFs' => [], 'VIFs' => []]); $net1 = $this->add('network', ['name_label' => 'Lab backend (eth1)', 'name_description' => 'Replication traffic', 'bridge' => 'xenbr1', 'MTU' => 1500, 'PIFs' => [], 'VIFs' => []]); $this->add('network', ['name_label' => 'Host internal management network', 'name_description' => '', 'bridge' => 'xenapi', 'MTU' => 1500, 'PIFs' => [], 'VIFs' => [], 'other_config' => ['is_host_internal_management_network' => 'true']]); $nfs = $this->add('sr', ['name_label' => 'Lab NFS', 'name_description' => 'NFS SR [10.20.0.5:/srv/lab]', 'type' => 'nfs', 'content_type' => 'user', 'shared' => true, 'physical_size' => 2000 * self::GIB, 'physical_utilisation' => 410 * self::GIB, 'virtual_allocation' => 520 * self::GIB, 'PBDs' => [], 'VDIs' => []]); $name = 'xcp-lab-01'; $ip = '192.168.2.21'; $metrics = $this->add('host_metrics', ['memory_total' => 64 * self::GIB, 'memory_free' => 40 * self::GIB, 'live' => true, 'last_updated' => $this->date()]); $host = $this->add('host', ['name_label' => $name, 'name_description' => 'Lab, bench 3', 'hostname' => $name, 'address' => $ip, 'enabled' => true, 'metrics' => $metrics, 'resident_VMs' => [], 'PIFs' => [], 'PBDs' => [], 'memory_overhead' => 280 * self::MIB, 'API_version_major' => 2, 'API_version_minor' => 21, 'cpu_info' => ['cpu_count' => '12', 'socket_count' => '1', 'vendor' => 'AuthenticAMD', 'speed' => '3693.062', 'modelname' => 'AMD Ryzen 9 5900X 12-Core Processor', 'family' => '25', 'model' => '33', 'stepping' => '0', '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' => 'American Megatrends', 'system-manufacturer' => 'ASRock', 'system-product-name' => 'X570 Taichi', 'system-serial-number' => 'LAB0001'], 'other_config' => ['iscsi_iqn' => "iqn.2024-01.local.lab:$name", 'agent_start_time' => (string)($this->now - 86400 * 3), 'boot_time' => (string)($this->now - 86400 * 9)], 'iscsi_iqn' => "iqn.2024-01.local.lab:$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' => '']); foreach ([[$net0, 'eth0', $ip, '0c:c4:7a:10:00:01'], [$net1, 'eth1', '', '0c:c4:7a:10:00:02']] as [$net, $device, $address, $mac]) { $pm = $this->add('pif_metrics', ['speed' => 2500, 'duplex' => true, 'carrier' => true, 'vendor_name' => 'Intel Corporation', 'device_name' => 'Ethernet Controller I225-V', 'pci_bus_path' => '0000:05:00.0']); $pif = $this->addPif($host, $net, $device, $mac, $pm, -1, true); if ($address !== '') { $this->set('pif', $pif, ['IP' => $address, 'netmask' => '255.255.255.0', 'gateway' => '192.168.2.1', 'DNS' => '192.168.2.1', 'ip_configuration_mode' => 'Static', 'management' => true]); } } $local = $this->add('sr', ['name_label' => 'Local storage', 'name_description' => "Local storage on $name", 'type' => 'ext', 'content_type' => 'user', 'shared' => false, 'physical_size' => 1800 * self::GIB, 'physical_utilisation' => 120 * self::GIB, 'virtual_allocation' => 160 * self::GIB, 'PBDs' => [], 'VDIs' => [], 'other_config' => ['i18n-key' => 'local-storage']]); foreach ([$local, $nfs] as $sr) { $pbd = $this->add('pbd', ['host' => $host, 'SR' => $sr, 'currently_attached' => true, 'device_config' => $sr === $nfs ? ['server' => '10.20.0.5', 'serverpath' => '/srv/lab'] : ['device' => '/dev/nvme0n1p3']]); $this->append('sr', $sr, 'PBDs', $pbd); $this->append('host', $host, 'PBDs', $pbd); } $this->addVm(['name_label' => "Control domain on host: $name", 'is_control_domain' => true, 'power_state' => 'Running', 'resident_on' => $host, 'memory' => 2 * self::GIB, 'vcpus' => 8, 'os' => 'XCP-ng 8.3']); $this->add('pool', ['name_label' => 'Demo pool 2', 'name_description' => 'Lab pool (cross-pool migration target)', 'master' => $host, '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' => '12', 'socket_count' => '1', 'vendor' => 'AuthenticAMD']], $pool); foreach (['Debian Bookworm 12', 'Other install media'] as $t) { $this->addVm(['name_label' => $t, 'is_a_template' => true, 'default_template' => true, 'power_state' => 'Halted', 'memory' => 2 * self::GIB, 'vcpus' => 2, 'description' => "Template for $t", 'disk' => 10]); } foreach ([['lab-dns', 'Unbound resolver', 'Running', 2, 1, 'Debian GNU/Linux 12 (bookworm)', 8, $nfs], ['lab-ci', 'CI runner', 'Halted', 8, 4, 'Ubuntu 24.04.1 LTS', 80, $local]] as $i => [$vmName, $desc, $state, $memG, $vcpus, $os, $disk, $sr]) { $this->addVm(['name_label' => $vmName, 'description' => $desc, 'power_state' => $state, 'resident_on' => $state === 'Halted' ? 'OpaqueRef:NULL' : $host, 'affinity' => $host, 'memory' => $memG * self::GIB, 'vcpus' => $vcpus, 'os' => $os, 'disk' => $disk, 'sr' => $sr, 'networks' => [$net0], 'index' => 40 + $i, 'tags' => ['lab']]); } } // cross-pool migration ---------------------------------------------------------------------------------------------- /** @return array{0: float, 1: \Closure, 2: string, 3: string}|null */ private function migrationOperation(string $class, string $method, array $p): ?array { $ref = is_scalar($p[0] ?? null) ? (string)$p[0] : ''; switch ("$class.$method") { case 'host.migrate_receive': $this->record('host', $ref); $this->record('network', (string)($p[1] ?? '')); return [0.2, function () use ($ref, $p): array { $session = $this->ref(); $this->sessions[$session] = true; $address = (string)$this->db['host'][$ref]['address']; return ['SM' => "http://$address/services/SM?session_id=$session", 'host' => $ref, 'xenops' => "http://$address/services/xenops?session_id=$session", 'session_id' => $session, 'master' => "http://{$this->address}/", 'network' => (string)$p[1]]; }, 'Preparing to receive a VM', $ref]; case 'vm.assert_can_migrate': case 'vm.migrate_send': $this->record('vm', $ref); $dest = (array)($p[1] ?? []); $live = (bool)($p[2] ?? false); $vdiMap = (array)($p[3] ?? []); $vifMap = (array)($p[4] ?? []); $check = fn (): FakeXapi => $this->assertCanMigrate($ref, $dest, $live, $vdiMap, $vifMap); if ($method === 'assert_can_migrate') { return [0.3, function () use ($check): string { $check(); return ''; }, 'Checking a migration', $ref]; } return [6.0, fn (): string => $this->sendVm($check(), $ref, (string)$dest['host'], $vdiMap, $vifMap), 'Migrating to another pool', $ref]; } return null; } private function assertCanMigrate(string $vm, array $dest, bool $live, array $vdiMap, array $vifMap): FakeXapi { $target = self::at((string)parse_url((string)($dest['master'] ?? ''), PHP_URL_HOST)); $host = (string)($dest['host'] ?? ''); if ($target === null || $target === $this || !isset($target->db['host'][$host])) { throw new XapiError(['HOST_CANNOT_ATTACH_NETWORK', $host]); } $r = $this->db['vm'][$vm]; if (!empty($r['is_control_domain']) || !empty($r['is_a_template']) || !empty($r['is_a_snapshot'])) { throw new XapiError(['OPERATION_NOT_ALLOWED', 'This VM cannot be migrated']); } $state = (string)$r['power_state']; if (!in_array($state, ['Running', 'Halted'], true) || ($state === 'Running' && !$live)) { throw new XapiError(['VM_BAD_POWER_STATE', $vm, $live ? 'running' : 'halted', strtolower($state)]); } foreach ((array)$r['VBDs'] as $vbd) { $vdi = (string)($this->db['vbd'][$vbd]['VDI'] ?? 'OpaqueRef:NULL'); if ($vdi === 'OpaqueRef:NULL' || ($this->db['vbd'][$vbd]['type'] ?? '') === 'CD') { continue; } if (!isset($vdiMap[$vdi]) || !isset($target->db['sr'][(string)$vdiMap[$vdi]])) { throw new XapiError(['VDI_NOT_IN_MAP', $vdi]); } } foreach ((array)$r['VIFs'] as $vif) { if (!isset($vifMap[$vif]) || !isset($target->db['network'][(string)$vifMap[$vif]])) { throw new XapiError(['VIF_NOT_IN_MAP', (string)$vif]); } } $free = (float)($target->db['host_metrics'][(string)$target->db['host'][$host]['metrics']]['memory_free'] ?? 0); if ($state === 'Running' && (float)$r['memory_dynamic_max'] > $free) { throw new XapiError(['HOST_NOT_ENOUGH_FREE_MEMORY', sprintf('%.0f', (float)$r['memory_dynamic_max']), sprintf('%.0f', $free)]); } return $target; } /** The VM leaves this pool for $target: same uuid there, new refs; its disks on the mapped SRs, its VIFs on the mapped networks. */ private function sendVm(FakeXapi $target, string $vm, string $host, array $vdiMap, array $vifMap): string { $r = $this->db['vm'][$vm]; $running = $r['power_state'] === 'Running'; $metrics = $target->add('vm_metrics', $this->db['vm_metrics'][(string)$r['metrics']] ?? ['memory_actual' => 0, 'VCPUs_number' => 0]); $guest = isset($this->db['vm_guest_metrics'][(string)$r['guest_metrics']]) ? $target->add('vm_guest_metrics', $this->db['vm_guest_metrics'][(string)$r['guest_metrics']]) : 'OpaqueRef:NULL'; $new = $target->add('vm', ['resident_on' => $running ? $host : 'OpaqueRef:NULL', 'affinity' => $host, 'metrics' => $metrics, 'guest_metrics' => $guest, 'VBDs' => [], 'VIFs' => [], 'snapshots' => [], 'consoles' => [], 'current_operations' => []] + $r); if ($running) { $target->append('host', $host, 'resident_VMs', $new); } $target->addConsole($new); foreach ((array)$r['VBDs'] as $vbd) { $b = $this->db['vbd'][$vbd]; $vdi = (string)$b['VDI']; $copy = 'OpaqueRef:NULL'; if ($b['type'] !== 'CD' && isset($this->db['vdi'][$vdi])) { $d = $this->db['vdi'][$vdi]; $copy = $target->addVdi((string)$vdiMap[$vdi], (string)$d['name_label'], (float)$d['virtual_size'], (string)$d['type'], (bool)$d['read_only']); $target->set('vdi', $copy, ['name_description' => (string)$d['name_description'], 'physical_utilisation' => (float)$d['physical_utilisation']]); } $nb = $target->addVbd($new, $copy, (string)$b['userdevice'], (string)$b['type'], (string)$b['device'], $running); $target->set('vbd', $nb, ['bootable' => (bool)$b['bootable'], 'mode' => (string)$b['mode'], 'unpluggable' => (bool)$b['unpluggable']]); } foreach ((array)$r['VIFs'] as $vif) { $v = $this->db['vif'][$vif]; $nv = $target->addVif($new, (string)$vifMap[$vif], (string)$v['device'], $running, 0); $target->set('vif', $nv, ['MAC' => (string)$v['MAC'], 'MTU' => (int)$v['MTU']]); } foreach ((array)$r['snapshots'] as $snap) { if (isset($this->db['vm'][$snap])) { $s = $target->add('vm', ['uuid' => $target->uuid(), 'snapshot_of' => $new, 'metrics' => 'OpaqueRef:NULL', 'VBDs' => [], 'VIFs' => [], 'snapshots' => []] + $this->db['vm'][$snap]); $target->append('vm', $new, 'snapshots', $s); $this->delete('vm', (string)$snap); } } // gone from here if (isset($this->db['host'][(string)$r['resident_on']])) { $this->removeFrom('host', (string)$r['resident_on'], 'resident_VMs', $vm); } foreach ((array)$r['VBDs'] as $vbd) { $b = $this->db['vbd'][$vbd]; $vdi = (string)$b['VDI']; if (isset($this->db['vdi'][$vdi])) { if ($b['type'] === 'CD') { $this->removeFrom('vdi', $vdi, 'VBDs', (string)$vbd); // the ISO stays in its library } else { $this->removeFrom('sr', (string)$this->db['vdi'][$vdi]['SR'], 'VDIs', $vdi); $this->delete('vdi', $vdi); } } $this->delete('vbd', (string)$vbd); } foreach ((array)$r['VIFs'] as $vif) { $this->removeFrom('network', (string)$this->db['vif'][$vif]['network'], 'VIFs', (string)$vif); $this->delete('vif', (string)$vif); } foreach ((array)($r['consoles'] ?? []) as $console) { $this->delete('console', (string)$console); } $this->delete('vm_metrics', (string)$r['metrics']); if (isset($this->db['vm_guest_metrics'][(string)$r['guest_metrics']])) { $this->delete('vm_guest_metrics', (string)$r['guest_metrics']); } $this->delete('vm', $vm); return $new; } }