<?phpnamespace App\Feature\Storage;use App\Form\ConfirmForm;use App\Form\CrossPoolMigrateForm;use App\Form\DiskForm;use App\Form\MainForm;use App\Form\MoveDiskForm;use App\Form\NetworkPropertiesForm;use App\Form\NewNetworkForm;use App\Form\NewSrForm;use App\Form\StrongConfirmForm;use App\Model\Connection;use App\Model\ServerProfile;use App\Xapi\FakeXapi;use JumPHP\Async\Task;use JumPHP\Kernel\Engine;/** * The commands of these features, run by MainForm::command() (one call at its start) and the results of their dialogs, given to * MainForm::resume() (one call too). Commands: sr.new / detach / reattach / forget / destroy / setDefault, network.new / properties / * remove, disk.add / attach / move / detach / delete, vm.crossPoolMigrate, server.demo2. */final class Router{ public const SECOND_DEMO = 'xcp-lab.local'; private const IDS = ['sr.new', 'sr.detach', 'sr.reattach', 'sr.forget', 'sr.destroy', 'sr.setDefault', 'network.new', 'network.properties', 'network.remove', 'disk.add', 'disk.attach', 'disk.move', 'disk.detach', 'disk.delete', 'vm.crossPoolMigrate']; /** @return bool true when the command was one of these (done, or a dialog opened) */ public static function command(MainForm $form, string $id, ?Connection $c, string $target, bool $confirmed): bool { if ($id === 'server.demo2') { self::openSecondDemo($form); return true; } if ($id === 'object.properties' && $c?->cache->classOf($target) === 'network') { $id = 'network.properties'; } if (!in_array($id, self::IDS, true)) { return false; } if ($c === null || !$c->isConnected()) { return true; } $cache = $c->cache; $scenes = Engine::get()->scenes; [, $selected] = $form->selection(); switch ($id) { case 'sr.new': $scenes->push(NewSrForm::class, NewSrForm::paramsFor($c, $cache->classOf($target) === 'host' ? $target : null)); return true; case 'network.new': $scenes->push(NewNetworkForm::class, NewNetworkForm::paramsFor($c)); return true; case 'network.properties': if ($cache->classOf($target) === 'network') { $scenes->push(NetworkPropertiesForm::class, NetworkPropertiesForm::paramsFor($c, $target)); } return true; case 'disk.add': case 'disk.attach': if ($cache->classOf($target) === 'vm') { $scenes->push(DiskForm::class, DiskForm::paramsFor($c, $target, $id === 'disk.add' ? 'add' : 'attach')); } return true; case 'disk.move': $vdi = $cache->classOf($target) === 'vbd' ? (string)$cache->field($target, 'VDI', '') : $target; if ($cache->classOf($vdi) === 'vdi') { $scenes->push(MoveDiskForm::class, MoveDiskForm::paramsFor($c, $vdi)); } return true; case 'vm.crossPoolMigrate': if (Menus::canCrossPool($c, $target)) { $scenes->push(CrossPoolMigrateForm::class, CrossPoolMigrateForm::paramsFor($form->session, $c, $target)); } return true; } if (!$confirmed && ($ask = self::question($id, $c, $target)) !== null) { [$message, $ok, $strong] = $ask; $scenes->push($strong ? StrongConfirmForm::class : ConfirmForm::class, ['message' => $message, 'command' => $id, 'target' => $target, 'ok' => $ok, 'check' => $strong ? 'I understand: this cannot be undone.' : '']); return true; } $name = $cache->name($target); $task = match ($id) { 'sr.detach' => Actions::detachSr($c, $target), 'sr.reattach' => Actions::reattachSr($c, $target), 'sr.forget' => Actions::removeSr($c, $target, false), 'sr.destroy' => Actions::removeSr($c, $target, true), 'sr.setDefault' => Actions::setDefaultSr($c, $target), 'network.remove' => Actions::removeNetwork($c, $target), 'disk.detach' => Actions::detachDisk($c, $target, false), 'disk.delete' => Actions::detachDisk($c, $target, true), }; $gone = in_array($id, ['sr.forget', 'sr.destroy', 'network.remove'], true) && $selected === $target; $task->then(function () use ($form, $c, $gone): void { if ($gone) { // the object shown is no more: its pool $form->navigate((string)($c->cache->isRealPool() ? $c->cache->poolRef() : $c->cache->master())); } }, fn () => null); if ($name !== '') { $form->refresh(); } return true; } /** @return array{0: string, 1: string, 2: bool}|null the question, the button, a strong confirmation (a box to tick) */ private static function question(string $id, Connection $c, string $target): ?array { $name = $c->cache->name($target); $disk = $c->cache->name((string)$c->cache->field($target, 'VDI', '')); return match ($id) { 'sr.detach' => ["Detach the storage repository '$name'?\n\nIts virtual disks cannot be used until it is reattached. The VMs that use them must be shut down first.", 'Detach', false], 'sr.forget' => ["Forget the storage repository '$name'?\n\nXenCenter removes it from the pool: the data stays on the storage, but the VMs lose their disks on it. It can be introduced again later.", 'Forget', true], 'sr.destroy' => ["Destroy the storage repository '$name'?\n\nEvery virtual disk on it is DELETED, for good: the VMs lose them and the data cannot be recovered.", 'Destroy', true], 'network.remove' => ["Remove the network '$name'?\n\nIts VLANs and bonds are removed on every server.", 'Remove', false], 'disk.detach' => ["Detach the virtual disk '$disk' from VM '" . $c->cache->name((string)$c->cache->field($target, 'VM', '')) . "'?\n\nThe disk stays on its storage repository.", 'Detach', false], 'disk.delete' => ["Delete the virtual disk '$disk'?\n\nIts data is lost. This cannot be undone.", 'Delete', false], default => null, }; } /** What the dialogs of these features answered. @param array<string, mixed> $result */ public static function resume(MainForm $form, array $result): bool { $session = $form->session; $task = match (true) { isset($result['newSr']) => Actions::createSr($session, (array)$result['newSr']), isset($result['newNetwork']) => Actions::createNetwork($session, (array)$result['newNetwork']), isset($result['networkProperties']) => Actions::networkProperties($session, (array)$result['networkProperties']), isset($result['moveDisk']) => Actions::moveDisk($session, (string)$result['moveDisk']['vdi'], (string)$result['moveDisk']['sr']), isset($result['addDisk']) => Actions::addDisk($session, (array)$result['addDisk']), isset($result['attachDisk']) => Actions::attachDisk($session, (string)$result['attachDisk']['vm'], (string)$result['attachDisk']['vdi']), isset($result['crossPool']) => Actions::crossPoolMigrate($session, (array)$result['crossPool']), default => null, }; if (!$task instanceof Task) { return false; } $task->then(fn () => null, fn () => null); $form->refresh(true); return true; } /** File > Open the Second Demo Pool: another FakeXapi ("Demo pool 2"), to try cross-pool migration. */ public static function openSecondDemo(MainForm $form): Connection { foreach ($form->session->connections as $c) { if ($c->profile->method === 'demo' && strtolower($c->profile->host) === self::SECOND_DEMO) { if (!$c->isConnected() && $c->state !== Connection::CONNECTING) { $form->connect($c); } return $c; } } $c = $form->session->add(new ServerProfile(self::SECOND_DEMO, FakeXapi::USER, FakeXapi::PASSWORD, 'demo', 0, true, 'root', 22, '', 'Demo pool 2')); $form->connect($c); return $c; }}