<?phpnamespace App\Form;use App\Feature\Storage\Actions;use App\Gui\Units;use App\Model\Connection;use JumPHP\Gui\Button;use JumPHP\Gui\ComboBox;use JumPHP\Gui\Container;use JumPHP\Gui\Form;use JumPHP\Gui\Label;use JumPHP\Gui\TextBox;/** * The New Storage Repository wizard of XenCenter, on one page: the type, then where the storage is. What XAPI is asked * (Feature\Storage\Actions::createSr): SR.create(host, device_config, 0, name, description, type, content_type, shared, sm_config) * - NFS: {server, serverpath, nfsversion}, "nfs", "user", shared; SMB: {server: //nas/share, username, password}, "smb"; * - ISO library: {location: server:/path | //nas/share (+ type cifs, username, cifspassword)}, "iso", "iso", sm_config iso_type; * - iSCSI: SR.probe(master, {target, port, targetIQN}) lists the LUNs (Scan), then {target, port, targetIQN, SCSIid}, "lvmoiscsi"; * - local EXT / LVM: {device: /dev/sdb} on the chosen server, not shared. * Closes with ['newSr' => [...]]. * * @property Label $where * @property ComboBox $kind * @property TextBox $name * @property TextBox $description * @property Container $nfsPanel * @property TextBox $nfsServer * @property TextBox $nfsPath * @property ComboBox $nfsVersion * @property Button $nfsScan * @property Label $nfsFound * @property Container $smbPanel * @property TextBox $smbServer * @property TextBox $smbUser * @property TextBox $smbPassword * @property Container $iscsiPanel * @property TextBox $iscsiTarget * @property TextBox $iscsiPort * @property TextBox $iscsiIqn * @property ComboBox $iscsiLun * @property Button $iscsiScan * @property Label $iscsiFound * @property Container $localPanel * @property ComboBox $localHost * @property TextBox $localDevice * @property Label $error * @property Button $create * @property Button $cancel */final class NewSrForm extends Form{ /** the types the wizard makes, and the name it proposes for each */ public const KINDS = ['nfs' => ['NFS', 'NFS virtual disk storage'], 'smb' => ['SMB / CIFS', 'SMB virtual disk storage'], 'iso-nfs' => ['ISO library (NFS)', 'NFS ISO library'], 'iso-smb' => ['ISO library (SMB / CIFS)', 'SMB ISO library'], 'iscsi' => ['iSCSI (software initiator)', 'iSCSI virtual disk storage'], 'ext' => ['Local EXT (on a device)', 'Local EXT storage'], 'lvm' => ['Local LVM (on a device)', 'Local LVM storage']]; public function __construct(private \App\Model\Session $session) { parent::__construct(); } /** XAPI's answers (the scans), events and the console go on while this dialog is open. */ protected function tick(float $dt): void { $this->session->tick($dt); } private array $params = []; /** @var list<array{lun: string, scsiId: string, size: float, vendor: string}> what the last iSCSI scan found */ public array $luns = []; /** @return array<string, mixed> */ public static function paramsFor(Connection $c, ?string $host = null): array { $hosts = []; foreach ($c->cache->hosts() as $ref => $h) { $hosts[] = ['ref' => $ref, 'name' => (string)$h['name_label']]; } return ['server' => $c->name(), 'pool' => (string)$c->cache->poolRef(), 'master' => (string)$c->cache->master(), 'hosts' => $hosts, 'host' => $host]; } protected function loaded(array $params = []): void { $this->params = $params; $this->luns = []; $this->where->text = "In '" . ($params['server'] ?? '') . "'"; $this->kind->items = array_column(array_values(self::KINDS), 0); $this->kind->selected = 0; $this->localHost->items = array_column($params['hosts'] ?? [], 'name'); $at = array_search($params['host'] ?? null, array_column($params['hosts'] ?? [], 'ref'), true); $this->localHost->selected = $at === false ? 0 : (int)$at; foreach ([$this->name, $this->description, $this->nfsServer, $this->nfsPath, $this->smbServer, $this->smbUser, $this->smbPassword, $this->iscsiTarget, $this->iscsiIqn, $this->localDevice] as $box) { $box->text = ''; } $this->iscsiPort->text = '3260'; $this->iscsiLun->items = []; $this->iscsiLun->selected = -1; $this->nfsVersion->selected = 0; $this->nfsFound->text = $this->iscsiFound->text = $this->error->text = ''; $this->onKindChange($this->kind, 0, ''); $this->setFocus($this->name); } public function kind(): string { return array_keys(self::KINDS)[(int)$this->kind->selected] ?? 'nfs'; } public function onKindChange(ComboBox $sender, int $index, string $item): void { $kind = $this->kind(); $this->nfsPanel->visible = $kind === 'nfs' || $kind === 'iso-nfs'; $this->smbPanel->visible = $kind === 'smb' || $kind === 'iso-smb'; $this->iscsiPanel->visible = $kind === 'iscsi'; $this->localPanel->visible = $kind === 'ext' || $kind === 'lvm'; // the proposed name follows the type until the user writes one $defaults = array_column(array_values(self::KINDS), 1); if ($this->name->text === '' || in_array($this->name->text, $defaults, true)) { $this->name->text = self::KINDS[$kind][1]; } $this->error->text = ''; } public function onNfsScanClick(Button $sender): void { $c = $this->connection(); $server = trim($this->nfsServer->text); if ($c === null || $server === '') { $this->nfsFound->text = 'Enter the address of the NFS server first.'; return; } $this->nfsFound->text = 'Scanning…'; $this->error->text = ''; Actions::probe($c, (string)$this->params['master'], ['server' => $server], 'nfs')->then(function (string $xml): void { $paths = Actions::values($xml, 'Path'); $this->nfsFound->text = $paths === [] ? 'No export found.' : 'Exports: ' . implode(', ', $paths); if ($paths !== [] && trim($this->nfsPath->text) === '') { $this->nfsPath->text = $paths[0]; } }, fn (\Throwable $e) => $this->nfsFound->text = 'Scan failed: ' . $e->getMessage()); } /** SR.probe on the pool master: the target's IQN (when none is typed), then its LUNs. */ public function onIscsiScanClick(Button $sender): void { $c = $this->connection(); $target = trim($this->iscsiTarget->text); if ($c === null || $target === '') { $this->iscsiFound->text = 'Enter the address of the iSCSI target first.'; return; } $this->iscsiFound->text = 'Scanning…'; $this->error->text = ''; $dc = array_filter(['target' => $target, 'port' => trim($this->iscsiPort->text) ?: '3260', 'targetIQN' => trim($this->iscsiIqn->text)], fn ($v) => $v !== ''); Actions::probe($c, (string)$this->params['master'], $dc, 'lvmoiscsi')->then(function (string $xml) use ($sender): void { $iqns = Actions::values($xml, 'TargetIQN'); if (trim($this->iscsiIqn->text) === '' && $iqns !== []) { $this->iscsiIqn->text = $iqns[0]; $this->onIscsiScanClick($sender); // and now its LUNs return; } $this->luns = Actions::luns($xml); $this->iscsiLun->items = array_map(fn ($l) => 'LUN ' . $l['lun'] . ': ' . Units::bytes($l['size']) . ' (' . $l['vendor'] . ')', $this->luns); $this->iscsiLun->selected = $this->luns === [] ? -1 : 0; $this->iscsiFound->text = $this->luns !== [] ? count($this->luns) . ' LUN(s) found.' : ($iqns !== [] ? 'Unknown IQN: this target is ' . implode(', ', $iqns) . '.' : 'No LUN on this target.'); }, fn (\Throwable $e) => $this->iscsiFound->text = 'Scan failed: ' . $e->getMessage()); } private function connection(): ?Connection { return $this->session->connectionOf((string)($this->params['master'] ?? '')); } public function onCreateClick(mixed $sender = null): void { $kind = $this->kind(); $name = trim($this->name->text); if ($name === '') { $this->error->text = 'Enter a name for the storage repository.'; return; } $master = (string)($this->params['master'] ?? ''); $spec = ['pool' => $this->params['pool'] ?? '', 'host' => $master, 'name' => $name, 'description' => trim($this->description->text), 'shared' => true, 'content' => 'user', 'smConfig' => []]; switch ($kind) { case 'nfs': case 'iso-nfs': $server = trim($this->nfsServer->text); $path = trim($this->nfsPath->text); if ($server === '' || !str_starts_with($path, '/')) { $this->error->text = 'Enter the NFS server and its export path (it starts with /).'; return; } $version = (string)($this->nfsVersion->value() ?? '3'); $spec += $kind === 'nfs' ? ['type' => 'nfs', 'deviceConfig' => ['server' => $server, 'serverpath' => $path, 'nfsversion' => $version]] : ['type' => 'iso', 'deviceConfig' => ['location' => "$server:$path", 'nfsversion' => $version]]; if ($kind === 'iso-nfs') { $spec['content'] = 'iso'; $spec['smConfig'] = ['iso_type' => 'nfs_iso']; } break; case 'smb': case 'iso-smb': $share = str_replace('\\', '/', trim($this->smbServer->text)); if (!preg_match('#^//[^/]+/.+#', $share)) { $this->error->text = 'Enter the share as //server/share.'; return; } $user = trim($this->smbUser->text); $spec += $kind === 'smb' ? ['type' => 'smb', 'deviceConfig' => array_filter(['server' => $share, 'username' => $user, 'password' => $this->smbPassword->text], fn ($v) => $v !== '')] : ['type' => 'iso', 'deviceConfig' => array_filter(['location' => $share, 'type' => 'cifs', 'username' => $user, 'cifspassword' => $this->smbPassword->text], fn ($v) => $v !== '')]; if ($kind === 'iso-smb') { $spec['content'] = 'iso'; $spec['smConfig'] = ['iso_type' => 'cifs']; } break; case 'iscsi': $lun = $this->luns[(int)$this->iscsiLun->selected] ?? null; if ($lun === null) { $this->error->text = 'Scan the target, then choose a LUN.'; return; } $spec += ['type' => 'lvmoiscsi', 'deviceConfig' => ['target' => trim($this->iscsiTarget->text), 'port' => trim($this->iscsiPort->text) ?: '3260', 'targetIQN' => trim($this->iscsiIqn->text), 'SCSIid' => $lun['scsiId']]]; break; default: $device = trim($this->localDevice->text); if (!str_starts_with($device, '/dev/')) { $this->error->text = 'Enter the device of the server (/dev/sdb…).'; return; } $spec['host'] = (string)($this->params['hosts'][(int)$this->localHost->selected]['ref'] ?? $master); $spec['shared'] = false; $spec += ['type' => $kind, 'deviceConfig' => ['device' => $device]]; } $this->close(['newSr' => $spec]); } public function onCancelClick(Button $sender): void { $this->close(); }}