Host anlegen: die Seite beschrieb das Rettungssystem und startete die Pipeline

main
nexxo 2026-08-02 01:38:48 +02:00
parent acc4193071
commit 2c30d1fb86
11 changed files with 165 additions and 187 deletions

View File

@ -3,6 +3,7 @@
namespace App\Livewire\Admin;
use App\Actions\StartHostOnboarding;
use App\Models\Datacenter;
use App\Support\HostEnrolment;
use App\Support\HostName;
use App\Support\HostTakeoverCommand;
@ -14,16 +15,9 @@ use Livewire\Component;
class HostCreate extends Component
{
/**
* Die fertige Befehlszeile gesetzt, sobald der Host angelegt ist.
*
* Sie wird GENAU EINMAL gezeigt. In der Datenbank steht nur der Hash des
* Codes, und der private Schlüssel steht dort überhaupt nicht; wer die Seite
* neu lädt, bekommt sie nicht wieder, sondern legt einen neuen Code an. Das
* ist kein Versehen, sondern der Grund, warum ein abgefangener Blick auf
* einen Bildschirm später nichts mehr wert ist.
* Gesetzt, sobald der Host angelegt ist und zugleich die Weiche der
* Ansicht zwischen Formular und Bestätigung.
*/
public ?string $command = null;
public ?string $createdUuid = null;
public ?string $createdName = null;
@ -33,7 +27,7 @@ class HostCreate extends Component
public function mount(): void
{
$this->datacenter = (string) \App\Models\Datacenter::query()->active()->orderBy('name')->value('code');
$this->datacenter = (string) Datacenter::query()->active()->orderBy('name')->value('code');
}
#[Validate('required|ip|unique:hosts,public_ip')]
@ -49,10 +43,18 @@ class HostCreate extends Component
$host = $action->run($data);
// Kein Weiterleiten mehr. Die Befehlszeile gibt es nur hier und nur
// jetzt — wer den Betreiber auf eine andere Seite schickt, schickt ihn
// von dem einzigen Wert weg, den er braucht.
$this->command = HostTakeoverCommand::for($host, HostEnrolment::issueWithKeys($host));
// Der Einmal-Code und das Schlüsselpaar entstehen weiterhin: dieselbe
// Methode legt auch die Tunneladresse und den öffentlichen Schlüssel am
// Host an, und der Code trägt den Bootstrap-Weg, der über
// Admin\ReissueTakeover erreichbar bleibt.
//
// Die BEFEHLSZEILE daraus zeigt diese Seite nicht mehr. Sie gehört zum
// Rettungssystem-Weg, und das Speichern hier startet die
// Provisionierungs-Pipeline: `EstablishSshTrust` meldet sich mit dem
// Root-Passwort an einer laufenden Debian-Maschine an, danach kommt
// Proxmox per Paket dazu. Es gibt für den Betreiber nichts zu kopieren.
HostEnrolment::issueWithKeys($host);
$this->createdUuid = $host->uuid;
$this->createdName = $host->name;
}
@ -65,10 +67,13 @@ class HostCreate extends Component
$preview = $this->datacenter === '' ? null : HostName::preview($this->datacenter);
return view('livewire.admin.host-create', [
'datacenters' => \App\Models\Datacenter::query()->active()->orderBy('name')->get(),
'datacenters' => Datacenter::query()->active()->orderBy('name')->get(),
'previewName' => $preview,
'previewFqdn' => $preview === null ? null : HostName::fqdn($preview),
'archiveUrl' => HostTakeoverCommand::archiveUrl(),
// `archiveUrl` fiel weg: er war der Hinweis, woher die Bootstrap-
// Zeile ihr Skript holt, und die Zeile zeigt diese Seite nicht mehr.
// Die fehlenden Tunnel-Einstellungen bleiben — ohne sie kommt auch
// die Pipeline nicht in den Tunnel.
'missingSettings' => HostTakeoverCommand::missingSettings(),
]);
}

View File

