EnsureNetworkBridge in die Pipeline: nach dem Neustart, vor der Nachpruefung
Der Schritt haengt jetzt zwischen RebootIntoPveKernel und ConfigureProxmox. Nach dem Neustart, weil dort ifupdown2 steht und der Tunnel gerade bewiesen hat, dass er einen Neustart ueberlebt. Vor ConfigureProxmox, dessen refuseWithoutBridge() unveraendert stehen bleibt und damit zur Nachpruefung wird — bauen UND pruefen, dasselbe Paar wie BuildVmTemplate -> VerifyVmTemplate. Beschriftung in beiden Sprachen, und ein Test verlangt das kuenftig von JEDEM Pipeline-Schritt statt nur vom neuen: wer einen einhaengt und die Sprachdateien vergisst, faellt im Test auf statt in der Konsole. bridge.sh und bridge-run.sh gehoeren ins Bootstrap-Archiv — es ist der einzige Weg, auf dem das Skript auf eine nackte Maschine kommt. Der End-to-End-Test lief rot, und zu Recht: sein Attrappen-Host sagte nichts ueber sein Netz, also lehnte der neue Schritt ab. Das war der Beweis, dass er wirklich haengt. Er bekommt jetzt einen Host, der seine Bruecke schon hat — den Bau-Pfad kann eine Attrappe nicht nachstellen, er lebt davon, dass die Verbindung abreisst und wiederkommt. Dafuer gibt es die Schritt-Tests und die drei Sandkasten-Laeufe. scriptBridgeState/scriptBridgeStatus liegen in tests/Pest.php, nicht in einer einzelnen Testdatei — zwei Dateien brauchen sie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/hostnamen-vergabe
parent
9888f898b8
commit
66bddd4bce
|
|
@ -36,6 +36,15 @@ return [
|
|||
Host\ConfigureWireguard::class,
|
||||
Host\InstallProxmoxVe::class,
|
||||
Host\RebootIntoPveKernel::class,
|
||||
// Nach dem Neustart, weil dort ifupdown2 steht (PrepareBaseSystem
|
||||
// installiert es) und der Tunnel gerade bewiesen hat, dass er einen
|
||||
// Neustart überlebt.
|
||||
//
|
||||
// Vor ConfigureProxmox, dessen refuseWithoutBridge() unverändert
|
||||
// stehen bleibt und damit zur Nachprüfung wird — dasselbe Paar wie
|
||||
// BuildVmTemplate → VerifyVmTemplate. Ein Bau, der still danebenging,
|
||||
// fällt dann hier auf und nicht erst beim ersten bezahlten Klon.
|
||||
Host\EnsureNetworkBridge::class,
|
||||
Host\ConfigureProxmox::class,
|
||||
Host\CreateAutomationToken::class,
|
||||
Host\VerifyProxmoxApi::class,
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ return [
|
|||
'configure_wireguard' => 'WireGuard einrichten',
|
||||
'install_proxmox_ve' => 'Proxmox VE installieren',
|
||||
'reboot_into_pve_kernel' => 'In Proxmox-Kernel neu starten',
|
||||
'ensure_network_bridge' => 'Netzbrücke vmbr0 bauen',
|
||||
'configure_proxmox' => 'Proxmox konfigurieren',
|
||||
'create_automation_token' => 'Automation-Token erstellen',
|
||||
'verify_proxmox_api' => 'Proxmox-API prüfen',
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ return [
|
|||
'configure_wireguard' => 'Configure WireGuard',
|
||||
'install_proxmox_ve' => 'Install Proxmox VE',
|
||||
'reboot_into_pve_kernel' => 'Reboot into Proxmox kernel',
|
||||
'ensure_network_bridge' => 'Build the vmbr0 network bridge',
|
||||
'configure_proxmox' => 'Configure Proxmox',
|
||||
'create_automation_token' => 'Create automation token',
|
||||
'verify_proxmox_api' => 'Verify Proxmox API',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Host;
|
||||
use App\Support\HostEnrolment;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
|
||||
/**
|
||||
* Das Archiv ist der einzige Weg, auf dem das Skript auf eine nackte Maschine
|
||||
* kommt. Was hier schiefgeht, merkt man erst im Rettungssystem eines Servers,
|
||||
|
|
@ -7,10 +11,10 @@
|
|||
*/
|
||||
beforeEach(function () {
|
||||
fakeServices();
|
||||
$this->code = App\Support\HostEnrolment::issue(App\Models\Host::factory()->create());
|
||||
$this->code = HostEnrolment::issue(Host::factory()->create());
|
||||
});
|
||||
|
||||
function fetchArchive(): \Illuminate\Testing\TestResponse
|
||||
function fetchArchive(): TestResponse
|
||||
{
|
||||
return test()->get('http://files.clupilot.test/bootstrap.tar.gz?code='.test()->code);
|
||||
}
|
||||
|
|
@ -39,6 +43,8 @@ it('unpacks to bootstrap/ with the script and its library', function () {
|
|||
->toContain('bootstrap/lib/report.sh')
|
||||
->toContain('bootstrap/lib/proxmox.sh')
|
||||
->toContain('bootstrap/lib/network.sh')
|
||||
->toContain('bootstrap/lib/bridge.sh')
|
||||
->toContain('bootstrap/lib/bridge-run.sh')
|
||||
->toContain('bootstrap/lib/traefik.sh')
|
||||
->toContain('bootstrap/lib/template.sh')
|
||||
->toContain('bootstrap/lib/register.sh')
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ it('drives a fresh host all the way to active (mocked)', function () {
|
|||
$s = fakeServices();
|
||||
$s['shell']->script('wg pubkey', CommandResult::success('HOSTPUBKEY0000='));
|
||||
scriptLiveTunnel($s['shell']);
|
||||
// Ein Host, der seine Brücke schon hat — der ISO-installierte oder der von
|
||||
// Hand gebaute. EnsureNetworkBridge fasst den nicht an und meldet advance.
|
||||
// Der BAU-Pfad steckt nicht hier: er ist abgekoppelt und lebt davon, dass
|
||||
// die Verbindung abreißt und wiederkommt, was eine Attrappe nicht
|
||||
// nachstellt. Dafür gibt es die Schritt-Tests und die drei Sandkasten-Läufe
|
||||
// in BridgeScriptTest, die bridge-run.sh wirklich ausführen.
|
||||
scriptBridgeState($s['shell'], up: true);
|
||||
$s['shell']->script('uname -r', CommandResult::success('6.8.12-4-pve'));
|
||||
$s['shell']->script('pveum user token add', CommandResult::success(
|
||||
json_encode(['full-tokenid' => 'automation@pve!clupilot', 'value' => 'tok-secret-123'])
|
||||
|
|
@ -89,6 +96,13 @@ it('does not duplicate external resources when a step re-runs after a crash', fu
|
|||
$s = fakeServices();
|
||||
$s['shell']->script('wg pubkey', CommandResult::success('HOSTPUBKEY0000='));
|
||||
scriptLiveTunnel($s['shell']);
|
||||
// Ein Host, der seine Brücke schon hat — der ISO-installierte oder der von
|
||||
// Hand gebaute. EnsureNetworkBridge fasst den nicht an und meldet advance.
|
||||
// Der BAU-Pfad steckt nicht hier: er ist abgekoppelt und lebt davon, dass
|
||||
// die Verbindung abreißt und wiederkommt, was eine Attrappe nicht
|
||||
// nachstellt. Dafür gibt es die Schritt-Tests und die drei Sandkasten-Läufe
|
||||
// in BridgeScriptTest, die bridge-run.sh wirklich ausführen.
|
||||
scriptBridgeState($s['shell'], up: true);
|
||||
$s['shell']->script('uname -r', CommandResult::success('6.8.12-4-pve'));
|
||||
$s['shell']->script('pveum user token add', CommandResult::success(
|
||||
json_encode(['full-tokenid' => 'automation@pve!clupilot', 'value' => 'tok-secret-123'])
|
||||
|
|
|
|||
|
|
@ -1655,31 +1655,6 @@ it('asks only for privileges Proxmox 9 still knows', function () {
|
|||
// Deshalb: erst den Zeitgeber stellen, dann umstellen, dann neu verbinden,
|
||||
// nachsehen, abbestellen.
|
||||
|
||||
/** Was der Host auf die Zustandsfrage antwortet. */
|
||||
function scriptBridgeState(FakeRemoteShell $shell, bool $up, string $iface = 'enp0s31f6', bool $physical = true): void
|
||||
{
|
||||
$shell->script('clupilot-bridge-state', CommandResult::success(implode("\n", [
|
||||
'up='.($up ? 'yes' : 'no'),
|
||||
'iface='.$iface,
|
||||
'physical='.($physical ? 'yes' : 'no'),
|
||||
'route=default via 49.12.121.65 dev '.($up ? 'vmbr0' : $iface),
|
||||
'',
|
||||
])));
|
||||
}
|
||||
|
||||
/** Was der Host auf die Statusfrage antwortet. */
|
||||
function scriptBridgeStatus(FakeRemoteShell $shell, string $state, bool $alive = false, string $phase = '', string $note = '', bool $rolledBack = false): void
|
||||
{
|
||||
$shell->script('clupilot-bridge-status', CommandResult::success(implode("\n", [
|
||||
'state='.$state,
|
||||
'alive='.($alive ? 'yes' : 'no'),
|
||||
'phase='.$phase,
|
||||
'note='.$note,
|
||||
'rolledback='.($rolledBack ? 'yes' : 'no'),
|
||||
'',
|
||||
])));
|
||||
}
|
||||
|
||||
it('fasst einen Host, der die Brücke schon hat, nicht an', function () {
|
||||
// pve-fns-1 hat sie von Hand. Ein Wiederanlauf, der sie umbaut, baut ein
|
||||
// funktionierendes Netz um — und riskiert dafür genau das, wofür es die
|
||||
|
|
@ -1980,3 +1955,56 @@ it('schickt nur Shell, die eine Shell auch parsen kann', function () {
|
|||
|
||||
expect($checked)->toBeGreaterThanOrEqual(6);
|
||||
});
|
||||
|
||||
it('baut die Brücke nach dem Neustart und vor ConfigureProxmox', function () {
|
||||
// Nach dem Neustart, weil dort ifupdown2 steht und der Tunnel gerade
|
||||
// bewiesen hat, dass er einen Neustart überlebt. Vor ConfigureProxmox, weil
|
||||
// dessen refuseWithoutBridge() die Nachprüfung ist — bauen UND prüfen,
|
||||
// dasselbe Paar wie BuildVmTemplate → VerifyVmTemplate.
|
||||
$pipeline = config('provisioning.pipelines.host');
|
||||
|
||||
$bridge = array_search(EnsureNetworkBridge::class, $pipeline, true);
|
||||
$reboot = array_search(RebootIntoPveKernel::class, $pipeline, true);
|
||||
$proxmox = array_search(ConfigureProxmox::class, $pipeline, true);
|
||||
|
||||
expect($bridge)->not->toBeFalse()
|
||||
->and($reboot)->toBeLessThan($bridge)
|
||||
->and($bridge)->toBeLessThan($proxmox);
|
||||
});
|
||||
|
||||
it('lässt ConfigureProxmox die Nachprüfung bleiben', function () {
|
||||
// Der Schritt davor baut. Dieser prüft. Beides, nicht eines von beiden —
|
||||
// sonst meldet ein Bau, der still danebenging, trotzdem Erfolg.
|
||||
//
|
||||
// CommandResult::failure(int $exitCode = 1, string $stderr = '') — zwei
|
||||
// Parameter, nicht drei.
|
||||
$s = fakeServices();
|
||||
$host = Host::factory()->active()->create();
|
||||
$run = hostRun($host);
|
||||
proveTunnel($run, $host);
|
||||
$s['shell']->script('ip link show vmbr0', CommandResult::failure(1, 'Device does not exist'));
|
||||
|
||||
expect(app(ConfigureProxmox::class)->execute($run)->type)->toBe('fail');
|
||||
});
|
||||
|
||||
it('beschriftet den Schritt in beiden Sprachen', function () {
|
||||
// Die Konsole zeigt eine Zeile je Schritt. Eine ohne Beschriftung zeigt den
|
||||
// Schlüssel — der Betreiber säße beim Zusehen vor 'hosts.step.…'.
|
||||
foreach (['de', 'en'] as $locale) {
|
||||
expect(trans('hosts.step.ensure_network_bridge', [], $locale))
|
||||
->not->toBe('hosts.step.ensure_network_bridge', "Beschriftung fehlt in {$locale}");
|
||||
}
|
||||
});
|
||||
|
||||
it('legt jeden Pipeline-Schritt mit einer Beschriftung ab', function () {
|
||||
// Nicht nur den neuen. Wer künftig einen Schritt einhängt und die
|
||||
// Sprachdateien vergisst, fällt hier auf statt in der Konsole.
|
||||
foreach (config('provisioning.pipelines.host') as $class) {
|
||||
$key = app($class)->key();
|
||||
|
||||
foreach (['de', 'en'] as $locale) {
|
||||
expect(trans("hosts.step.{$key}", [], $locale))
|
||||
->not->toBe("hosts.step.{$key}", "Beschriftung fehlt: {$key} ({$locale})");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -224,6 +224,43 @@ function withRealEnv(array $vars, Closure $callback): mixed
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Was der Host auf die Zustandsfrage von EnsureNetworkBridge antwortet.
|
||||
*
|
||||
* `up` ist absichtlich EIN Wert aus drei Fakten — Brücke da, trägt die
|
||||
* Standardroute, hat eine Adresse. Der Schritt fragt sie in einem Rundlauf ab,
|
||||
* weil eine vmbr0 ohne Adresse und ohne Route eine Brücke im Sinne von
|
||||
* `ip link show` ist und sonst nichts.
|
||||
*/
|
||||
function scriptBridgeState(FakeRemoteShell $shell, bool $up, string $iface = 'enp0s31f6', bool $physical = true): void
|
||||
{
|
||||
$shell->script('clupilot-bridge-state', CommandResult::success(implode("\n", [
|
||||
'up='.($up ? 'yes' : 'no'),
|
||||
'iface='.$iface,
|
||||
'physical='.($physical ? 'yes' : 'no'),
|
||||
'route=default via 49.12.121.65 dev '.($up ? 'vmbr0' : $iface),
|
||||
'',
|
||||
])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Was der Host über einen laufenden Brückenbau meldet.
|
||||
*
|
||||
* `alive` wird auf dem Host gerechnet (`kill -0`), weil die PID hier nichts
|
||||
* bedeutet; `rolledback` ist die Marke, die der Zeitgeber ZULETZT setzt.
|
||||
*/
|
||||
function scriptBridgeStatus(FakeRemoteShell $shell, string $state, bool $alive = false, string $phase = '', string $note = '', bool $rolledBack = false): void
|
||||
{
|
||||
$shell->script('clupilot-bridge-status', CommandResult::success(implode("\n", [
|
||||
'state='.$state,
|
||||
'alive='.($alive ? 'yes' : 'no'),
|
||||
'phase='.$phase,
|
||||
'note='.$note,
|
||||
'rolledback='.($rolledBack ? 'yes' : 'no'),
|
||||
'',
|
||||
])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Was ein stehender Tunnel auf dem Host meldet.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue