141 lines
5.5 KiB
PHP
141 lines
5.5 KiB
PHP
<?php
|
|
|
|
use App\Models\Host;
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\RunRunner;
|
|
use App\Services\Ssh\FakeRemoteShell;
|
|
use App\Services\Ssh\RemoteShell;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
// QUEUE_CONNECTION ist in dieser Suite `sync`: ohne das hier faehrt jedes
|
|
// `AdvanceRunJob::dispatch()` den Lauf sofort im Test, gegen eine echte,
|
|
// unerreichbare Adresse — zwei Minuten TCP-Zeitueberlauf je Host. Die Laeufe,
|
|
// die dieser Test wirklich fahren will, stoesst er unten selbst an.
|
|
beforeEach(fn () => 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');
|
|
});
|