@ -56,7 +56,6 @@ class Hosts extends Component
// Für die aufklappbare Anleitung: dieselbe Adresse, die später in
// der Befehlszeile steht, damit hier nichts anderes behauptet wird
// als dort getan wird.
'archiveUrl' => \App\Support\HostTakeoverCommand::archiveUrl(),
]);
}
}

View File

@ -183,43 +183,38 @@ return [
'takeover' => [
'eyebrow' => 'Host-Übernahme',
'how_title' => 'So läuft eine Host-Übernahme ab',
'how_sub' => 'Sechs Schritte, etwa 20 bis 40 Minuten. Die ersten beiden passieren beim Anbieter.',
'how_sub' => 'Drei Schritte. Nur der erste passiert beim Anbieter.',
'rail_title' => 'Ablauf',
'badge_before' => 'vorher',
// Kurz — sie stehen in der Schiene nebeneinander, nicht als Absatz.
//
// Bis hierher standen hier SECHS Schritte, und zwei davon hießen
// „Rettungssystem". Sie beschrieben den Bootstrap-Weg: Rettungssystem
// starten, Befehlszeile kopieren, dort einfügen. Beim Speichern läuft
// aber die Provisionierungs-Pipeline — `EstablishSshTrust` meldet sich
// schlicht mit dem Root-Passwort an einer LAUFENDEN Debian-Maschine an,
// dann kommt Proxmox per Paket dazu. Die Seite beschrieb den einen Weg
// und ging den anderen.
's1_label' => 'Server bestellen',
's2_label' => 'Rettungssystem',
's3_label' => 'Host anlegen',
's4_label' => 'Zeile kopieren',
's5_label' => 'Einfügen',
's6_label' => 'Zusehen',
's2_label' => 'Host anlegen',
's3_label' => 'Zusehen',
's1_title' => 'Server bestellen',
's1_body' => 'Dediziert, nicht Cloud. Ohne /dev/kvm startet kein Gast, und das Skript weist die Maschine ab.',
's1_body' => 'Dediziert, nicht Cloud, mit vorinstalliertem Debian 13 und Zugang als Root. Ohne /dev/kvm startet später kein Gast.',
's2_title' => 'Rettungssystem starten',
's2_body' => 'Einschalten UND neu starten. Läuft der Server noch im alten System, weigert sich das Skript — es überschreibt Platten.',
's2_title' => 'Host anlegen',
's2_body' => 'Rechenzentrum, öffentliche IP, Root-Passwort. Das Passwort gilt nur für die erste Anmeldung und wird verworfen, sobald der Schlüssel liegt.',
's3_title' => 'Host anlegen',
's3_body' => 'Die Angaben unten. Danach entsteht der Einmal-Code, und seine Frist von 24 Stunden beginnt.',
's4_title' => 'Befehlszeile kopieren',
's4_body' => 'Sie erscheint nach dem Speichern und wird genau einmal gezeigt.',
's5_title' => 'Im Rettungssystem einfügen',
's5_body' => 'Per SSH als root, Zeile einfügen, Eingabetaste. Nichts abtippen, nichts ausfüllen.',
's5_hint' => 'Holt das Skript von :url und startet es. Mehr lädt der Server nicht nach.',
's6_title' => 'Zusehen',
's6_body' => 'Der Fortschritt läuft auf der Host-Seite mit. Bis der Tunnel steht, meldet der Server nichts — danach kommt alles auf einmal nach.',
's3_title' => 'Zusehen',
's3_body' => 'CluPilot meldet sich selbst an, installiert Proxmox, startet in dessen Kernel und baut Netzbrücke, Tunnel und Vorlage. Der Fortschritt läuft auf der Host-Seite mit.',
'requirements_title' => 'Beim Anbieter, vorher',
'requirements_sub' => 'Beides muss erledigt sein, bevor unten gespeichert wird — der Code läuft ab dem Speichern.',
'requirements_sub' => 'Das muss stehen, bevor unten gespeichert wird — danach meldet sich CluPilot an der Maschine an.',
'form_title' => 'Der Host',
'form_sub' => 'Vier Angaben. Den Rest holt sich der Server selbst.',
'form_sub' => 'Drei Angaben. Den Rest holt sich der Server selbst.',
'command_title' => 'Die Befehlszeile',
'command_sub' => 'Einfügen im Rettungssystem, als root. Sie trägt alles, was der Server vor dem Tunnel braucht.',

View File

@ -183,42 +183,35 @@ return [
'takeover' => [
'eyebrow' => 'Host takeover',
'how_title' => 'How a host takeover works',
'how_sub' => 'Six steps, roughly 20 to 40 minutes. The first two happen at the provider.',
'how_sub' => 'Three steps. Only the first happens at the provider.',
'rail_title' => 'Procedure',
'badge_before' => 'first',
// There were SIX steps here, two of them named "rescue system": boot the
// rescue system, copy the command line, paste it there. That described
// the bootstrap route. Saving actually starts the provisioning pipeline
// — EstablishSshTrust simply signs in with the root password on a
// RUNNING Debian machine, and Proxmox arrives as a package after that.
// The page described one route and took the other.
's1_label' => 'Order server',
's2_label' => 'Rescue system',
's3_label' => 'Add host',
's4_label' => 'Copy line',
's5_label' => 'Paste',
's6_label' => 'Watch',
's2_label' => 'Add host',
's3_label' => 'Watch',
's1_title' => 'Order the server',
's1_body' => 'Dedicated, not cloud. Without /dev/kvm no guest will start, and the script turns the machine away.',
's1_body' => 'Dedicated, not cloud, with Debian 13 preinstalled and root access. Without /dev/kvm no guest will start later on.',
's2_title' => 'Boot the rescue system',
's2_body' => 'Enable it AND restart. If the server is still in the old system the script refuses — it overwrites disks.',
's2_title' => 'Add the host',
's2_body' => 'Datacenter, public IP, root password. The password is only for the first sign-in and is discarded once the key is in place.',
's3_title' => 'Add the host',
's3_body' => 'The fields below. The one-time code is created after that, and its 24-hour window starts.',
's4_title' => 'Copy the command line',
's4_body' => 'It appears after saving and is shown exactly once.',
's5_title' => 'Paste it into the rescue system',
's5_body' => 'SSH in as root, paste, press enter. Nothing typed out, nothing filled in.',
's5_hint' => 'Fetches the installer from :url and starts it. The server downloads nothing else.',
's6_title' => 'Watch',
's6_body' => 'Progress appears on the host page. Until the tunnel is up the server reports nothing — then everything arrives at once.',
's3_title' => 'Watch',
's3_body' => 'CluPilot signs in by itself, installs Proxmox, reboots into its kernel and builds the bridge, the tunnel and the template. Progress appears on the host page.',
'requirements_title' => 'At the provider, first',
'requirements_sub' => 'Both have to be done before you save below — the code starts expiring the moment you do.',
'requirements_sub' => 'This has to be in place before you save below — CluPilot signs in to the machine straight after.',
'form_title' => 'The host',
'form_sub' => 'Four fields. The server works out the rest itself.',
'form_sub' => 'Three fields. The server works out the rest itself.',
'command_title' => 'The command line',
'command_sub' => 'Paste into the rescue system as root. It carries everything the server needs before the tunnel.',

View File

@ -1,7 +1,7 @@
@props([
// Der Schritt, an dem der Betreiber gerade steht: 3 solange das Formular
// offen ist, 4 sobald die Befehlszeile da ist.
'current' => 3,
// Der Schritt, an dem der Betreiber gerade steht: 2 solange das Formular
// offen ist, 3 sobald gespeichert wurde und die Pipeline läuft.
'current' => 2,
])
{{-- Der Ablauf als schmale Schiene, nicht als Textwand.
@ -20,13 +20,15 @@
dasselbe Problem wäre eine zweite Sprache. --}}
@php
// Drei, nicht sechs. Die drei gestrichenen — Rettungssystem starten, Zeile
// kopieren, dort einfügen — gehörten zum Bootstrap-Weg. Gefahren wird beim
// Speichern die Provisionierungs-Pipeline, und die meldet sich selbst an
// einer laufenden Debian-Maschine an. Der Betreiber tut wirklich weniger;
// die Schiene sagt es jetzt auch.
$steps = [
1 => __('hosts.takeover.s1_label'),
2 => __('hosts.takeover.s2_label'),
3 => __('hosts.takeover.s3_label'),
4 => __('hosts.takeover.s4_label'),
5 => __('hosts.takeover.s5_label'),
6 => __('hosts.takeover.s6_label'),
];
@endphp

View File

@ -1,22 +1,22 @@
@props(['archiveUrl' => ''])
{{-- Dieselben sechs Schritte, für die Stelle, an der noch nichts angelegt ist:
{{-- Dieselben drei Schritte, für die Stelle, an der noch nichts angelegt ist:
die Host-Liste. Dort ist es eine Auskunft und kein Ablauf also Zeilen mit
je einem Satz, nicht die Schiene mit Zustand.
Eine Zeile je Schritt, nach dem Muster von `x-ui.row`: links wozu, rechts
was. Die zwei Schritte beim Anbieter tragen eine Plakette, sonst liest sich
die Liste, als könne man oben anfangen und sich durcharbeiten und Schritt
2 ist eine halbe Stunde Wartezeit. --}}
was. Der Schritt beim Anbieter trägt eine Plakette, sonst liest sich die
Liste, als könne man oben anfangen und sich durcharbeiten.
Waren sechs, zwei davon „Rettungssystem". Sie beschrieben den
Bootstrap-Weg; gefahren wird beim Anlegen die Provisionierungs-Pipeline,
die sich selbst an einer laufenden Debian-Maschine anmeldet. Mit den
Schritten fiel auch `archiveUrl` weg die Adresse gehörte zur
Befehlszeile, die es hier nicht mehr gibt. --}}
@php
$steps = [
1 => [__('hosts.takeover.s1_title'), __('hosts.takeover.s1_body'), true],
2 => [__('hosts.takeover.s2_title'), __('hosts.takeover.s2_body'), true],
2 => [__('hosts.takeover.s2_title'), __('hosts.takeover.s2_body'), false],
3 => [__('hosts.takeover.s3_title'), __('hosts.takeover.s3_body'), false],
4 => [__('hosts.takeover.s4_title'), __('hosts.takeover.s4_body'), false],
5 => [__('hosts.takeover.s5_title'), __('hosts.takeover.s5_body'), false],
6 => [__('hosts.takeover.s6_title'), __('hosts.takeover.s6_body'), false],
];
@endphp
@ -32,9 +32,6 @@
</div>
<p class="text-sm leading-relaxed text-muted">
{{ $body }}
@if ($number === 5 && $archiveUrl)
<span class="block pt-0.5 text-xs">{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}</span>
@endif
</p>
</li>
@endforeach

View File

@ -6,25 +6,25 @@
<header class="animate-rise">
<p class="lbl">{{ __('hosts.takeover.eyebrow') }}</p>
<h1 class="mt-[7px] text-[23px] font-bold leading-[1.12] tracking-[-0.03em] text-ink min-[901px]:text-[30px]">
@if ($command === null)
@if ($createdUuid === null)
{{ __('hosts.create_title') }}
@else
{{ __('hosts.takeover.title', ['name' => $createdName]) }}
@endif
</h1>
<p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">
{{ $command === null ? __('hosts.create_sub') : __('hosts.takeover.subtitle') }}
{{ $createdUuid === null ? __("hosts.create_sub") : __("hosts.takeover.subtitle") }}
</p>
</header>
<div class="flex flex-col gap-6 lg:flex-row lg:items-start lg:gap-8">
{{-- Der Ablauf als Schiene, nicht als Wand. Sie beantwortet „wo stehe
ich" auf einen Blick und überlässt dem Hauptteil die Arbeit. --}}
<x-admin.takeover-rail :current="$command === null ? 3 : 4"
<x-admin.takeover-rail :current="$createdUuid === null ? 2 : 3"
class="animate-rise [animation-delay:40ms]" />
<div class="min-w-0 flex-1 space-y-8 lg:max-w-[820px]">
@if ($command === null)
@if ($createdUuid === null)
@if ($missingSettings)
<x-ui.alert variant="warning" class="animate-rise [animation-delay:60ms]">
<p class="font-medium">{{ __('hosts.takeover.missing_title') }}</p>
@ -32,9 +32,12 @@
</x-ui.alert>
@endif
{{-- Die zwei Schritte beim Anbieter. Sie stehen VOR dem
Formular, weil sie vorher erledigt sein müssen: der
Einmal-Code beginnt mit dem Speichern abzulaufen. --}}
{{-- Der eine Schritt beim Anbieter. Er steht VOR dem Formular,
weil er vorher erledigt sein muss: unmittelbar nach dem
Speichern meldet sich CluPilot an der Maschine an.
Hier standen zwei Zeilen die zweite hieß „Rettungssystem
starten" und gehörte zum Bootstrap-Weg, den diese Seite
nicht geht. --}}
<section class="animate-rise [animation-delay:60ms]">
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.requirements_title') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('hosts.takeover.requirements_sub') }}</p>
@ -43,9 +46,6 @@
<x-ui.row :label="__('hosts.takeover.s1_title')">
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s1_body') }}</p>
</x-ui.row>
<x-ui.row :label="__('hosts.takeover.s2_title')">
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s2_body') }}</p>
</x-ui.row>
</x-ui.panel>
</section>
@ -97,55 +97,24 @@
</div>
</form>
@else
{{-- Ab hier ist die Befehlszeile die Hauptsache. Sie bekommt die
volle Breite und steht ganz oben; alles andere ordnet sich
unter. --}}
{{-- Hier stand die Befehlszeile, die der Betreiber im
Rettungssystem einfügen sollte. Sie gehört zum
Bootstrap-Weg; beim Speichern läuft die
Provisionierungs-Pipeline, und die meldet sich selbst an.
Es gibt also nichts mehr zu kopieren nur noch etwas
anzusehen. --}}
<section class="animate-rise [animation-delay:60ms]">
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.command_title') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('hosts.takeover.command_sub') }}</p>
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.s3_title') }}</h2>
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s3_body') }}</p>
<div x-data="{ copied: false }" class="mt-4 overflow-hidden rounded-lg border border-accent-border bg-surface shadow-xs">
<div class="flex items-center justify-between gap-3 border-b border-line bg-accent-subtle px-4 py-2.5">
<p class="min-w-0 text-xs font-medium text-accent-text">{{ __('hosts.takeover.once_title') }}</p>
<button type="button"
class="inline-flex shrink-0 items-center gap-1.5 rounded border border-line bg-surface px-2.5 py-1 text-xs font-medium text-body transition hover:text-ink"
x-on:click="navigator.clipboard.writeText($refs.command.textContent.trim()); copied = true; setTimeout(() => copied = false, 2000)">
<x-ui.icon name="copy" class="size-4" x-show="!copied" />
<x-ui.icon name="check" class="size-4" x-show="copied" x-cloak />
<span x-text="copied ? @js(__('hosts.takeover.copied')) : @js(__('hosts.takeover.copy'))">{{ __('hosts.takeover.copy') }}</span>
</button>
</div>
{{-- Umbrechen statt waagerecht rollen: aus einem Kasten
mit Rollbalken markiert jemand die Hälfte und merkt
es erst auf der Maschine. --}}
<pre class="max-h-64 overflow-y-auto px-4 py-3.5 font-mono text-xs leading-relaxed text-ink break-all whitespace-pre-wrap"
x-ref="command">{{ $command }}</pre>
<div class="mt-5">
<a href="{{ route('admin.hosts.show', ['host' => $createdUuid]) }}" wire:navigate>
<x-ui.button variant="primary">
<x-slot:icon><x-ui.icon name="activity" class="size-4" /></x-slot:icon>
{{ __('hosts.takeover.watch') }}
</x-ui.button>
</a>
</div>
<p class="mt-2 text-xs leading-relaxed text-muted">{{ __('hosts.takeover.once_body') }}</p>
</section>
<section class="animate-rise [animation-delay:80ms]">
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('hosts.takeover.after_title') }}</h2>
<x-ui.panel class="mt-4">
<x-ui.row :label="__('hosts.takeover.s5_title')">
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s5_body') }}</p>
<p class="mt-1 text-xs leading-relaxed text-muted">{{ __('hosts.takeover.s5_hint', ['url' => $archiveUrl]) }}</p>
</x-ui.row>
<x-ui.row :label="__('hosts.takeover.s6_title')">
<p class="text-sm leading-relaxed text-muted">{{ __('hosts.takeover.s6_body') }}</p>
<div class="mt-3">
<a href="{{ route('admin.hosts.show', ['host' => $createdUuid]) }}" wire:navigate>
<x-ui.button variant="primary" size="sm">
<x-slot:icon><x-ui.icon name="activity" class="size-4" /></x-slot:icon>
{{ __('hosts.takeover.watch') }}
</x-ui.button>
</a>
</div>
</x-ui.row>
</x-ui.panel>
</section>
@endif

View File

@ -23,7 +23,7 @@
</span>
<x-ui.icon name="chevron-down" class="size-4 shrink-0 text-muted transition group-open:rotate-180" />
</summary>
<x-admin.takeover-steps :archive-url="$archiveUrl" class="border-t border-line" />
<x-admin.takeover-steps class="border-t border-line" />
</details>
{{-- Filter bar scales to many hosts (search + datacenter + status). --}}

View File

@ -57,7 +57,10 @@ it('creates a host and starts onboarding with an encrypted password', function (
// das Rettungssystem gibt es nur hier und nur jetzt. Wer den Betreiber
// weiterschickt, schickt ihn von dem einzigen Wert weg, den er braucht.
->assertNoRedirect()
->assertSee('clupilot-bootstrap.sh');
// Keine Befehlszeile mehr: das Speichern startet die Pipeline, die
// sich selbst anmeldet. Was bleibt, ist der Weg zum Zusehen.
->assertDontSee('clupilot-bootstrap.sh')
->assertSee(__('hosts.takeover.watch'));
$host = Host::query()->where('name', 'fsn-01')->first();
expect($host)->not->toBeNull()->and($host->status)->toBe('pending');

View File

@ -3,6 +3,7 @@
use App\Livewire\Admin\HostCreate;
use App\Livewire\Admin\Hosts;
use App\Models\Operator;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
beforeEach(function () {
@ -11,7 +12,7 @@ beforeEach(function () {
config()->set('provisioning.dns.platform_zone', 'clupilot.com');
});
function guidePage(string $component): \Livewire\Features\SupportTesting\Testable
function guidePage(string $component): Testable
{
return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')->test($component);
}
@ -30,14 +31,13 @@ it('explains the whole procedure before anything is created', function () {
// Alle sechs, als Schiene — kurze Beschriftungen, damit die Seite eine
// Seite bleibt und keine Textwand wird. Das war der zweite Fund: sechs
// Absätze übereinander beantworteten alles und zeigten nichts.
foreach (range(1, 6) as $step) {
foreach (range(1, 3) as $step) {
$page->assertSee(__("hosts.takeover.s{$step}_label"));
}
// Die zwei beim Anbieter ausführlich, weil sie VOR dem Speichern erledigt
// sein müssen und nur hier erklärt werden.
$page->assertSee(__('hosts.takeover.s1_body'))
->assertSee(__('hosts.takeover.s2_body'));
// Der beim Anbieter ausführlich, weil er VOR dem Speichern erledigt sein
// muss und nur hier erklärt wird.
$page->assertSee(__('hosts.takeover.s1_body'));
});
/**
@ -57,23 +57,10 @@ it('offers the same procedure on the hosts list', function () {
* durcharbeiten und Schritt 2 ist dann eine halbe Stunde Wartezeit mitten
* in einem laufenden Code.
*/
it('marks the two provider steps as things to do first', function () {
it('marks the provider step as something to do first', function () {
guidePage(HostCreate::class)->assertSee(__('hosts.takeover.badge_before'));
});
/**
* Die Anleitung nennt die Adresse, von der geladen wird, und es muss dieselbe
* sein, die später in der Zeile steht. Zwei Fassungen davon liefen auseinander,
* und der Unterschied fiele auf einem Server auf, der schon bestellt ist.
*/
it('names the same archive address the command will use', function () {
// Auf der Liste, wo die Anleitung Auskunft gibt. Auf der Anlegen-Seite
// steht die Adresse erst NACH dem Speichern: dort ist sie Teil der
// Befehlszeile und keine Angabe für sich, und vorher wäre sie eine
// Einzelheit mehr auf einer Seite, die vier Felder ausgefüllt haben will.
guidePage(Hosts::class)->assertSee('files.clupilot.test/bootstrap.tar.gz');
});
/**
* Vor dem Anlegen gibt es keine Befehlszeile sonst stünde dort eine, die zu
* keinem Host gehört.

View File

@ -1,10 +1,13 @@
<?php
use App\Livewire\Admin\HostCreate;
use App\Livewire\Admin\ReissueTakeover;
use App\Models\Datacenter;
use App\Models\Host;
use App\Models\Operator;
use App\Support\HostEnrolment;
use App\Support\HostTakeoverCommand;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
beforeEach(function () {
@ -25,7 +28,7 @@ beforeEach(function () {
);
});
function createHostAs(): \Livewire\Features\SupportTesting\Testable
function createHostAs(): Testable
{
return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(HostCreate::class)
@ -36,21 +39,46 @@ function createHostAs(): \Livewire\Features\SupportTesting\Testable
}
/**
* Die eine Frage, die diese Seite beantworten muss: was gebe ich auf dem Server
* ein? Steht die Zeile nicht da, ist die ganze Übernahme nicht benutzbar.
* Die Bootstrap-Zeile, wie sie der Weg über das Rettungssystem braucht.
*
* Sie wird hier direkt gebaut statt über eine Seite abgegriffen. Ihre
* Richtigkeit hat nichts damit zu tun, wer sie anzeigt und genau diese
* Kopplung war der Grund, warum fünf Tests umfielen, als die Anlegen-Seite
* aufhörte, sie zu zeigen.
*/
it('shows the command exactly once, right after creating the host', function () {
$page = createHostAs();
function takeoverCommandFor(Host $host): string
{
return HostTakeoverCommand::for($host, HostEnrolment::issueWithKeys($host));
}
$page->assertSee('curl')
->assertSee('clupilot-bootstrap.sh')
->assertSee('--code');
/**
* Die Anlegen-Seite zeigt keine Befehlszeile mehr.
*
* Sie gehörte zum Bootstrap-Weg: Rettungssystem starten, Zeile kopieren, dort
* einfügen. Das Speichern startet aber die Provisionierungs-Pipeline, und die
* meldet sich mit dem Root-Passwort selbst an einer laufenden Debian-Maschine
* an. Es gibt nichts zu kopieren die Seite behauptete es trotzdem.
*/
it('does not hand the operator a command line they do not need', function () {
createHostAs()
->assertDontSee('clupilot-bootstrap.sh')
->assertDontSee('--code')
// Stattdessen der Weg zu dem, was jetzt wirklich passiert.
->assertSee(__('hosts.takeover.watch'));
});
/**
* Der Weg über das Rettungssystem bleibt erreichbar nur nicht mehr von der
* Anlegen-Seite aus. Wer leeres Blech ohne vorinstalliertes System hat,
* bekommt die Zeile über Admin\ReissueTakeover.
*/
it('still issues the command line where the rescue route lives', function () {
$host = Host::factory()->create(['datacenter' => 'fsn']);
// Ein frisch geladenes Bauteil — also jemand, der die Seite neu lädt — hat
// nichts mehr. Der Code steht nur als Hash in der Datenbank.
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(HostCreate::class)
->assertDontSee('curl');
->test(ReissueTakeover::class, ['uuid' => $host->uuid])
->call('issue')
->assertSee('clupilot-bootstrap.sh');
});
/**
@ -59,7 +87,7 @@ it('shows the command exactly once, right after creating the host', function ()
* das merkt man erst auf einer Maschine, die schon bestellt ist.
*/
it('carries every value the script needs before the tunnel', function () {
$command = createHostAs()->get('command');
$command = takeoverCommandFor(Host::factory()->create(['datacenter' => 'fsn']));
foreach (['--code', '--wg-private', '--wg-ip', '--hub-pubkey', '--hub-endpoint', '--api', '--fqdn'] as $argument) {
expect($command)->toContain($argument);
@ -94,7 +122,7 @@ it('keeps host names out of the customer zone', function () {
$host = Host::factory()->create(['name' => 'fsn-01']);
expect(App\Support\HostTakeoverCommand::fqdnFor($host))
expect(HostTakeoverCommand::fqdnFor($host))
->toBe('fsn-01.node.clupilot.com');
});
@ -110,7 +138,7 @@ it('fetches the installer over the public hostname', function () {
// Konsolen-Hostnamen: die Konsole ist per Client-IP abgeriegelt
// (RestrictConsoleNetwork), und ein frischer Server im Rettungssystem steht
// in keinem der freigegebenen Bereiche.
expect(createHostAs()->get('command'))
expect(takeoverCommandFor(Host::factory()->create(['datacenter' => 'fsn'])))
->toContain('https://files.clupilot.test/bootstrap.tar.gz')
->not->toContain('admin.');
});
@ -120,7 +148,7 @@ it('fetches the installer over the public hostname', function () {
* zerlegt die Shell die Zeile an einer Stelle, die niemand sieht.
*/
it('quotes the keys so a shell cannot split them', function () {
$command = createHostAs()->get('command');
$command = takeoverCommandFor(Host::factory()->create(['datacenter' => 'fsn']));
expect($command)->toMatch("/--wg-private '[^']+'/")
->and($command)->toMatch("/--hub-pubkey '[^']+'/");
@ -131,13 +159,14 @@ it('quotes the keys so a shell cannot split them', function () {
* die Anleitung richtig und der Ablauf trotzdem kaputt.
*/
it('shows a code that actually resolves to the new host', function () {
$command = createHostAs()->get('command');
$host = Host::factory()->create(['datacenter' => 'fsn', 'name' => 'fsn-07']);
$command = takeoverCommandFor($host);
preg_match('/--code ([A-Za-z0-9]+)/', $command, $matches);
// Der Name kommt jetzt von CluPilot selbst, nicht mehr vom Formular — der
// erste Host in diesem (frischen) Testlauf für fsn heißt fsn-01.
expect(HostEnrolment::claim($matches[1])?->name)->toBe('fsn-01');
// Auf DIESEN Host, nicht bloß auf irgendeinen: der Code ist der Ausweis,
// mit dem sich der Server meldet.
expect(HostEnrolment::claim($matches[1])?->is($host))->toBeTrue();
});
/**
@ -155,16 +184,15 @@ it('warns before creating a host when the tunnel settings are missing', function
});
/**
* Nach dem Anlegen bleibt dieselbe Anleitung stehen, nur mit der Zeile darin.
* Ein Betreiber, der bei Schritt 5 nicht weiterweiß, soll nicht auf einer Seite
* landen, die nur noch aus einem Kasten besteht.
* Nach dem Anlegen bleibt derselbe Ablauf stehen, nur einen Schritt weiter.
* Die Schiene bleibt vollstaendig sichtbar: wer wissen will, was noch kommt,
* soll nicht auf einer Seite landen, die nur noch aus einem Knopf besteht.
*/
it('keeps the whole procedure visible after the host exists', function () {
createHostAs()
->assertSee(__('hosts.takeover.s1_title'))
->assertSee(__('hosts.takeover.s5_title'))
->assertSee(__('hosts.takeover.s6_title'))
->assertSee('--wg-private', escape: false);
->assertSee(__('hosts.takeover.s1_label'))
->assertSee(__('hosts.takeover.s2_label'))
->assertSee(__('hosts.takeover.s3_label'));
});
it('refuses to create a host without the permission', function () {