|string> $items @return list|string> */ public static function context(?Connection $c, string $ref, string $kind, array $items): array { if ($c === null || !$c->isConnected()) { return $items; } return match ($kind) { 'pool', 'host' => self::after($items, 'vm.new', [Commands::item('sr.new', 'New Storage…', true, 'new-sr'), Commands::item('network.new', 'New Network…', true, 'network')]), 'vm' => self::before($items, 'vm.snapshot', [Commands::item('vm.crossPoolMigrate', 'Migrate to Another Pool…', self::canCrossPool($c, $ref), 'migrate'), '-']), 'sr' => self::before($items, 'object.properties', [...self::srItems($c->cache, $ref), '-']), 'network' => self::before($items, 'object.properties', [Commands::item('network.new', 'New Network…', true, 'network'), Commands::item('network.remove', 'Remove Network…', self::removable($c->cache, $ref)), '-']), default => $items, }; } /** @param list|string> $items @return list|string> */ public static function menu(string $menu, ?Connection $c, string $ref, string $kind, array $items): array { $connected = $c !== null && $c->isConnected(); return match ($menu) { 'File' => self::after($items, 'server.demo', [Commands::item('server.demo2', 'Open the Second Demo Pool')]), 'Storage' => [Commands::item('sr.new', 'New Storage…', $connected, 'new-sr'), '-', ...$items], 'Server' => $kind === 'host' ? self::context($c, $ref, 'host', $items) : $items, 'VM' => $kind === 'vm' ? self::context($c, $ref, 'vm', $items) : $items, default => $items, }; } /** @return array the toolbar's New Storage button */ public static function toolbar(?Connection $c): array { return ['newStorage' => ['id' => 'sr.new', 'text' => 'New Storage', 'enabled' => $c !== null && $c->isConnected()]]; } /** The buttons over a tab (and a few flags on its lines, for the buttons that need a line). */ public static function page(?Connection $c, string $ref, string $kind, string $tab, Page $page): Page { if ($c === null || !$c->isConnected()) { return $page; } $cache = $c->cache; switch ("$kind:$tab") { case 'sr:General': $attached = self::attached($cache, $ref); foreach ($page->sections as &$section) { foreach ($section['rows'] as &$row) { if ($row[0] === 'State:') { $row[1] = $attached === 0 ? 'Detached' : ($attached < count((array)$cache->field($ref, 'PBDs', [])) ? 'Partially attached' : 'OK'); } } unset($row); } unset($section); $page->actions = [...$page->actions, ...self::srButtons($cache, $ref)]; break; case 'sr:Storage': if ($cache->field($ref, 'content_type') !== 'iso') { $page->actions[] = ['id' => 'disk.move', 'text' => 'Move…', 'needsRow' => true]; } break; case 'vm:Storage': foreach ($page->rows as &$row) { $vbd = $cache->get((string)$row['id']) ?? []; $row['disk'] = ($vbd['type'] ?? '') !== 'CD' && $cache->get((string)($vbd['VDI'] ?? '')) !== null; } unset($row); $page->actions = [['id' => 'disk.add', 'text' => 'Add…', 'style' => 'primary'], ['id' => 'disk.attach', 'text' => 'Attach…'], ['id' => 'disk.move', 'text' => 'Move…', 'needsRow' => 'disk'], ['id' => 'disk.detach', 'text' => 'Detach', 'needsRow' => 'disk'], ['id' => 'disk.delete', 'text' => 'Delete', 'needsRow' => 'disk', 'style' => 'danger']]; break; case 'pool:Storage': case 'host:Storage': $page->actions = [['id' => 'sr.new', 'text' => 'New SR…', 'style' => 'primary'], ...$page->actions]; break; case 'pool:Networking': case 'host:Networking': foreach ($page->rows as &$row) { $row['removable'] = self::removable($cache, (string)$row['id']); } unset($row); $page->actions = [['id' => 'network.new', 'text' => 'Add Network…', 'style' => 'primary'], ['id' => 'network.properties', 'text' => 'Properties', 'needsRow' => true], ['id' => 'network.remove', 'text' => 'Remove', 'needsRow' => 'removable', 'style' => 'danger']]; break; case 'network:General': $page->actions[] = ['id' => 'network.remove', 'text' => 'Remove Network…', 'enabled' => self::removable($cache, $ref), 'style' => 'danger']; break; } return $page; } // what can be done --------------------------------------------------------------------------------------------------- /** How many hosts see the SR now (its PBDs plugged). */ public static function attached(Cache $cache, string $sr): int { $n = 0; foreach ((array)$cache->field($sr, 'PBDs', []) as $pbd) { $n += !empty($cache->field((string)$pbd, 'currently_attached')) ? 1 : 0; } return $n; } /** XCP-ng's own SRs (the tools ISOs, removable media) are not detached or destroyed. */ public static function managed(Cache $cache, string $sr): bool { return $cache->field($sr, 'type') !== 'udev' && ($cache->field($sr, 'other_config', [])['xenserver_tools_sr'] ?? '') !== 'true'; } public static function isDefault(Cache $cache, string $sr): bool { return (string)$cache->field((string)$cache->poolRef(), 'default_SR', '') === $sr; } /** @return list|string> */ private static function srItems(Cache $cache, string $sr): array { $attached = self::attached($cache, $sr); $all = count((array)$cache->field($sr, 'PBDs', [])); $managed = self::managed($cache, $sr); return [ Commands::item('sr.setDefault', 'Set as Default', !self::isDefault($cache, $sr) && $cache->field($sr, 'content_type') !== 'iso' && $attached > 0), '-', Commands::item('sr.detach', 'Detach…', $managed && $attached > 0), Commands::item('sr.reattach', 'Reattach', $managed && $attached < $all), Commands::item('sr.forget', 'Forget…', $managed), Commands::item('sr.destroy', 'Destroy…', $managed), ]; } /** @return list> */ private static function srButtons(Cache $cache, string $sr): array { $out = []; foreach (self::srItems($cache, $sr) as $item) { if (is_array($item)) { $out[] = ['id' => $item['id'], 'text' => rtrim((string)$item['text'], '…'), 'enabled' => $item['enabled'], 'style' => in_array($item['id'], ['sr.destroy', 'sr.forget'], true) ? 'danger' : 'default']; } } return $out; } /** A network goes when no VM uses it and no physical NIC carries it (VLANs and bonds go with it). */ public static function removable(Cache $cache, string $network): bool { $n = $cache->get($network); if ($n === null || $cache->classOf($network) !== 'network' || (array)($n['VIFs'] ?? []) !== [] || ($n['other_config']['is_host_internal_management_network'] ?? '') === 'true') { return false; } foreach ((array)($n['PIFs'] ?? []) as $pif) { $p = $cache->get((string)$pif) ?? []; if (!empty($p['physical']) || !empty($p['management'])) { return false; } } return true; } /** Another connected pool, and the VM running or halted. */ public static function canCrossPool(Connection $c, string $vm): bool { return in_array($c->cache->field($vm, 'power_state'), ['Running', 'Halted'], true) && self::otherPools($c) !== []; } /** @return array the other connected pools, by index of the session */ public static function otherPools(Connection $c): array { $out = []; foreach (\App\Model\Session::$current->connections ?? [] as $i => $other) { if ($other !== $c && $other->isConnected()) { $out[$i] = $other; } } return $out; } // inserting into a menu ------------------------------------------------------------------------------------------------- /** @param list|string> $items @param list|string> $new @return list|string> */ private static function after(array $items, string $id, array $new): array { $at = self::indexOf($items, $id); return $at === null ? [...$items, ...$new] : [...array_slice($items, 0, $at + 1), ...$new, ...array_slice($items, $at + 1)]; } /** @param list|string> $items @param list|string> $new @return list|string> */ private static function before(array $items, string $id, array $new): array { $at = self::indexOf($items, $id); return $at === null ? [...$items, ...$new] : [...array_slice($items, 0, $at), ...$new, ...array_slice($items, $at)]; } private static function indexOf(array $items, string $id): ?int { foreach ($items as $i => $item) { if (is_array($item) && ($item['id'] ?? '') === $id) { return $i; } } return null; } }