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 = '';
foreach (['/export/xcp', '/export/backup', '/export/iso'] as $path) {
$xml .= "$server$path*";
}
return $xml . '';
}
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',
'0' . $dc['target'] . 'iqn.2024-01.local.demo:san']);
}
$xml = '2iscsi_tcp';
foreach ([[0, 500], [1, 2048]] as [$lun, $gib]) {
$xml .= 'LIO-ORGdemo' . $lun . '' . $lun . '' . sprintf('%.0f', $gib * self::GIB)
. '' . self::scsiId($iqn, $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 . '']);
}
return '';
}
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);
}
}