client === null) { $done->settle(null, new XapiError(['TRANSPORT', 'Not connected'])); return $done; } $c->client->call('SR.probe', $host, self::map($deviceConfig), $type, self::map([]))->then(fn ($xml) => $done->settle((string)$xml), function (\Throwable $e) use ($done): void { if ($e instanceof XapiError && in_array($e->code(), ['SR_BACKEND_FAILURE_96', 'SR_BACKEND_FAILURE_107'], true) && str_contains((string)($e->failure[3] ?? ''), '<')) { $done->settle((string)$e->failure[3]); return; } $done->settle(null, $e); }); return $done; } /** @return list the LUNs of a probe's XML */ public static function luns(string $xml): array { $out = []; if (preg_match_all('#(.*?)#s', $xml, $m)) { foreach ($m[1] as $lun) { $get = fn (string $tag): string => preg_match("#<$tag>\\s*(.*?)\\s*#s", $lun, $v) ? $v[1] : ''; $out[] = ['lun' => $get('LUNid'), 'scsiId' => $get('SCSIid'), 'size' => (float)$get('size'), 'vendor' => $get('vendor')]; } } return $out; } /** @return list what a probe found under a tag (Path of NFS exports, TargetIQN of iSCSI targets) */ public static function values(string $xml, string $tag): array { return preg_match_all("#<$tag>\\s*(.*?)\\s*#s", $xml, $m) ? array_values(array_unique($m[1])) : []; } /** SR.create (one PBD per host of a shared SR, plugged by XAPI). @param array $p what NewSrForm closed with */ public static function createSr(Session $session, array $p): Task { $c = $session->connectionOf((string)$p['host']) ?? throw new \RuntimeException('Not connected'); return $c->run("Creating storage repository '{$p['name']}'", 'SR.create', [(string)$p['host'], self::map((array)$p['deviceConfig']), 0.0, (string)$p['name'], (string)$p['description'], (string)$p['type'], (string)$p['content'], (bool)$p['shared'], self::map((array)$p['smConfig'])], (string)$p['host']); } /** @return list the PBDs of an SR, the pool master's last (unplugged last, plugged first, as XenCenter does) */ private static function pbds(Connection $c, string $sr, ?bool $attached): array { $master = (string)$c->cache->master(); $out = []; foreach ((array)$c->cache->field($sr, 'PBDs', []) as $pbd) { $pbd = (string)$pbd; if ($attached === null || (bool)$c->cache->field($pbd, 'currently_attached') === $attached) { $out[] = $pbd; } } usort($out, fn ($a, $b) => ($c->cache->field($a, 'host') === $master) <=> ($c->cache->field($b, 'host') === $master)); return $out; } /** @return list<\Closure> PBD.unplug on each host that has the SR plugged */ private static function unplugSteps(Connection $c, string $sr): array { $name = $c->cache->name($sr); return array_map(fn (string $pbd) => fn () => $c->run("Detaching '$name' from '" . $c->cache->name((string)$c->cache->field($pbd, 'host')) . "'", 'PBD.unplug', [$pbd], $sr), self::pbds($c, $sr, true)); } public static function detachSr(Connection $c, string $sr): Task { return Chain::run(self::unplugSteps($c, $sr)); } public static function reattachSr(Connection $c, string $sr): Task { $name = $c->cache->name($sr); return Chain::run(array_map(fn (string $pbd) => fn () => $c->run("Attaching '$name' to '" . $c->cache->name((string)$c->cache->field($pbd, 'host')) . "'", 'PBD.plug', [$pbd], $sr), array_reverse(self::pbds($c, $sr, false)))); } /** Forget (the records go, the data stays) or destroy (the data goes too): detached first. */ public static function removeSr(Connection $c, string $sr, bool $destroy): Task { $name = $c->cache->name($sr); return Chain::run([...self::unplugSteps($c, $sr), fn () => $destroy ? $c->run("Destroying storage repository '$name'", 'SR.destroy', [$sr], $sr) : $c->run("Forgetting storage repository '$name'", 'SR.forget', [$sr], $sr)]); } /** The default SR of the pool (new VMs' disks), and with it the suspend and crash dump SRs, as XenCenter sets them. */ public static function setDefaultSr(Connection $c, string $sr): Task { $pool = (string)$c->cache->poolRef(); return Chain::run([ fn () => $c->run("Setting '" . $c->cache->name($sr) . "' as the default storage", 'pool.set_default_SR', [$pool, $sr], $sr), fn () => $c->call('pool.set_suspend_image_SR', $pool, $sr), fn () => $c->call('pool.set_crash_dump_SR', $pool, $sr), ]); } // networks ------------------------------------------------------------------------------------------------------------ /** network.create, then the VLAN on a NIC of every host (External) or a bond of NICs on each host (Bonded). @param array $p */ public static function createNetwork(Session $session, array $p): Task { $c = $session->connectionOf((string)$p['pool']) ?? throw new \RuntimeException('Not connected'); $name = (string)$p['name']; $record = ['name_label' => $name, 'name_description' => (string)$p['description'], 'MTU' => (int)$p['mtu'], 'other_config' => ['automatic' => !empty($p['auto']) ? 'true' : 'false'], 'tags' => []]; return Chain::run([ fn () => $c->run("Creating network '$name'", 'network.create', [$record], (string)$p['pool']), fn (string $network) => match ((string)$p['type']) { 'external' => Chain::keep($c->run("Creating VLAN {$p['vlan']} on {$p['device']} for '$name'", 'pool.create_VLAN', [(string)$p['device'], $network, (int)$p['vlan']], $network), $network), 'bond' => Chain::keep(Chain::run(self::bondSteps($c, $network, $name, (array)$p['members'], (string)$p['mode'])), $network), default => $network, }, ]); } /** @return list<\Closure> Bond.create on each host, with that host's NICs of these devices */ private static function bondSteps(Connection $c, string $network, string $name, array $devices, string $mode): array { $steps = []; foreach ($c->cache->hosts() as $host => $h) { $members = []; $mac = ''; foreach ($c->cache->pifsOf($host) as $pif => $r) { if (in_array($r['device'], $devices, true) && !empty($r['physical']) && (int)$r['VLAN'] === -1) { $members[] = $pif; $mac = $mac !== '' ? $mac : (string)$r['MAC']; } } $steps[] = fn () => $c->run("Creating bond '$name' on '{$h['name_label']}'", 'Bond.create', [$network, $members, $mac, $mode, self::map([])], $network); } return $steps; } /** Properties of a network: name, description, MTU. @param array $p */ public static function networkProperties(Session $session, array $p): Task { $net = (string)$p['network']; $c = $session->connectionOf($net) ?? throw new \RuntimeException('Not connected'); $r = $c->cache->get($net) ?? []; $steps = []; if ($p['name'] !== ($r['name_label'] ?? '')) { $steps[] = fn () => $c->call('network.set_name_label', $net, (string)$p['name']); } if ($p['description'] !== ($r['name_description'] ?? '')) { $steps[] = fn () => $c->call('network.set_name_description', $net, (string)$p['description']); } if ((int)$p['mtu'] !== (int)($r['MTU'] ?? 0)) { $steps[] = fn () => $c->run("Changing the MTU of '{$p['name']}' to {$p['mtu']}", 'network.set_MTU', [$net, (int)$p['mtu']], $net); } return Chain::run($steps); } /** Its VLAN interfaces and bonds first, then the network (XAPI refuses while a VIF uses it: NETWORK_CONTAINS_VIF). */ public static function removeNetwork(Connection $c, string $network): Task { $name = $c->cache->name($network); $steps = []; // used by VMs: XAPI is asked straight away and refuses, the VLANs and bonds stay as they are $inUse = (array)$c->cache->field($network, 'VIFs', []) !== []; foreach ($inUse ? [] : (array)$c->cache->field($network, 'PIFs', []) as $pif) { $r = $c->cache->get((string)$pif) ?? []; if ((array)($r['bond_master_of'] ?? []) !== []) { $bond = (string)$r['bond_master_of'][0]; $steps[] = fn () => $c->run("Removing the bond of '$name' on '" . $c->cache->name((string)$r['host']) . "'", 'Bond.destroy', [$bond], $network); } elseif ((int)($r['VLAN'] ?? -1) >= 0) { $steps[] = fn () => $c->run("Removing VLAN {$r['VLAN']} of '$name' on '" . $c->cache->name((string)$r['host']) . "'", 'PIF.destroy', [(string)$pif], $network); } } $steps[] = fn () => $c->run("Removing network '$name'", 'network.destroy', [$network], $network); return Chain::run($steps); } // virtual disks ------------------------------------------------------------------------------------------------------- /** The VM a disk belongs to (its first VBD's), or null. */ public static function vmOfDisk(Connection $c, string $vdi): ?string { foreach ((array)$c->cache->field($vdi, 'VBDs', []) as $vbd) { $vm = (string)$c->cache->field((string)$vbd, 'VM', ''); if ($vm !== '') { return $vm; } } return null; } /** A disk in use by a running VM moves live (storage live migration). */ public static function isLive(Connection $c, string $vdi): bool { foreach ((array)$c->cache->field($vdi, 'VBDs', []) as $vbd) { if (!empty($c->cache->field((string)$vbd, 'currently_attached')) && $c->cache->field((string)$c->cache->field((string)$vbd, 'VM', ''), 'power_state') === 'Running') { return true; } } return false; } /** * Moves a disk to another SR. VM running: VDI.pool_migrate (XAPI moves it under the VM). Else: VDI.copy, the VBDs re-made on the * copy (same position, bootable flag, mode), the original destroyed. */ public static function moveDisk(Session $session, string $vdi, string $sr): Task { $c = $session->connectionOf($vdi) ?? throw new \RuntimeException('Not connected'); $name = $c->cache->name($vdi); $to = $c->cache->name($sr); $vm = self::vmOfDisk($c, $vdi) ?? $vdi; if (self::isLive($c, $vdi)) { return $c->run("Migrating virtual disk '$name' to '$to'", 'VDI.pool_migrate', [$vdi, $sr, self::map([])], $vm); } $vbds = []; foreach ((array)$c->cache->field($vdi, 'VBDs', []) as $vbd) { if (($b = $c->cache->get((string)$vbd)) !== null) { $vbds[(string)$vbd] = $b; } } $steps = [fn () => $c->run("Copying virtual disk '$name' to '$to'", 'VDI.copy', [$vdi, $sr], $vm)]; foreach ($vbds as $ref => $b) { $steps[] = fn (string $copy) => Chain::keep($c->run("Detaching '$name' from '" . $c->cache->name((string)$b['VM']) . "'", 'VBD.destroy', [$ref], (string)$b['VM']), $copy); $steps[] = fn (string $copy) => Chain::keep($c->run("Attaching the moved '$name' to '" . $c->cache->name((string)$b['VM']) . "'", 'VBD.create', [self::vbdRecord((string)$b['VM'], $copy, (string)$b['userdevice'], (bool)$b['bootable'], (string)$b['mode'], (bool)($b['unpluggable'] ?? true))], (string)$b['VM']), $copy); } $steps[] = fn (string $copy) => Chain::keep($c->run("Deleting the original of '$name'", 'VDI.destroy', [$vdi], $vm), $copy); return Chain::run($steps); } /** @return array a VBD record for VBD.create */ public static function vbdRecord(string $vm, string $vdi, string $userdevice, bool $bootable = false, string $mode = 'RW', bool $unpluggable = true): array { return ['VM' => $vm, 'VDI' => $vdi, 'userdevice' => $userdevice, 'bootable' => $bootable, 'mode' => $mode, 'type' => 'Disk', 'unpluggable' => $unpluggable, 'empty' => false, 'other_config' => self::map([]), 'qos_algorithm_type' => '', 'qos_algorithm_params' => self::map([])]; } /** The first free position of a VM's disks (3 is the DVD drive's, by custom). */ public static function freeUserdevice(Connection $c, string $vm): string { $used = []; foreach ((array)$c->cache->field($vm, 'VBDs', []) as $vbd) { $used[] = (string)$c->cache->field((string)$vbd, 'userdevice', ''); } for ($n = 0; $n < 16; $n++) { if ($n !== 3 && !in_array((string)$n, $used, true)) { return (string)$n; } } return '15'; } /** VBD.create (+ VBD.plug when the VM runs: the disk shows at once in the guest). */ public static function attachDisk(Session $session, string $vm, string $vdi): Task { $c = $session->connectionOf($vm) ?? throw new \RuntimeException('Not connected'); $name = $c->cache->name($vdi); $vmName = $c->cache->name($vm); $running = $c->cache->field($vm, 'power_state') === 'Running'; return Chain::run([ fn () => $c->run("Attaching '$name' to '$vmName'", 'VBD.create', [self::vbdRecord($vm, $vdi, self::freeUserdevice($c, $vm))], $vm), fn (string $vbd) => $running ? Chain::keep($c->run("Plugging '$name' into '$vmName'", 'VBD.plug', [$vbd], $vm), $vbd) : $vbd, ]); } /** VDI.create on an SR, then attached to the VM. @param array $p */ public static function addDisk(Session $session, array $p): Task { $vm = (string)$p['vm']; $c = $session->connectionOf($vm) ?? throw new \RuntimeException('Not connected'); $record = ['name_label' => (string)$p['name'], 'name_description' => (string)$p['description'], 'SR' => (string)$p['sr'], 'virtual_size' => (float)$p['size'], 'type' => 'user', 'sharable' => false, 'read_only' => false, 'other_config' => self::map([]), 'xenstore_data' => self::map([]), 'sm_config' => self::map([]), 'tags' => []]; return Chain::run([ fn () => $c->run("Creating virtual disk '{$p['name']}' on '" . $c->cache->name((string)$p['sr']) . "'", 'VDI.create', [$record], $vm), fn (string $vdi) => self::attachDisk($session, $vm, $vdi), ]); } /** The disk leaves the VM (unplugged first when it runs); delete: the disk is destroyed too. */ public static function detachDisk(Connection $c, string $vbd, bool $delete): Task { $vm = (string)$c->cache->field($vbd, 'VM', ''); $vdi = (string)$c->cache->field($vbd, 'VDI', ''); $name = $c->cache->name($vdi); $vmName = $c->cache->name($vm); $steps = []; if (!empty($c->cache->field($vbd, 'currently_attached'))) { $steps[] = fn () => $c->run("Unplugging '$name' from '$vmName'", 'VBD.unplug', [$vbd], $vm); } $steps[] = fn () => $c->run("Detaching '$name' from '$vmName'", 'VBD.destroy', [$vbd], $vm); if ($delete) { $steps[] = fn () => $c->run("Deleting virtual disk '$name'", 'VDI.destroy', [$vdi], $vm); } return Chain::run($steps); } // cross-pool migration --------------------------------------------------------------------------------------------------- /** * On the target pool host.migrate_receive (its map says where to send), then on the source VM.assert_can_migrate and * Async.VM.migrate_send with the SR of each disk and the network of each VIF. @param array $p what CrossPoolMigrateForm closed with */ public static function crossPoolMigrate(Session $session, array $p): Task { $vm = (string)$p['vm']; $source = $session->connectionOf($vm) ?? throw new \RuntimeException('Not connected'); $target = $session->connections[(int)$p['target']] ?? throw new \RuntimeException('The other pool is not connected'); $host = (string)$p['host']; $network = ''; foreach ($target->cache->pifsOf($host) as $pif) { if (!empty($pif['management'])) { $network = (string)$pif['network']; // the VM's memory crosses on the management network } } $live = $source->cache->field($vm, 'power_state') === 'Running'; $vdiMap = self::map((array)$p['vdiMap']); $vifMap = self::map((array)$p['vifMap']); $name = $source->cache->name($vm); return Chain::run([ fn () => $target->call('host.migrate_receive', $host, $network, self::map([])), fn (array $dest) => Chain::keep($source->call('VM.assert_can_migrate', $vm, $dest, $live, $vdiMap, $vifMap, self::map([])), $dest), fn (array $dest) => $source->run("Migrating VM '$name' to '" . $target->cache->name($host) . "' in '" . $target->name() . "'", 'VM.migrate_send', [$vm, $dest, $live, $vdiMap, $vifMap, self::map([])], $vm), ]); } }