Host anlegen: die Seite beschrieb das Rettungssystem und startete die Pipeline
parent
acc4193071
commit
2c30d1fb86
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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). --}}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue