Sign in
EN
games/xencenter-xenadmin-xcp-jumphp / src / Xapi / Fake / PoolOps.php · commit fb1b7c9
<?phpnamespace App\Xapi\Fake;use App\Xapi\FakeXapi;use App\Xapi\XapiError;/** * Pools in the demo (used by FakeXapi): another demo address than the demo pool's is a standalone host of its own * ('xcp-demo-2.local' = xcp-ng-03, 'xcp-demo-3.local' = xcp-ng-04...), and the pool calls behave as XAPI's: * - pool.join(master_address, user, password), called ON THE JOINING HOST: the host, its local storage and its VMs move into the *   master's database (its networks mapped onto the pool's by device, PBDs made for the pool's shared SRs); then the joining host *   answers HOST_IS_SLAVE <master> to a login and its sessions end, as a real one whose xapi restarted as a pool member; * - pool.eject(host) on the master: the host leaves with its local storage and the VMs on it (it reboots as a fresh standalone host); * - pool.designate_new_master(host): the pool's master changes. * The instances know each other through a registry by address (two FakeXapi are separate objects, as two servers are). */trait PoolOps{    /** @var array<string, FakeXapi> address (lower case) => the fake server answering there */    private static array $fakes = [];    /** this host joined a pool: the master's address (login answers HOST_IS_SLAVE) */    private ?string $slaveOf = null;    private float $dropIn = -1;    /** @var array<string, string> host ref => the address of the demo server it came from (joined hosts) */    private array $joinedFrom = [];    /** The fake server at an address: a host that joined a pool stays (it answers HOST_IS_SLAVE), anything else starts afresh. */    public static function open(string $address): FakeXapi    {        $known = self::$fakes[strtolower($address)] ?? null;        return $known !== null && $known->slaveOf !== null ? $known : new FakeXapi($address);    }    /** Forgets every demo server (tests). */    public static function forget(): void    {        self::$fakes = [];    }    public function isSlave(): bool    {        return $this->slaveOf !== null;    }    /** The demo pool at its own address, "Demo pool 2" at 'xcp-lab.local', a standalone host anywhere else; registered under its address. */    private function buildFor(): void    {        if (self::isMainDemo($this->address)) {            $this->buildMainDemo();             // MigrationOps        } elseif (self::isSecondDemo($this->address)) {            $this->buildSecondDemo();           // "Demo pool 2" (MigrationOps)        } else {            $this->buildStandalone();        }        self::$fakes[strtolower($this->address)] = $this;    }    private static function findByAddress(string $address): ?FakeXapi    {        $key = strtolower($address);        if (isset(self::$fakes[$key]) && self::$fakes[$key]->slaveOf === null) {            return self::$fakes[$key];        }        foreach (self::$fakes as $fake) {            foreach ($fake->db['host'] ?? [] as $host) {                if ($fake->slaveOf === null && strtolower((string)$host['address']) === $key) {                    return $fake;                }            }        }        return null;    }    private function masterRef(): string    {        $pool = array_key_first($this->db['pool'] ?? []);        return $pool === null ? '' : (string)$this->db['pool'][$pool]['master'];    }    private function refuseIfSlave(): void    {        if ($this->slaveOf !== null) {            throw new XapiError(['HOST_IS_SLAVE', $this->slaveOf]);        }    }    private function tickPool(float $dt): void    {        if ($this->dropIn > 0) {            $this->dropIn -= $dt;            if ($this->dropIn <= 0) {                // xapi restarted as a pool member: the sessions of this host are gone, its event.from calls cut                $this->sessions = [];                $this->close();            }        }    }    /** /rrd_updates answers for the host it is asked on (the master by default), as each XAPI host only has its own figures. */    private function rrdAnswersFor(array $host, ?string $hostUuid): bool    {        $want = $hostUuid ?? (string)($this->db['host'][$this->masterRef()]['uuid'] ?? '');        return (string)$host['uuid'] === $want;    }    // a standalone host ------------------------------------------------------------------------------------------------    private function buildStandalone(): void    {        $n = preg_match('/(\d+)/', $this->address, $m) ? max(2, (int)$m[1] % 90) : 2;        // its refs and uuids differ from the demo pool's (they meet in one database after a join)        $this->nextRef = 0x100000 * $n + 1;        $this->seed = 7 + 7919 * $n;        $name = sprintf('xcp-ng-%02d', $n + 1);        $ip = '192.168.1.' . (11 + $n);        $net0 = $this->add('network', ['name_label' => 'Pool-wide network associated with eth0', 'name_description' => '', 'bridge' => 'xenbr0', 'MTU' => 1500, 'PIFs' => [], 'VIFs' => []]);        $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']]);        $metrics = $this->add('host_metrics', ['memory_total' => 64 * self::GIB, 'memory_free' => 58 * self::GIB, 'live' => true, 'last_updated' => $this->date()]);        $host = $this->add('host', ['name_label' => $name, 'name_description' => 'Rack B, unit 3', 'hostname' => $name, 'address' => $ip, 'enabled' => true,            'metrics' => $metrics, 'resident_VMs' => [], 'PIFs' => [], 'PBDs' => [], 'memory_overhead' => 380 * self::MIB, 'API_version_major' => 2, 'API_version_minor' => 21,            'cpu_info' => ['cpu_count' => '12', 'socket_count' => '1', '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 R440', 'system-serial-number' => 'CN8' . (4100 + $n)],            'other_config' => ['iscsi_iqn' => "iqn.2024-01.local.demo:$name", 'agent_start_time' => (string)($this->now - 86400 * 3), 'boot_time' => (string)($this->now - 86400 * 9)],            'iscsi_iqn' => "iqn.2024-01.local.demo:$name", 'license_params' => ['sku_type' => 'free'], 'edition' => 'xcp-ng', 'logging' => [], 'suspend_image_sr' => 'OpaqueRef:NULL', 'crash_dump_sr' => 'OpaqueRef:NULL',            'external_auth_type' => '', 'ssl_legacy' => false, 'tls_verification_enabled' => true, 'chipset_info' => ['iommu' => 'true'], 'control_domain' => '']);        $pm = $this->add('pif_metrics', ['speed' => 1000, 'duplex' => true, 'carrier' => true, 'vendor_name' => 'Intel Corporation', 'device_name' => 'I350 Gigabit Network Connection', 'pci_bus_path' => '0000:01:00.0']);        $pif = $this->add('pif', ['device' => 'eth0', 'host' => $host, 'network' => $net0, 'MAC' => sprintf('00:1b:21:3a:%02x:10', $n), 'MTU' => 1500, 'VLAN' => -1, 'IP' => $ip,            'netmask' => '255.255.255.0', 'gateway' => '192.168.1.1', 'DNS' => '192.168.1.1', 'ip_configuration_mode' => 'Static', 'management' => true, 'currently_attached' => true,            'physical' => true, 'metrics' => $pm, 'bond_master_of' => [], 'bond_slave_of' => 'OpaqueRef:NULL']);        $this->append('host', $host, 'PIFs', $pif);        $this->append('network', $net0, 'PIFs', $pif);        $lsr = $this->add('sr', ['name_label' => 'Local storage', 'name_description' => "Local storage on $name", 'type' => 'ext', 'content_type' => 'user', 'shared' => false,            'physical_size' => 900 * self::GIB, 'physical_utilisation' => 130 * self::GIB, 'virtual_allocation' => 160 * self::GIB, 'PBDs' => [], 'VDIs' => [], 'other_config' => ['i18n-key' => 'local-storage']]);        $pbd = $this->add('pbd', ['host' => $host, 'SR' => $lsr, 'currently_attached' => true, 'device_config' => ['device' => '/dev/sda3']]);        $this->append('sr', $lsr, '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' => 4 * self::GIB, 'vcpus' => 8, 'os' => 'XCP-ng 8.3']);        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 ([['staging-web', 'Staging web server', 4, 2, 40], ['build-agent', 'CI build agent', 8, 4, 80]] as $i => [$vmName, $desc, $memG, $vcpus, $disk]) {            $this->addVm(['name_label' => $vmName, 'description' => $desc, 'power_state' => 'Halted', 'resident_on' => 'OpaqueRef:NULL', 'affinity' => $host, 'memory' => $memG * self::GIB,                'vcpus' => $vcpus, 'os' => '', 'disk' => $disk, 'sr' => $lsr, 'networks' => [$net0], 'index' => 30 + 10 * $n + $i, 'tags' => ['staging']]);        }        $this->add('pool', ['name_label' => '', 'name_description' => '', 'master' => $host, 'default_SR' => $lsr, 'suspend_image_SR' => $lsr, 'crash_dump_SR' => $lsr, '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' => 'GenuineIntel']]);        $this->log = [];        $this->eventId = 100;    }    // the calls ------------------------------------------------------------------------------------------------------------    /** @param list<mixed> $p @return array{0: float, 1: \Closure, 2: string, 3: string}|null */    private function poolOperation(string $method, array $p): ?array    {        $pool = (string)array_key_first($this->db['pool'] ?? []);        if ($method === 'join') {            return [3.0, function () use ($p): string {                $this->join((string)($p[0] ?? ''), (string)($p[1] ?? ''), (string)($p[2] ?? ''));                return '';            }, 'Joining a pool', $this->masterRef()];        }        if ($method === 'eject' || $method === 'designate_new_master') {            $host = (string)($p[0] ?? '');            $this->record('host', $host);            return $method === 'eject'                ? [3.0, function () use ($host): string {                    $this->eject($host);                    return '';                }, 'Ejecting a server from the pool', $host]                : [2.0, function () use ($host, $pool): string {                    $this->set('pool', $pool, ['master' => $host]);                    return '';                }, 'Designating a new pool master', $host];        }        return null;    }    private function join(string $masterAddress, string $user, string $password): void    {        $master = self::findByAddress($masterAddress);        if ($master === null || $master === $this) {            throw new XapiError(['POOL_JOINING_HOST_CONNECTION_FAILED']);        }        if ($user !== self::USER || $password !== self::PASSWORD) {            throw new XapiError(['SESSION_AUTHENTICATION_FAILED', $user, 'Authentication failure']);        }        if (count($this->db['host'] ?? []) > 1) {            throw new XapiError(['JOINING_HOST_CANNOT_BE_MASTER_OF_OTHER_HOSTS']);        }        foreach ($this->db['vm'] ?? [] as $vm) {            if (\App\Model\Cache::isRealVm($vm) && in_array($vm['power_state'], ['Running', 'Paused', 'Suspended'], true)) {                throw new XapiError(['JOINING_HOST_CANNOT_HAVE_RUNNING_OR_SUSPENDED_VMS']);            }        }        $master->adoptHost($this);        $this->slaveOf = $master->address;        $this->dropIn = 1.5;          // the task's end still reaches the client, then the sessions go    }    /** The host of a standalone server becomes a member of this pool, with its local storage and its VMs. */    private function adoptHost(FakeXapi $from): void    {        $host = $from->masterRef();        $this->joinedFrom[$host] = strtolower($from->address);        // the networks of the new host are the pool's ones of the same device (eth0 -> the pool's eth0 network), as XAPI does        $poolNet = [];        foreach ($this->db['pif'] ?? [] as $pif) {            if ((int)$pif['VLAN'] === -1) {                $poolNet[(string)$pif['device']] ??= (string)$pif['network'];            }        }        foreach ($this->db['network'] ?? [] as $ref => $net) {            $poolNet['bridge:' . $net['bridge']] ??= $ref;        }        $netMap = [];        foreach ($from->db['network'] ?? [] as $ref => $net) {            $device = null;            foreach ($from->db['pif'] ?? [] as $pif) {                if ($pif['network'] === $ref && (int)$pif['VLAN'] === -1) {                    $device = (string)$pif['device'];                }            }            $netMap[$ref] = ($device !== null ? $poolNet[$device] ?? null : null) ?? $poolNet['bridge:' . $net['bridge']] ?? null;            if ($netMap[$ref] === null) {                $this->add('network', ['PIFs' => [], 'VIFs' => []] + $net, $ref);                $netMap[$ref] = $ref;            }        }        $templates = [];        foreach ($from->db['vm'] ?? [] as $ref => $vm) {            if (!empty($vm['is_a_template'])) {                $templates[$ref] = true;          // the pool has its own templates            }        }        foreach (['host_metrics', 'host', 'pif_metrics', 'pif', 'sr', 'pbd', 'vdi', 'vm_metrics', 'vm_guest_metrics', 'vm', 'vbd', 'vif', 'console'] as $class) {            foreach ($from->db[$class] ?? [] as $ref => $record) {                if (($class === 'vm' && isset($templates[$ref])) || ($class === 'console' && isset($templates[(string)$record['VM']]))) {                    continue;                }                if ($class === 'pif' || $class === 'vif') {                    $record['network'] = $netMap[(string)$record['network']] ?? $record['network'];                }                $this->add($class, $record, $ref);                if ($class === 'pif' || $class === 'vif') {                    $this->append('network', (string)$record['network'], $class === 'pif' ? 'PIFs' : 'VIFs', $ref);                }            }        }        // the pool's shared storage is plugged on the new host too        foreach ($this->db['sr'] ?? [] as $sr => $record) {            if (!empty($record['shared'])) {                $pbd = $this->add('pbd', ['host' => $host, 'SR' => $sr, 'currently_attached' => true, 'device_config' => []]);                $this->append('sr', $sr, 'PBDs', $pbd);                $this->append('host', $host, 'PBDs', $pbd);            }        }    }    private function eject(string $host): void    {        if ($host === $this->masterRef()) {            throw new XapiError(['OPERATION_NOT_ALLOWED', 'The pool master cannot be ejected: designate another master first.']);        }        foreach ($this->db['host'][$host]['resident_VMs'] as $vm) {            if (empty($this->db['vm'][$vm]['is_control_domain'])) {                throw new XapiError(['HOST_IN_USE', $host, 'VM', $vm]);            }        }        $record = $this->db['host'][$host];        // its local storage goes, with the VMs whose disks are there        $localSrs = [];        foreach ($record['PBDs'] as $pbd) {            $sr = (string)($this->db['pbd'][$pbd]['SR'] ?? '');            if (isset($this->db['sr'][$sr]) && empty($this->db['sr'][$sr]['shared'])) {                $localSrs[$sr] = true;            } elseif (isset($this->db['sr'][$sr])) {                $this->removeFrom('sr', $sr, 'PBDs', $pbd);            }            $this->delete('pbd', $pbd);        }        foreach ($this->db['vm'] ?? [] as $vm => $v) {            $onLocal = !empty($v['is_control_domain']) && $v['resident_on'] === $host;            foreach ($v['VBDs'] as $vbd) {                $onLocal = $onLocal || isset($localSrs[(string)($this->db['vdi'][(string)($this->db['vbd'][$vbd]['VDI'] ?? '')]['SR'] ?? '')]);            }            if ($onLocal) {                foreach ($v['VBDs'] as $vbd) {                    $this->delete('vbd', $vbd);                }                foreach ($v['VIFs'] as $vif) {                    $this->removeFrom('network', (string)$this->db['vif'][$vif]['network'], 'VIFs', $vif);                    $this->delete('vif', $vif);                }                foreach ((array)($v['consoles'] ?? []) as $console) {                    $this->delete('console', (string)$console);                }                $this->delete('vm', $vm);            } elseif ($v['affinity'] === $host) {                $this->set('vm', $vm, ['affinity' => 'OpaqueRef:NULL']);            }        }        foreach (array_keys($localSrs) as $sr) {            foreach ($this->db['sr'][$sr]['VDIs'] as $vdi) {                $this->delete('vdi', $vdi);            }            $this->delete('sr', $sr);        }        foreach ($record['PIFs'] as $pif) {            $this->removeFrom('network', (string)$this->db['pif'][$pif]['network'], 'PIFs', $pif);            $this->delete('pif_metrics', (string)$this->db['pif'][$pif]['metrics']);            $this->delete('pif', $pif);        }        $this->delete('host_metrics', (string)$record['metrics']);        $this->delete('host', $host);        // it reboots as a standalone host with an empty database: a new server at its old demo address        $origin = $this->joinedFrom[$host] ?? null;        if ($origin !== null) {            unset(self::$fakes[$origin], $this->joinedFrom[$host]);            new FakeXapi($origin);        }    }}