storage->delete('servers.json'); $this->game->start(MainForm::class); /** @var MainForm $form */ $form = $this->game->scene(); $form->session->reset(); $this->game->start(MainForm::class, $params); $this->form = $form; return $form; } private function connectDemo(): Connection { $form = $this->fresh(['connectDemo' => true]); $this->game->run(10); $c = $form->session->connections[0]; self::assertTrue($c->isConnected(), 'the demo pool connects: ' . $c->error); return $c; } /** Another demo server (a standalone host), connected. */ private function addServer(string $address = 'xcp-demo-2.local'): Connection { $c = $this->form->session->add(ServerProfile::demoServer($address)); $this->form->connect($c); $this->until(fn () => $c->isConnected(), 60); return $c; } private function until(callable $condition, int $frames = 600): void { for ($i = 0; $i < $frames && !$condition(); $i++) { $this->game->run(1); } self::assertTrue((bool)$condition(), 'still waiting after ' . $frames . ' frames'); } private function ref(Connection $c, string $class, string $name): string { $ref = $c->cache->byName($class, $name); self::assertNotNull($ref, "no $class $name"); return $ref; } /** @param list|string> $items @return array text => enabled */ private static function enabled(array $items): array { return array_column(array_filter($items, 'is_array'), 'enabled', 'text'); } public function testAnotherDemoAddressIsAStandaloneHostOfItsOwn(): void { FakeXapi::forget(); $pool = new FakeXapi(); $alone = new FakeXapi('xcp-demo-2.local'); self::assertCount(2, $pool->db['host']); self::assertCount(1, $alone->db['host']); self::assertSame('xcp-ng-03', array_values($alone->db['host'])[0]['name_label']); self::assertSame('', array_values($alone->db['pool'])[0]['name_label'], 'a host alone: its pool has no name'); self::assertSame([], array_intersect_key($pool->db['vm'], $alone->db['vm']), 'their refs never meet'); self::assertSame('xcp-ng-04', array_values((new FakeXapi('xcp-demo-3.local'))->db['host'])[0]['name_label']); } public function testANewPoolIsMadeOfTwoStandaloneServers(): void { $this->connectDemo(); $a = $this->addServer('xcp-demo-2.local'); $b = $this->addServer('xcp-demo-3.local'); self::assertFalse($a->cache->isRealPool()); $menu = self::enabled(Commands::menu('Pool', $a, (string)$a->cache->master(), 'host', ['mode' => 'infrastructure', 'templates' => false, 'networks' => false])); self::assertTrue($menu['New Pool…']); $this->form->command('pool.new'); $this->game->run(1); $dialog = $this->game->scene(); self::assertInstanceOf(NewPoolForm::class, $dialog); self::assertSame(['xcp-ng-03 (xcp-demo-2.local)', 'xcp-ng-04 (xcp-demo-3.local)'], $dialog->master->items, 'the connected servers in no pool'); self::assertSame([$b->profile->key()], array_keys($dialog->boxes), 'the master is not offered as a member'); $dialog->name->text = 'Lab pool'; $dialog->boxes[$b->profile->key()]->checked = true; $this->game->run(2); $this->game->shot('new-pool'); $dialog->click($dialog->ok); $this->game->run(1); self::assertSame($this->form, $this->game->scene()); $this->until(fn () => count($this->form->session->connections) === 2 && count($a->cache->hosts()) === 2); self::assertSame('Lab pool', $a->name(), 'pool.set_name_label on the master'); self::assertTrue($a->cache->isRealPool()); self::assertNotNull($a->cache->byName('vm', 'staging-web'), "the joined host's VMs are in the pool"); self::assertNotContains($b, $this->form->session->connections, "the joined server's own connection is left"); // the join was called on the joining host, which now answers as a pool member $joining = FakeXapi::open('xcp-demo-3.local'); self::assertTrue($joining->isSlave()); self::assertContains('Async.pool.join', $joining->calls); $login = (new XapiClient($joining))->login(FakeXapi::USER, FakeXapi::PASSWORD); $this->until(function () use ($joining, $login): bool { $joining->poll(0.1); return $login->isDone(); }, 30); self::assertInstanceOf(XapiError::class, $login->error()); self::assertSame(['HOST_IS_SLAVE', 'xcp-demo-2.local'], $login->error()->failure); $this->game->run(20); self::assertContains($this->ref($a, 'host', 'xcp-ng-04'), $this->form->tree->visibleIds()); $this->game->shot('lab-pool'); } public function testAServerJoinsTheDemoPoolThenLeavesIt(): void { $pool = $this->connectDemo(); $this->addServer(); $this->form->navigate((string)$pool->cache->poolRef()); $this->game->run(2); $this->form->command('pool.addServer'); $this->game->run(1); $dialog = $this->game->scene(); self::assertInstanceOf(NewPoolForm::class, $dialog); self::assertStringContainsString("pool 'Demo pool'", $dialog->heading->text); self::assertCount(1, $dialog->members(), 'the standalone server is ticked'); $this->game->shot('add-server'); $dialog->click($dialog->ok); $this->until(fn () => count($pool->cache->hosts()) === 3 && count($this->form->session->connections) === 1); $host = $this->ref($pool, 'host', 'xcp-ng-03'); $vm = $this->ref($pool, 'vm', 'staging-web'); $vifs = $pool->cache->vifsOf($vm); self::assertCount(1, $vifs); $eth0 = (string)$pool->cache->field((string)$vifs[0]['vif']['network'], 'name_label', ''); self::assertSame('Pool-wide network associated with eth0', $eth0, "its VM's network card is on the pool's eth0 network"); $nfs = $this->ref($pool, 'sr', 'NFS storage'); self::assertArrayHasKey($nfs, $pool->cache->srsOf($host), "the pool's shared storage is plugged on it"); $this->game->run(20); self::assertContains($host, $this->form->tree->visibleIds()); // now it leaves: XenCenter's strong warning first $this->form->navigate($host); $this->game->run(2); self::assertTrue(self::enabled(Commands::hostItems($pool, $host))['Remove Server from Pool']); $this->form->command('host.removeFromPool'); $this->game->run(1); $confirm = $this->game->scene(); self::assertInstanceOf(ConfirmForm::class, $confirm); self::assertStringContainsString('local storage is destroyed', $confirm->message->text); self::assertSame('Remove', $confirm->ok->text); self::assertSame('danger', $confirm->ok->style); $this->game->shot('remove-from-pool'); $confirm->click($confirm->ok); $this->until(fn () => count($pool->cache->hosts()) === 2); $this->game->run(20); self::assertNull($pool->cache->byName('vm', 'staging-web'), 'its VMs on local storage went with it'); self::assertNotContains($host, $this->form->tree->visibleIds()); self::assertFalse(FakeXapi::open('xcp-demo-2.local')->isSlave(), 'it is a standalone server again'); } public function testTheMasterCannotLeaveAndAnotherServerBecomesMaster(): void { $pool = $this->connectDemo(); $master = (string)$pool->cache->master(); $other = $this->ref($pool, 'host', 'xcp-ng-02'); $onMaster = self::enabled(Commands::hostItems($pool, $master)); self::assertFalse($onMaster['Remove Server from Pool']); self::assertFalse($onMaster['Make this server the pool master']); $items = self::enabled(Commands::hostItems($pool, $other)); self::assertTrue($items['Make this server the pool master']); self::assertFalse($items['Remove Server from Pool'], 'VMs run on it'); $this->form->navigate($other); $this->game->run(2); $this->form->command('host.designateMaster'); $this->game->run(1); $confirm = $this->game->scene(); self::assertInstanceOf(ConfirmForm::class, $confirm); $confirm->click($confirm->ok); $this->until(fn () => $pool->cache->master() === $other); self::assertContains('Async.pool.designate_new_master', $pool->fake()?->calls ?? []); } public function testAServerWithRunningVmsCannotJoinAPool(): void { $pool = $this->connectDemo(); $alone = $this->addServer(); $host = (string)$alone->cache->master(); self::assertTrue(self::enabled(Commands::hostItems($alone, $host))["Add to Pool 'Demo pool'"]); $vm = $this->ref($alone, 'vm', 'staging-web'); $this->form->command('vm.start', $vm, true); $this->until(fn () => $alone->cache->field($vm, 'power_state') === 'Running'); self::assertFalse(self::enabled(Commands::hostItems($alone, $host))["Add to Pool 'Demo pool'"], 'XenCenter offers it no more'); $join = $alone->run('Join', 'pool.join', ['xcp-demo.local', FakeXapi::USER, FakeXapi::PASSWORD]); $this->until(fn () => $join->isDone()); $failure = $join->error(); self::assertSame('JOINING_HOST_CANNOT_HAVE_RUNNING_OR_SUSPENDED_VMS', $failure instanceof XapiError ? $failure->code() : ''); self::assertCount(2, $pool->cache->hosts()); } public function testConnectionAddressesAreSavedAndUsedFirstForConsolesAndFigures(): void { $pool = $this->connectDemo(); $other = $this->ref($pool, 'host', 'xcp-ng-02'); $uuid = (string)$pool->cache->field($other, 'uuid'); $this->form->navigate((string)$pool->cache->poolRef()); $this->game->run(2); $this->form->command('pool.addresses'); $this->game->run(1); $dialog = $this->game->scene(); self::assertInstanceOf(HostAddressesForm::class, $dialog); self::assertCount(2, $dialog->fields); self::assertSame('192.168.1.12', $dialog->fields[$uuid]->placeholder, "empty: XAPI's address (no naming to copy from an address)"); $dialog->fields[$uuid]->text = '10.8.0.12'; $this->game->run(2); $this->game->shot('addresses'); $dialog->click($dialog->ok); $this->game->run(1); self::assertSame(['10.8.0.12' => true], array_fill_keys(array_values($pool->profile->hostAddresses), true)); $saved = Engine::get()->storage->readJson('servers.json', []); self::assertSame('10.8.0.12', $saved[0]['hostAddresses'][$uuid] ?? null, 'saved in var/servers.json'); self::assertSame(['10.8.0.12'], array_values(ServerProfile::fromArray($saved[0])->hostAddresses)); self::assertSame('10.8.0.12', $pool->consoleTargets($this->ref($pool, 'vm', 'db-replica'))[0], 'the console of a VM on that host: there first'); self::assertSame('10.8.0.12', \App\Feature\Pools\HostAddresses::reachable($pool, $other)); $this->form->navigate($this->ref($pool, 'vm', 'db-replica')); $this->form->tabs->selectTab('Performance'); $this->until(fn () => ($pool->hostLinks()->addresses()[$other] ?? null) === '10.8.0.12', 60); } public function testFiguresOfAVmOnAnotherHostComeFromThatHost(): void { $pool = $this->connectDemo(); $vm = $this->ref($pool, 'vm', 'db-replica'); $uuid = (string)$pool->cache->field($vm, 'uuid'); // the master only has its own figures (and its VMs') $fake = $pool->fake(); self::assertNotNull($fake); $reply = $fake->request('GET', '/rrd_updates?session_id=' . $pool->client?->session . '&start=0&host=true'); $this->until(fn () => $reply->isDone(), 30); self::assertSame([], Rrd::parse($reply->result()->body)->names('vm', $uuid)); $this->form->navigate($vm); $this->form->tabs->selectTab('Performance'); $this->until(fn () => isset($this->form->rrd[0]) && $this->form->rrd[0]->names('vm', $uuid) !== [], 120); $this->game->run(4); self::assertNotSame([], $this->form->chart0->series[0]['points'] ?? [], 'its CPU graph has points'); self::assertSame('192.168.1.12', $pool->hostLinks()->addresses()[$this->ref($pool, 'host', 'xcp-ng-02')]); $master = (string)$pool->cache->field((string)$pool->cache->master(), 'uuid'); self::assertNotSame([], $this->form->rrd[0]->names('host', $master), "the master's figures stay in the merge"); $this->game->shot('perf-other-host'); $pool->disconnect(false); self::assertSame([], $pool->hostLinks()->addresses(), 'the roads close with the connection'); } public function testTheDashboardShowsACardPerConnectionAndAClickSelectsIt(): void { $pool = $this->connectDemo(); $alone = $this->addServer(); $off = $this->form->session->add(ServerProfile::demoServer('xcp-demo-3.local')); $this->form->navigate(TreeBuilder::ROOT); $this->game->run(2); self::assertSame('Dashboard', $this->form->tabs->value(), 'the first tab of XenCenter'); self::assertTrue($this->form->dashboard->visible); $cards = $this->form->dashboard->cards; self::assertCount(3, $cards); self::assertSame(['Demo pool', 'xcp-ng-03'], [$cards[0]['name'], $cards[1]['name']]); self::assertSame([2, 1], [$cards[0]['hosts'], $cards[1]['hosts']]); self::assertSame(Connection::DISCONNECTED, $cards[2]['state']); self::assertGreaterThan(0, $cards[0]['alerts']); $this->until(fn () => ($this->form->dashboard->cards[0]['cpu'] ?? null) !== null, 400); // the figures come every 5 s self::assertGreaterThan(0.0, $this->form->dashboard->cards[0]['memory']); $this->game->run(2); $this->game->shot('dashboard'); $t = $this->form->theme; [$x, $y, $w, $h] = $this->form->dashboard->cardRect($t, 1); $this->form->clickAt($x + $w / 2, $y + $h / 3); $this->game->run(2); self::assertSame((string)$alone->cache->master(), $this->form->selected, 'a click on a card selects that server'); $this->form->navigate(TreeBuilder::ROOT); $this->game->run(2); [$bx, $by, $bw, $bh] = $this->form->dashboard->connectRect($t, 2); $this->form->clickAt($bx + $bw / 2, $by + $bh / 2); $this->until(fn () => $off->isConnected(), 60); self::assertTrue($pool->isConnected()); } }