Queue::fake()); /** * Der Nachzieh-Griff für Hosts, die vor dem Frühwarnsystem übernommen wurden. * * Ihr /etc/nftables.conf kennt `clupilot_blocked` nicht, dort scheitert jedes * `nft add element` — und die Sperre steht in Datenbank, Portal, Konsole und in * der Mail an den Kunden als aktiv, in der Firewall aber nie. Der * Übernahme-Lauf hilft nicht: `SecureHostFirewall` kürzt sich über den * `host_firewall`-Brotkrumen ab, und der hängt am LAUF, nicht am Host — genau * das behauptete das Runbook bis zu dieser Fix-Welle fälschlich. */ function uebernommenerHost(array $attributes = []): Host { return Host::factory()->active()->create(array_merge([ 'node' => 'pve', 'ssh_host_key' => 'SHA256:abc', ], $attributes)); } it('startet je uebernommenem Host einen host-firewall-Lauf', function () { $host = uebernommenerHost(); $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful(); $run = ProvisioningRun::query()->where('subject_id', $host->id)->firstOrFail(); expect($run->pipeline)->toBe('host-firewall') ->and($run->subject_type)->toBe(Host::class) ->and($run->current_step)->toBe(0); }); it('schreibt das Regelwerk MIT den Sperrmengen, obwohl der Host laengst uebernommen ist', function () { // Der eigentliche Punkt: der Brotkrumen des ALTEN Laufs darf den neuen // nicht abkuerzen. Ohne den eigenen Lauf taete der Schritt nichts und // meldete trotzdem „erledigt". $shell = new FakeRemoteShell; app()->instance(RemoteShell::class, $shell); $host = uebernommenerHost(); // Der Brotkrumen der Uebernahme, an ihrem eigenen Lauf. $alt = ProvisioningRun::factory()->forHost($host)->create([ 'status' => ProvisioningRun::STATUS_COMPLETED, ]); $alt->resources()->create([ 'host_id' => $host->id, 'kind' => 'host_firewall', 'external_id' => 'nftables', ]); $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful(); $neu = ProvisioningRun::query()->where('pipeline', 'host-firewall')->firstOrFail(); app(RunRunner::class)->advance($neu); $regelwerk = $shell->files()['/etc/nftables.conf'] ?? ''; expect($regelwerk)->toContain('clupilot_blocked') ->and($neu->fresh()->status)->toBe(ProvisioningRun::STATUS_COMPLETED); }); it('laesst einen Host in Ruhe, der keinen Tunnel hat', function () { // Der Schritt verbindet ueber die WireGuard-Adresse und prueft den Tunnel // von der Hostseite. Ohne beides gibt es nichts nachzuziehen. Host::factory()->create(['status' => 'pending']); $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful(); expect(ProvisioningRun::query()->where('pipeline', 'host-firewall')->count())->toBe(0); }); it('faengt einen Host nicht zweimal an, solange ein Lauf offen ist', function () { $host = uebernommenerHost(); ProvisioningRun::factory()->forHost($host)->create([ 'status' => ProvisioningRun::STATUS_RUNNING, ]); $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful(); expect(ProvisioningRun::query()->where('pipeline', 'host-firewall')->count())->toBe(0); }); it('aendert unter --dry-run gar nichts', function () { uebernommenerHost(); $this->artisan('clupilot:refresh-host-firewall', ['--dry-run' => true])->assertSuccessful(); expect(ProvisioningRun::count())->toBe(0); }); it('nimmt mit --host genau einen', function () { $gemeint = uebernommenerHost(['name' => 'pve-fsn-07']); uebernommenerHost(['name' => 'pve-fsn-08']); $this->artisan('clupilot:refresh-host-firewall', ['--host' => 'pve-fsn-07'])->assertSuccessful(); expect(ProvisioningRun::query()->where('pipeline', 'host-firewall')->pluck('subject_id')->all()) ->toBe([$gemeint->id]); }); it('stellt einen laufenden Host NICHT auf error, wenn der Nachzieh-Lauf scheitert', function () { // Deshalb eine eigene Pipeline und nicht 'host': RunRunner::failRun() // loest den Subjekt-Haken nur aus, wenn der gescheiterte Lauf DER Lauf des // Subjekts ist. Ein Nachziehen darf einen bezahlten, laufenden Host nicht // als kaputt markieren. $shell = new FakeRemoteShell; $shell->failConnect = true; app()->instance(RemoteShell::class, $shell); $host = uebernommenerHost(); $this->artisan('clupilot:refresh-host-firewall')->assertSuccessful(); $run = ProvisioningRun::query()->where('pipeline', 'host-firewall')->firstOrFail(); // Bis zum Ende der Versuche fahren, statt eine Zahl zu raten: was hier // zaehlt, ist der Zustand des HOSTS, nachdem der Lauf endgueltig // gescheitert ist. for ($i = 0; $i < 20 && ! in_array($run->fresh()->status, [ProvisioningRun::STATUS_FAILED, ProvisioningRun::STATUS_COMPLETED], true); $i++) { $run->forceFill(['next_attempt_at' => null])->save(); app(RunRunner::class)->advance($run); } expect($run->fresh()->status)->toBe(ProvisioningRun::STATUS_FAILED) ->and($host->fresh()->status)->toBe('active'); });