Sign in
EN
games/xencenter-xenadmin-xcp-jumphp / src / Xapi / Fake / StorageOps.php · commit fb1b7c9
<?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);    }}