<?phpnamespace App\Form;use App\Feature\Storage\Menus;use App\Gui\Units;use App\Model\Connection;use App\Model\Session;use JumPHP\Gui\Button;use JumPHP\Gui\ComboBox;use JumPHP\Gui\Container;use JumPHP\Gui\Form;use JumPHP\Gui\FormLoader;use JumPHP\Gui\Label;/** * Cross-pool migration (XenCenter's Migrate wizard to another pool), on one page: the destination pool (another connected server), * its server, the SR of each disk and the network of each VIF. What XAPI is asked (Feature\Storage\Actions::crossPoolMigrate): * host.migrate_receive on the destination, then VM.assert_can_migrate and Async.VM.migrate_send(vm, dest, live, vdi_map, vif_map, {}) * on the source. Closes with ['crossPool' => [vm, target (index of the connection), host, vdiMap, vifMap]]. * * @property Label $heading * @property Label $where * @property ComboBox $pool * @property ComboBox $host * @property Container $disks * @property Container $vifs * @property Label $how * @property Label $error * @property Button $migrate * @property Button $cancel */final class CrossPoolMigrateForm extends Form{ public function __construct(private Session $session) { parent::__construct(); } /** XAPI's answers, events and the console go on while this dialog is open. */ protected function tick(float $dt): void { $this->session->tick($dt); } private array $params = []; /** @return array<string, mixed> the VM's disks and VIFs, and what each other connected pool offers */ public static function paramsFor(Session $session, Connection $c, string $vm): array { $cache = $c->cache; $disks = []; foreach ($cache->disksOf($vm) as $d) { if ($d['vdi'] !== null) { $disks[] = ['vdi' => (string)$d['vbd']['VDI'], 'label' => (string)$d['vdi']['name_label'] . ' (' . Units::bytes((float)$d['vdi']['virtual_size']) . ')']; } } $vifs = []; foreach ($cache->vifsOf($vm) as $v) { $vifs[] = ['vif' => $v['ref'], 'label' => 'Interface ' . $v['vif']['device'] . ' (' . $cache->name((string)$v['vif']['network']) . ')', 'network' => $cache->name((string)$v['vif']['network'])]; } $targets = []; foreach (Menus::otherPools($c) as $index => $other) { $oc = $other->cache; $hosts = $srs = $networks = []; foreach ($oc->hosts() as $ref => $h) { if ($h['enabled'] ?? true) { $m = $oc->hostMemory($ref); $hosts[] = ['ref' => $ref, 'name' => $h['name_label'] . ' (' . Units::bytes($m['free']) . ' free)']; } } $default = (string)$oc->field((string)$oc->poolRef(), 'default_SR', ''); foreach ($oc->srs() as $ref => $sr) { if (($sr['content_type'] ?? '') !== 'iso' && Menus::managed($oc, $ref) && Menus::attached($oc, $ref) > 0) { $srs[] = ['ref' => $ref, 'name' => $oc->name($ref) . ' (' . Units::bytes((float)$sr['physical_size'] - (float)$sr['physical_utilisation']) . ' free)']; } } usort($srs, fn ($a, $b) => ($b['ref'] === $default) <=> ($a['ref'] === $default)); foreach ($oc->networks() as $ref => $n) { $networks[] = ['ref' => $ref, 'name' => (string)$n['name_label']]; } $targets[] = ['index' => $index, 'name' => $other->name(), 'hosts' => $hosts, 'srs' => $srs, 'networks' => $networks]; } return ['vm' => $vm, 'name' => $cache->name($vm), 'from' => $c->name(), 'running' => $cache->field($vm, 'power_state') === 'Running', 'disks' => $disks, 'vifs' => $vifs, 'targets' => $targets]; } protected function loaded(array $params = []): void { $this->params = $params; $this->heading->text = "Migrate '" . ($params['name'] ?? '') . "' to another pool"; $this->where->text = "From '" . ($params['from'] ?? '') . "'"; $this->disks->clear(); foreach ($params['disks'] ?? [] as $i => $d) { $this->disks->add(FormLoader::widget(['type' => 'row', 'gap' => 8, 'children' => [['type' => 'label', 'text' => $d['label'], 'width' => 240], ['type' => 'combobox', 'id' => "disk:$i", 'items' => [], 'grow' => 1]]])); } $this->disks->visible = ($params['disks'] ?? []) !== []; $this->vifs->clear(); foreach ($params['vifs'] ?? [] as $i => $v) { $this->vifs->add(FormLoader::widget(['type' => 'row', 'gap' => 8, 'children' => [['type' => 'label', 'text' => $v['label'], 'width' => 240], ['type' => 'combobox', 'id' => "vif:$i", 'items' => [], 'grow' => 1]]])); } $this->vifs->visible = ($params['vifs'] ?? []) !== []; $this->pool->items = array_column($params['targets'] ?? [], 'name'); $this->pool->selected = ($params['targets'] ?? []) === [] ? -1 : 0; $this->how->text = !empty($params['running']) ? 'The VM keeps running: its memory and its disks are copied to the other pool while it works (storage motion), then it carries on there.' : 'The VM is halted: its disks are copied to the other pool, then it is removed from this one.'; $this->error->text = ($params['targets'] ?? []) === [] ? 'Connect another pool first.' : ''; $this->onPoolChange($this->pool, (int)$this->pool->selected, ''); } /** The servers, SRs and networks of the pool chosen: each disk goes to its default SR, each interface to the network of the same name. */ public function onPoolChange(ComboBox $sender, int $index, string $item): void { $t = $this->params['targets'][$index] ?? null; $this->host->items = array_column($t['hosts'] ?? [], 'name'); $this->host->selected = ($t['hosts'] ?? []) === [] ? -1 : 0; foreach ($this->params['disks'] ?? [] as $i => $d) { $box = $this->widget("disk:$i"); if ($box instanceof ComboBox) { $box->items = array_column($t['srs'] ?? [], 'name'); $box->selected = ($t['srs'] ?? []) === [] ? -1 : 0; } } foreach ($this->params['vifs'] ?? [] as $i => $v) { $box = $this->widget("vif:$i"); if ($box instanceof ComboBox) { $names = array_column($t['networks'] ?? [], 'name'); $box->items = $names; $same = array_search($v['network'], $names, true); $box->selected = $same !== false ? (int)$same : ($names === [] ? -1 : 0); } } $this->migrate->enabled = $t !== null; } public function onMigrateClick(mixed $sender = null): void { $t = $this->params['targets'][(int)$this->pool->selected] ?? null; $host = $t['hosts'][(int)$this->host->selected]['ref'] ?? null; if ($t === null || $host === null) { $this->error->text = 'Choose the destination pool and server.'; return; } $vdiMap = []; foreach ($this->params['disks'] ?? [] as $i => $d) { $box = $this->widget("disk:$i"); $sr = $box instanceof ComboBox ? ($t['srs'][(int)$box->selected]['ref'] ?? null) : null; if ($sr === null) { $this->error->text = "Choose where the disk '{$d['label']}' goes."; return; } $vdiMap[$d['vdi']] = $sr; } $vifMap = []; foreach ($this->params['vifs'] ?? [] as $i => $v) { $box = $this->widget("vif:$i"); $net = $box instanceof ComboBox ? ($t['networks'][(int)$box->selected]['ref'] ?? null) : null; if ($net === null) { $this->error->text = "Choose the network of '{$v['label']}'."; return; } $vifMap[$v['vif']] = $net; } $this->close(['crossPool' => ['vm' => $this->params['vm'], 'target' => $t['index'], 'host' => $host, 'vdiMap' => $vdiMap, 'vifMap' => $vifMap]]); } public function onCancelClick(Button $sender): void { $this->close(); }}