<?phpnamespace App\Xapi\Fake;use App\Xapi\XapiError;/** * The storage calls of the demo pool (used by FakeXapi), as XAPI answers them: SR.probe / SR.create (NFS, SMB, ISO, iSCSI, local * EXT / LVM), PBD.plug / unplug (detach, reattach), SR.forget / SR.destroy, pool.set_default_SR, VDI.create / copy / pool_migrate, * VBD.create / destroy / plug / unplug. Each returns [seconds, what it does, label, the object] like FakeXapi::operation(). */trait StorageOps{ /** @return array{0: float, 1: \Closure, 2: string, 3: string}|null */ private function storageOperation(string $class, string $method, array $p): ?array { $ref = is_scalar($p[0] ?? null) ? (string)$p[0] : ''; switch ("$class.$method") { case 'sr.probe': $this->record('host', $ref); return [1.0, fn (): string => $this->srProbe((array)($p[1] ?? []), (string)($p[2] ?? '')), 'Scanning for storage', $ref]; case 'sr.create': $this->record('host', $ref); return [3.0, fn (): string => $this->srCreate($ref, (array)($p[1] ?? []), (string)($p[3] ?? ''), (string)($p[4] ?? ''), (string)($p[5] ?? ''), (string)($p[6] ?? 'user'), (bool)($p[7] ?? false), (array)($p[8] ?? [])), 'Creating storage repository', $ref]; case 'sr.forget': case 'sr.destroy': $this->record('sr', $ref); return [$method === 'destroy' ? 2.5 : 1.0, function () use ($ref): string { $this->srRemove($ref); return ''; }, $method === 'destroy' ? 'Destroying storage repository' : 'Forgetting storage repository', $ref]; case 'pbd.plug': case 'pbd.unplug': $this->record('pbd', $ref); return [1.5, function () use ($ref, $method): string { $sr = (string)$this->db['pbd'][$ref]['SR']; if ($method === 'unplug') { foreach ((array)($this->db['sr'][$sr]['VDIs'] ?? []) as $vdi) { foreach ((array)($this->db['vdi'][$vdi]['VBDs'] ?? []) as $vbd) { if (!empty($this->db['vbd'][$vbd]['currently_attached'])) { throw new XapiError(['VDI_IN_USE', (string)$vdi, 'unplug']); } } } } $this->set('pbd', $ref, ['currently_attached' => $method === 'plug']); return ''; }, $method === 'plug' ? 'Plugging a storage connection' : 'Unplugging a storage connection', (string)$this->db['pbd'][$ref]['SR']]; case 'pool.set_default_SR': case 'pool.set_suspend_image_SR': case 'pool.set_crash_dump_SR': $this->record('pool', $ref); $this->record('sr', (string)($p[1] ?? '')); return [0.2, function () use ($ref, $method, $p): string { $this->set('pool', $ref, [substr($method, 4) => (string)$p[1]]); return ''; }, 'Setting the default storage', $ref]; case 'vdi.create': $r = (array)$p[0]; $this->record('sr', (string)($r['SR'] ?? '')); return [1.0, fn (): string => $this->vdiCreate($r), 'Creating a virtual disk', (string)$r['SR']]; case 'vdi.copy': $this->record('vdi', $ref); $this->record('sr', (string)($p[1] ?? '')); return [3.0, fn (): string => $this->vdiCopy($ref, (string)$p[1]), 'Copying a virtual disk', $ref]; case 'vdi.pool_migrate': $this->record('vdi', $ref); $this->record('sr', (string)($p[1] ?? '')); return [5.0, fn (): string => $this->vdiMigrate($ref, (string)$p[1]), 'Migrating a virtual disk', $ref]; case 'vbd.create': $r = (array)$p[0]; $this->record('vm', (string)($r['VM'] ?? '')); return [0.5, fn (): string => $this->vbdCreate($r), 'Attaching a virtual disk', (string)$r['VM']]; case 'vbd.destroy': $this->record('vbd', $ref); return [0.5, function () use ($ref): string { if (!empty($this->db['vbd'][$ref]['currently_attached'])) { throw new XapiError(['DEVICE_ALREADY_ATTACHED', $ref]); } $this->vbdRemove($ref); return ''; }, 'Detaching a virtual disk', (string)$this->db['vbd'][$ref]['VM']]; case 'vbd.plug': case 'vbd.unplug': $vbd = $this->record('vbd', $ref); return [1.0, function () use ($ref, $vbd, $method): string { $vm = (string)$vbd['VM']; $state = (string)($this->db['vm'][$vm]['power_state'] ?? ''); if ($state !== 'Running') { throw new XapiError(['VM_BAD_POWER_STATE', $vm, 'running', strtolower($state)]); } if ($method === 'unplug' && empty($this->db['vbd'][$ref]['unpluggable'])) { throw new XapiError(['VBD_NOT_UNPLUGGABLE', $ref]); } $this->set('vbd', $ref, ['currently_attached' => $method === 'plug']); return ''; }, $method === 'plug' ? 'Plugging a virtual disk' : 'Unplugging a virtual disk', (string)$vbd['VM']]; } return null; } /** What SR.probe finds: the exports of an NFS server, the LUNs of an iSCSI target (in the failure, as XAPI does). */ private function srProbe(array $dc, string $type): string { if ($type === 'nfs' || ($type === 'iso' && str_contains((string)($dc['location'] ?? ''), ':/'))) { $server = (string)($dc['server'] ?? strstr((string)($dc['location'] ?? ''), ':', true)); if ($server === '') { throw new XapiError(['SR_BACKEND_FAILURE_101', '', 'NFS server not specified']); } $xml = '<?xml version="1.0" ?><nfs-exports>'; foreach (['/export/xcp', '/export/backup', '/export/iso'] as $path) { $xml .= "<Export><Target>$server</Target><Path>$path</Path><Accesslist>*</Accesslist></Export>"; } return $xml . '</nfs-exports>'; } if ($type === 'lvmoiscsi') { if ((string)($dc['target'] ?? '') === '') { throw new XapiError(['SR_BACKEND_FAILURE_95', '', 'The request is missing the server parameter']); } $iqn = (string)($dc['targetIQN'] ?? ''); if (!str_starts_with($iqn, 'iqn.')) { // XAPI answers with the target's IQNs in the failure (the "discovery") throw new XapiError(['SR_BACKEND_FAILURE_96', '', 'The request is missing or has an incorrect target IQN parameter', '<?xml version="1.0" ?><iscsi-target-iqns><TGT><Index>0</Index><IPAddress>' . $dc['target'] . '</IPAddress><TargetIQN>iqn.2024-01.local.demo:san</TargetIQN></TGT></iscsi-target-iqns>']); } $xml = '<?xml version="1.0" ?><iscsi-target><Adapter><host>2</host><name>iscsi_tcp</name></Adapter>'; foreach ([[0, 500], [1, 2048]] as [$lun, $gib]) { $xml .= '<LUN><vendor>LIO-ORG</vendor><serial>demo' . $lun . '</serial><LUNid>' . $lun . '</LUNid><size>' . sprintf('%.0f', $gib * self::GIB) . '</size><SCSIid>' . self::scsiId($iqn, $lun) . '</SCSIid></LUN>'; } // the LUNs come back in the failure SR_BACKEND_FAILURE_107 ("SCSIid missing"), as on a real host throw new XapiError(['SR_BACKEND_FAILURE_107', '', 'The SCSIid parameter is missing or incorrect', $xml . '</iscsi-target>']); } return '<?xml version="1.0" ?><SRlist></SRlist>'; } private static function scsiId(string $iqn, int $lun): string { return '36001405' . sprintf('%08x', crc32($iqn) & 0x7fffffff) . sprintf('%016x', $lun + 1); } private function srCreate(string $host, array $dc, string $name, string $description, string $type, string $content, bool $shared, array $sm): string { $size = match ($type) { 'nfs' => 2048 * self::GIB, 'smb' => 1024 * self::GIB, 'iso' => 300 * self::GIB, 'ext', 'lvm' => 900 * self::GIB, 'lvmoiscsi' => 0.0, default => throw new XapiError(['SR_UNKNOWN_DRIVER', $type]), }; $describe = ''; switch ($type) { case 'nfs': if ((string)($dc['server'] ?? '') === '' || !str_starts_with((string)($dc['serverpath'] ?? ''), '/')) { throw new XapiError(['SR_BACKEND_FAILURE_73', '', 'NFS mount error [opterr=the export path must start with /]']); } $describe = "NFS SR [{$dc['server']}:{$dc['serverpath']}]"; break; case 'smb': if (!preg_match('#^(//|\\\\\\\\)[^/\\\\]+[/\\\\].+#', (string)($dc['server'] ?? ''))) { throw new XapiError(['SR_BACKEND_FAILURE_111', '', 'CIFS mount error [opterr=the share is written //server/share]']); } $describe = "SMB SR [{$dc['server']}]"; break; case 'iso': if ((string)($dc['location'] ?? '') === '') { throw new XapiError(['SR_BACKEND_FAILURE_222', '', 'The ISO library location is missing']); } $describe = "ISO library [{$dc['location']}]"; break; case 'lvmoiscsi': $lun = null; foreach ([[0, 500], [1, 2048]] as [$n, $gib]) { if (self::scsiId((string)($dc['targetIQN'] ?? ''), $n) === (string)($dc['SCSIid'] ?? '')) { $lun = $gib; } } if ((string)($dc['target'] ?? '') === '' || $lun === null) { throw new XapiError(['SR_BACKEND_FAILURE_107', '', 'The SCSIid parameter is missing or incorrect']); } $size = $lun * self::GIB; $describe = "iSCSI SR [{$dc['target']} ({$dc['targetIQN']}; LUN)]"; break; default: $device = (string)($dc['device'] ?? ''); if (!str_starts_with($device, '/dev/')) { throw new XapiError(['SR_BACKEND_FAILURE_90', '', 'The request is missing the device parameter']); } foreach ((array)($this->db['host'][$host]['PBDs'] ?? []) as $pbd) { if (($this->db['pbd'][$pbd]['device_config']['device'] ?? '') === $device) { throw new XapiError(['SR_BACKEND_FAILURE_77', '', "Logical Volume group creation failed: $device is in use"]); } } } $sr = $this->add('sr', ['name_label' => $name, 'name_description' => $description !== '' ? $description : $describe, 'type' => $type, 'content_type' => $content, 'shared' => $shared, 'physical_size' => $size, 'physical_utilisation' => $type === 'iso' ? 14 * self::GIB : 0.0, 'virtual_allocation' => 0.0, 'PBDs' => [], 'VDIs' => [], 'sm_config' => $sm, 'other_config' => []]); foreach ($shared ? array_keys($this->db['host']) : [$host] as $h) { $pbd = $this->add('pbd', ['host' => $h, 'SR' => $sr, 'currently_attached' => true, 'device_config' => $dc]); $this->append('sr', $sr, 'PBDs', $pbd); $this->append('host', $h, 'PBDs', $pbd); } if ($type === 'iso') { foreach (['install-media.iso', 'drivers.iso'] as $k => $iso) { $this->addVdi($sr, $iso, (4 + 3 * $k) * self::GIB, 'user', true); } } return $sr; } /** SR.forget / SR.destroy: the connections must be unplugged first; the records go (the disks too). */ private function srRemove(string $sr): void { foreach ((array)$this->db['sr'][$sr]['PBDs'] as $pbd) { if (!empty($this->db['pbd'][$pbd]['currently_attached'])) { throw new XapiError(['SR_HAS_PBD', $sr]); } } foreach ((array)$this->db['sr'][$sr]['VDIs'] as $vdi) { foreach ((array)($this->db['vdi'][$vdi]['VBDs'] ?? []) as $vbd) { $this->vbdRemove((string)$vbd); } $this->delete('vdi', (string)$vdi); } foreach ((array)$this->db['sr'][$sr]['PBDs'] as $pbd) { $this->removeFrom('host', (string)$this->db['pbd'][$pbd]['host'], 'PBDs', (string)$pbd); $this->delete('pbd', (string)$pbd); } foreach ($this->db['pool'] as $pool => $record) { foreach (['default_SR', 'suspend_image_SR', 'crash_dump_SR'] as $field) { if (($record[$field] ?? '') === $sr) { $this->set('pool', $pool, [$field => 'OpaqueRef:NULL']); } } } $this->delete('sr', $sr); } private function srAttached(string $sr): void { foreach ((array)($this->db['sr'][$sr]['PBDs'] ?? []) as $pbd) { if (!empty($this->db['pbd'][$pbd]['currently_attached'])) { return; } } throw new XapiError(['SR_NO_PBDS', $sr]); } private function srRoom(string $sr, float $size): void { $r = $this->db['sr'][$sr]; if (($r['content_type'] ?? '') === 'iso') { throw new XapiError(['SR_OPERATION_NOT_SUPPORTED', $sr]); } if ((float)$r['physical_utilisation'] + $size > (float)$r['physical_size'] && str_starts_with((string)$r['type'], 'lvm')) { throw new XapiError(['SR_BACKEND_FAILURE_44', '', 'There is insufficient space']); } } private function vdiCreate(array $r): string { $sr = (string)$r['SR']; $size = (float)($r['virtual_size'] ?? 0); if ($size <= 0) { throw new XapiError(['INVALID_VALUE', 'virtual_size', (string)$size]); } $this->srAttached($sr); $this->srRoom($sr, $size); $vdi = $this->addVdi($sr, (string)($r['name_label'] ?? 'disk'), $size, (string)($r['type'] ?? 'user'), (bool)($r['read_only'] ?? false)); $this->set('vdi', $vdi, ['name_description' => (string)($r['name_description'] ?? ''), 'physical_utilisation' => 0.0, 'sharable' => (bool)($r['sharable'] ?? false)]); return $vdi; } private function vdiCopy(string $vdi, string $sr): string { $source = $this->record('vdi', $vdi); $this->srAttached($sr); $this->srRoom($sr, (float)$source['virtual_size']); $copy = $this->addVdi($sr, (string)$source['name_label'], (float)$source['virtual_size'], (string)$source['type'], (bool)$source['read_only']); $this->set('vdi', $copy, ['name_description' => (string)$source['name_description'], 'physical_utilisation' => (float)$source['physical_utilisation']]); return $copy; } /** Storage live migration: the disk of a running VM moves to another SR, its VBDs follow it. */ private function vdiMigrate(string $vdi, string $sr): string { $source = $this->record('vdi', $vdi); $running = false; $vm = ''; foreach ((array)$source['VBDs'] as $vbd) { $vm = (string)($this->db['vbd'][$vbd]['VM'] ?? ''); $running = $running || ($this->db['vm'][$vm]['power_state'] ?? '') === 'Running'; } if (!$running) { // a halted VM's disk is copied (VDI.copy), not migrated throw new XapiError(['VM_BAD_POWER_STATE', $vm, 'running', strtolower((string)($this->db['vm'][$vm]['power_state'] ?? 'halted'))]); } $new = $this->vdiCopy($vdi, $sr); foreach ((array)$source['VBDs'] as $vbd) { $this->set('vbd', (string)$vbd, ['VDI' => $new]); $this->append('vdi', $new, 'VBDs', (string)$vbd); } $this->removeFrom('sr', (string)$source['SR'], 'VDIs', $vdi); $this->delete('vdi', $vdi); return $new; } private function vbdCreate(array $r): string { $vm = (string)$r['VM']; $vdi = (string)($r['VDI'] ?? 'OpaqueRef:NULL'); $user = (string)($r['userdevice'] ?? ''); $type = (string)($r['type'] ?? 'Disk'); if ($vdi !== 'OpaqueRef:NULL') { $this->record('vdi', $vdi); } foreach ((array)$this->db['vm'][$vm]['VBDs'] as $vbd) { if ((string)($this->db['vbd'][$vbd]['userdevice'] ?? '') === $user) { throw new XapiError(['DEVICE_ALREADY_EXISTS', $user]); } } $ref = $this->addVbd($vm, $vdi, $user, $type, 'xvd' . chr(ord('a') + max(0, min(25, (int)$user))), false); $this->set('vbd', $ref, ['bootable' => (bool)($r['bootable'] ?? false), 'mode' => (string)($r['mode'] ?? 'RW'), 'unpluggable' => (bool)($r['unpluggable'] ?? true), 'empty' => $vdi === 'OpaqueRef:NULL']); return $ref; } private function vbdRemove(string $vbd): void { $r = $this->db['vbd'][$vbd] ?? null; if ($r === null) { return; } if (isset($this->db['vdi'][(string)$r['VDI']])) { $this->removeFrom('vdi', (string)$r['VDI'], 'VBDs', $vbd); } if (isset($this->db['vm'][(string)$r['VM']])) { $this->removeFrom('vm', (string)$r['VM'], 'VBDs', $vbd); } $this->delete('vbd', $vbd); }}