로그인
KO
<?phpnamespace App\Feature\Storage;use App\Model\Chain;use App\Model\Connection;use App\Model\Session;use App\Xapi\XapiError;use JumPHP\Async\Task;/** * The XAPI sequences of the storage, network, disk and cross-pool commands, as XenCenter's actions make them. Every step that takes * time is an Async task (Connection::run: its progress in Events); the sequences stop at the first failure. * Empty XAPI maps go as {} (a PHP [] would be a JSON list). */final class Actions{    /** A XAPI map: {} when empty. */    public static function map(array $a): array|\stdClass    {        return $a === [] ? new \stdClass() : $a;    }    // storage repositories ------------------------------------------------------------------------------------------------    /**     * SR.probe: what a server finds at an address. Its answer is XML; for iSCSI XAPI gives it inside a failure (96: the target's IQNs,     * 107: its LUNs), as XenCenter reads it. The Task settles with the XML.     */    public static function probe(Connection $c, string $host, array $deviceConfig, string $type): Task    {        $done = new Task();        if ($c->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<array{lun: string, scsiId: string, size: float, vendor: string}> the LUNs of a probe's XML */    public static function luns(string $xml): array    {        $out = [];        if (preg_match_all('#<LUN>(.*?)</LUN>#s', $xml, $m)) {            foreach ($m[1] as $lun) {                $get = fn (string $tag): string => preg_match("#<$tag>\\s*(.*?)\\s*</$tag>#s", $lun, $v) ? $v[1] : '';                $out[] = ['lun' => $get('LUNid'), 'scsiId' => $get('SCSIid'), 'size' => (float)$get('size'), 'vendor' => $get('vendor')];            }        }        return $out;    }    /** @return list<string> 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*</$tag>#s", $xml, $m) ? array_values(array_unique($m[1])) : [];    }    /** SR.create (one PBD per host of a shared SR, plugged by XAPI). @param array<string, mixed> $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<string> 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<string, mixed> $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<string, mixed> $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<string, mixed> 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<string, mixed> $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<string, mixed> $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),        ]);    }}