PL
games/xencenter-xenadmin-xcp-jumphp / src / Xapi / Fake / MigrationOps.php · commit fb1b7c9
<?phpnamespace App\Xapi\Fake;use App\Xapi\FakeXapi;use App\Xapi\XapiError;/** * Cross-pool migration between demo pools (used by FakeXapi), and the demo pools themselves: * - every FakeXapi is kept by its address (PoolOps' registry), so that VM.migrate_send of one finds the pool named in the map that *   host.migrate_receive of the other gave: the VM record, its disks (to the SRs of vdi_map) and its VIFs (to the networks of vif_map) *   move from one instance to the other, as XAPI's storage motion does; * - the address of the main demo pool builds it (FakeXapi::build()) plus two spare NICs per host (to make bonds); 'xcp-lab.local' *   builds "Demo pool 2", a smaller pool of one host, with refs and uuids of its own (two demo pools can be open at once); any other *   address is a standalone host (PoolOps::buildFor()). */trait MigrationOps{    public static function isMainDemo(string $address): bool    {        return strtolower($address) === 'xcp-demo.local';    }    /** The address of "Demo pool 2" (File > 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;    }}