471 lines
22 KiB
PHP
471 lines
22 KiB
PHP
<?php // tests/Feature/Cancellation/ArchiveAndTearDownTest.php
|
|
|
|
use App\Actions\ArchiveAndTearDown;
|
|
use App\Models\Host;
|
|
use App\Models\Instance;
|
|
use App\Services\Proxmox\FakeProxmoxClient;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
/**
|
|
* Eine Instanz im Zustand NACH dem Laufzeitende, so wie ihn
|
|
* EndInstanceService hinterlässt (status 'ended', Adresse weg), und mit der
|
|
* Frist von vierzehn Tagen längst um — nach dem Muster von
|
|
* gekuendigteInstanz() aus DashboardCountdownTest.
|
|
*
|
|
* Anders als dort mit Host und VMID, denn hier geht es um die Maschine selbst:
|
|
* ohne beides gibt es nichts abzubauen.
|
|
*/
|
|
function abgelaufeneInstanz(int $vmid, ?Carbon $endeteVor = null): Instance
|
|
{
|
|
$host = Host::factory()->active()->create(['node' => 'pve']);
|
|
|
|
return Instance::factory()->create([
|
|
'host_id' => $host->id,
|
|
'vmid' => $vmid,
|
|
'status' => 'ended',
|
|
'service_ends_at' => $endeteVor ?? now()->subDays(20),
|
|
]);
|
|
}
|
|
|
|
it('faehrt herunter, archiviert, prueft nach, und loescht erst dann', function () {
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'size' => 4096]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeTrue();
|
|
|
|
// Die Reihenfolge IST die Zusicherung: Herunterfahren vor dem Sichern,
|
|
// Sichern vor dem Loeschen.
|
|
expect($pve->shutdownCalls)->toHaveCount(1)
|
|
->and($pve->backupCalls)->toHaveCount(1)
|
|
->and($pve->deletedVmids)->toBe([201]);
|
|
|
|
expect($instance->fresh()->archive_volid)->toBe('local:backup/vzdump-qemu-201.vma.zst')
|
|
->and($instance->fresh()->torn_down_at)->not->toBeNull();
|
|
});
|
|
|
|
it('LOESCHT NICHT, wenn nach dem Sichern kein Archiv daliegt', function () {
|
|
// Der Fall, der alles traegt. Ein vzdump kann mit einer Auftragskennung
|
|
// enden und trotzdem nichts hinterlassen — volle Ablage, abgebrochener
|
|
// Lauf, ein Fehler im Gast. Wer sich auf die Kennung verlaesst statt
|
|
// nachzusehen, loescht eine Maschine, deren Archiv es nicht gibt.
|
|
$pve = new FakeProxmoxClient; // backups bleibt leer
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('loescht auch dann nicht, wenn das Herunterfahren scheitert', function () {
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->shutdownThrows = true;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->backupCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('faellt nicht ueber eine Instanz, die schon abgebaut ist', function () {
|
|
// Wiederholbarkeit: ein zweiter Lauf nach einem Absturz darf nicht
|
|
// versuchen, eine Maschine zu loeschen, die es nicht mehr gibt.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
$instance->update(['torn_down_at' => now()->subDay()]);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeTrue()
|
|
->and($pve->deletedVmids)->toBe([]);
|
|
});
|
|
|
|
it('ruehrt eine Instanz nicht an, deren Frist noch nicht um ist', function () {
|
|
// Die vierzehn Tage sind der Sinn der Sache. Eine Maschine, die gestern
|
|
// ablief, ist die Rueckfahrkarte eines Menschen, der es sich vielleicht
|
|
// gerade anders ueberlegt.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201, endeteVor: now()->subDays(3));
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([]);
|
|
});
|
|
|
|
it('rührt eine Instanz nicht an, solange der vierzehnte Tag noch läuft', function () {
|
|
// Der Rand der Frist, fünf Minuten davor. Er gehört noch dem Kunden: eine
|
|
// Frist, die einen Tag zu früh zuschlägt, nimmt jedem Menschen einen ganzen
|
|
// Tag seiner Rückfahrkarte weg — und niemandem fiele es auf.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201, endeteVor: now()->subDays(14)->addMinutes(5));
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([]);
|
|
});
|
|
|
|
it('baut ab, sobald der vierzehnte Tag um ist', function () {
|
|
// Die andere Seite desselben Randes, fünf Minuten danach. Beide zusammen
|
|
// halten die Frist fest: verschöbe sie jemand um einen Tag — in welche
|
|
// Richtung auch immer —, wird eine der beiden Prüfungen rot.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201, endeteVor: now()->subDays(14)->subMinutes(5));
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeTrue()
|
|
->and($pve->deletedVmids)->toBe([201]);
|
|
});
|
|
|
|
it('rührt eine Instanz nicht an, deren Dienst gar nicht beendet ist', function () {
|
|
// Eine laufende Instanz mit einem alten service_ends_at ist kein Abbaufall:
|
|
// solange EndInstanceService sie nicht auf 'ended' gesetzt hat, ist die
|
|
// Adresse noch da und jemand arbeitet damit. Nur der eine Zustand zählt,
|
|
// nicht das Datum allein.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
$instance->update(['status' => 'active']);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([]);
|
|
});
|
|
|
|
it('fährt gar nichts herunter, wenn es die eingestellte Ablage auf dem Knoten nicht gibt', function () {
|
|
// Die Ablage ist eine Annahme über die Einrichtung der Hosts, kein Wissen.
|
|
// Ein Name, den es dort nicht gibt, muss laut scheitern — und zwar bevor
|
|
// eine Kundenmaschine dafür heruntergefahren wurde.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->storage = [['storage' => 'local-lvm', 'type' => 'lvmthin', 'content' => 'images,rootdir']];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->backupCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->toContain('local');
|
|
});
|
|
|
|
it('fährt gar nichts herunter, wenn die Ablage keine Sicherungen aufnimmt', function () {
|
|
// Es gibt sie, sie heißt richtig — und sie darf keine Sicherungen tragen.
|
|
// Ohne diese Prüfung fiele das erst beim vzdump auf, mit einer bereits
|
|
// heruntergefahrenen Kundenmaschine daneben.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->storage = [['storage' => 'local', 'type' => 'dir', 'content' => 'iso,vztmpl']];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->backupCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->toContain('local');
|
|
});
|
|
|
|
it('sichert nicht, wenn der Gast nach dem Herunterfahren weiterläuft', function () {
|
|
// shutdownVm() liefert eine Auftragskennung, keine Zusage. Ein Gast, der
|
|
// ACPI ignoriert, läuft weiter — und eine laufende Datenbank zu sichern
|
|
// und dann zu löschen hieße, ein Archiv mit halb geschriebenen Tabellen
|
|
// aufzubewahren.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->shutdownIgnored = true;
|
|
$pve->runningVmids = [201];
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toHaveCount(1)
|
|
->and($pve->backupCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('löscht nicht gegen ein Archiv von gestern', function () {
|
|
// Der zweite Weg, an dem die reine Ja/Nein-Frage „liegt da etwas?"
|
|
// vorbeigeht: die nächtliche Sicherung liegt auf derselben Ablage. Gegen
|
|
// sie zu löschen hieße, ein Archiv aufzubewahren, dem der letzte Tag
|
|
// fehlt — und es fiele erst auf, wenn es zu spät ist.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [[
|
|
'volid' => 'local:backup/vzdump-qemu-201-gestern.vma.zst',
|
|
'ctime' => now()->subDay()->timestamp,
|
|
]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->backupCalls)->toHaveCount(1)
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
->and($instance->fresh()->archive_volid)->toBeNull();
|
|
});
|
|
|
|
it('merkt sich das frische Archiv, nicht das danebenliegende alte', function () {
|
|
// Der Normalfall auf einer echten Ablage: die nächtliche Sicherung von
|
|
// gestern liegt neben dem Dump von gerade eben. Vermerkt werden muss der
|
|
// frische — ein Kunde, der in zwölf Monaten nach seinen Daten fragt, soll
|
|
// nicht den Stand von vorgestern bekommen.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [
|
|
['volid' => 'local:backup/vzdump-qemu-201-gestern.vma.zst', 'ctime' => now()->subDay()->timestamp],
|
|
['volid' => 'local:backup/vzdump-qemu-201-jetzt.vma.zst', 'ctime' => now()->timestamp],
|
|
];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeTrue()
|
|
->and($pve->deletedVmids)->toBe([201])
|
|
->and($instance->fresh()->archive_volid)->toBe('local:backup/vzdump-qemu-201-jetzt.vma.zst')
|
|
->and($instance->fresh()->archived_at)->not->toBeNull();
|
|
});
|
|
|
|
it('löscht nicht gegen mehrere Archive, von denen keines einen Zeitpunkt nennt', function () {
|
|
// Auf `local` liegen neben dem frischen Dump die nächtlichen Sicherungen
|
|
// derselben VM. Fehlt allen der Zeitpunkt, lassen sie sich nicht ordnen —
|
|
// vermerkt würde irgendeines, womöglich das von vorgestern. Eine Reihe, die
|
|
// sich nicht sortieren lässt, ist keine Grundlage für ein Löschen. Ein
|
|
// EINZELNES Archiv ohne Zeitpunkt bleibt dagegen gültig; das ist der Fall
|
|
// der ersten Prüfung dieser Datei.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [
|
|
['volid' => 'local:backup/vzdump-qemu-201-a.vma.zst', 'size' => 4096],
|
|
['volid' => 'local:backup/vzdump-qemu-201-b.vma.zst', 'size' => 4096],
|
|
];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
->and($instance->fresh()->archive_volid)->toBeNull()
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('löscht nicht, wenn das Archiv keine Kennung trägt', function () {
|
|
// Ein Archiv, dessen Ort nicht notiert werden kann, ist in zwölf Monaten
|
|
// keins: niemand findet es wieder, und niemand räumt es je auf.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [['size' => 4096, 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('löscht nicht, wenn der Sicherungsauftrag mit einem Fehler endet', function () {
|
|
// Die volle Ablage, wie Proxmox sie meldet. Dass daneben ein älteres
|
|
// Archiv derselben VM liegt, macht den Fehlschlag nicht zum Erfolg.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->taskExitStatus = 'no space left on device';
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->toContain('no space left on device');
|
|
});
|
|
|
|
it('lässt sich von Hinweisen im Sicherungsauftrag nicht aufhalten', function () {
|
|
// Proxmox beendet einen vzdump auch mit „WARNINGS: n", wenn das Archiv
|
|
// sauber geschrieben wurde. Das als Fehlschlag zu lesen hieße, den Abbau an
|
|
// einer Meldung hängen zu lassen, die kein Archiv verhindert — der Beweis
|
|
// ist die Nachschau auf der Ablage, nicht diese Zeichenkette.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->taskExitStatus = 'WARNINGS: 1';
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeTrue()
|
|
->and($pve->deletedVmids)->toBe([201]);
|
|
});
|
|
|
|
it('löscht nicht, solange der Sicherungsauftrag noch läuft', function () {
|
|
// Ein vzdump liefert seine Kennung sofort und arbeitet danach minutenlang
|
|
// weiter. Wer nicht abwartet, fragt die Ablage, bevor das Archiv darauf
|
|
// liegt. Läuft er über die Frist hinaus, bleibt die Maschine stehen — der
|
|
// nächste Lauf sieht erneut nach.
|
|
$pve = new class extends FakeProxmoxClient
|
|
{
|
|
public function taskStatus(string $node, string $upid): array
|
|
{
|
|
// Nur der vzdump hängt. Das Herunterfahren davor war fertig —
|
|
// sonst käme diese Prüfung nie bis zum Sichern.
|
|
return str_contains($upid, 'vzdump')
|
|
? ['status' => 'running']
|
|
: parent::taskStatus($node, $upid);
|
|
}
|
|
};
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
// Ohne Wartezeit, damit die Prüfung nicht wirklich eine Stunde wartet.
|
|
$abbau = app(ArchiveAndTearDown::class);
|
|
$abbau->backupWaitSeconds = 0;
|
|
|
|
expect($abbau($instance))->toBeFalse()
|
|
->and($pve->backupCalls)->toHaveCount(1)
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('löscht nicht, wenn die Instanz zwischen Sichern und Löschen wiederbelebt wird', function () {
|
|
// Zwischen dem Dump und dem Löschen liegen Minuten. Wird die Instanz in
|
|
// dieser Zeit von jemandem angefasst, ist sie keine, die gelöscht werden
|
|
// darf — die gemerkte Kopie im Speicher weiß davon nichts, also wird der
|
|
// Datensatz unmittelbar davor neu gelesen.
|
|
$pve = new class extends FakeProxmoxClient
|
|
{
|
|
public ?Instance $wiederbelebt = null;
|
|
|
|
public function backupsFor(string $node, int $vmid, string $storage): array
|
|
{
|
|
$this->wiederbelebt?->update(['status' => 'active']);
|
|
|
|
return parent::backupsFor($node, $vmid, $storage);
|
|
}
|
|
};
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
// Eine ZWEITE Kopie desselben Datensatzes, so wie sie ein anderer Vorgang
|
|
// hätte — die Aktion darf ihrer eigenen nicht glauben.
|
|
$pve->wiederbelebt = Instance::find($instance->id);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
// Das Archiv existiert wirklich und bleibt vermerkt: der Dump ist
|
|
// gelaufen, und ihn zu vergessen hieße, ihn beim nächsten Lauf noch
|
|
// einmal zu bezahlen.
|
|
->and($instance->fresh()->archive_volid)->toBe('local:backup/vzdump-qemu-201.vma.zst')
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|
|
|
|
it('behält das Archiv am Datensatz, wenn das Löschen scheitert', function () {
|
|
// Der Zwischenstand, für den es zwei Zeitstempel gibt: gesichert ja,
|
|
// gelöscht nein. Ginge archive_volid dabei verloren, liefe der nächste Lauf
|
|
// gegen dieselbe Maschine noch einen Dump — und der erste läge unauffindbar
|
|
// auf der Ablage.
|
|
$pve = new class extends FakeProxmoxClient
|
|
{
|
|
public function deleteVm(string $node, int $vmid): void
|
|
{
|
|
throw new RuntimeException('Proxmox: VM is locked (backup)');
|
|
}
|
|
};
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($instance->fresh()->archive_volid)->toBe('local:backup/vzdump-qemu-201.vma.zst')
|
|
->and($instance->fresh()->archived_at)->not->toBeNull()
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
->and($instance->fresh()->teardown_error)->toContain('locked');
|
|
});
|
|
|
|
it('trägt den Abbau nach, wenn die Maschine schon fort ist und das Archiv feststeht', function () {
|
|
// Der Absturz im Millisekundenfenster zwischen dem Löschen und dem
|
|
// Vermerk. Ohne diesen Weg zurück versuchte jeder weitere Lauf, einen
|
|
// Geist herunterzufahren, und die Instanz bliebe für immer im Fehler
|
|
// stehen. Es wird dabei nichts gelöscht — es gibt nichts mehr.
|
|
$pve = new FakeProxmoxClient; // kennt vmid 201 nicht, vmExists() ist falsch
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
$instance->update([
|
|
'archive_volid' => 'local:backup/vzdump-qemu-201.vma.zst',
|
|
'archived_at' => now()->subMinutes(5),
|
|
]);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeTrue()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->backupCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->not->toBeNull();
|
|
});
|
|
|
|
it('bucht keine Maschine als abgebaut, wenn der Knoten gar nicht antwortet', function () {
|
|
// Die Eingangslage entsteht im Regelbetrieb: ein Lauf hat gesichert und ist
|
|
// am Löschen gescheitert, weil der Knoten hängt. Am nächsten Tag steht
|
|
// `archive_volid` am Datensatz, und `vmExists()` — ein GET ohne `->throw()` —
|
|
// antwortet wegen desselben hakenden Knotens mit „nein". Ohne ein
|
|
// Lebenszeichen davor würde hier eine LAUFENDE Kundenmaschine als abgebaut
|
|
// verbucht und danach nie wieder angesehen.
|
|
$pve = new class extends FakeProxmoxClient
|
|
{
|
|
public function nodeStorage(string $node): array
|
|
{
|
|
throw new RuntimeException('Proxmox: 595 Connection refused');
|
|
}
|
|
};
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
$instance->update([
|
|
'archive_volid' => 'local:backup/vzdump-qemu-201.vma.zst',
|
|
'archived_at' => now()->subDay(),
|
|
'teardown_error' => 'Proxmox: VM is locked (backup)',
|
|
]);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
->and($instance->fresh()->teardown_error)->toContain('Connection refused');
|
|
});
|
|
|
|
it('lässt keinen zweiten Lauf über dieselbe Instanz', function () {
|
|
// Zwei Läufe nebeneinander — der Zeitplan und ein Betreiber von Hand —
|
|
// würden zweimal sichern und zweimal löschen; der zweite fände dabei eine
|
|
// Maschine vor, die der erste gerade wegnimmt. Hier hält bereits jemand die
|
|
// Sperre, und der Lauf hat nichts hinzuzufügen.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201.vma.zst', 'ctime' => now()->timestamp]];
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
|
|
$fremdeSperre = Cache::lock('instance-teardown:'.$instance->uuid, 60);
|
|
expect($fremdeSperre->get())->toBeTrue();
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->shutdownCalls)->toBe([])
|
|
->and($pve->backupCalls)->toBe([])
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull();
|
|
|
|
// Und die fremde Sperre steht noch: der abgewiesene Lauf gibt im `finally`
|
|
// nicht frei, was ihm nie gehört hat.
|
|
expect(Cache::lock('instance-teardown:'.$instance->uuid, 60)->get())->toBeFalse();
|
|
|
|
$fremdeSperre->release();
|
|
});
|
|
|
|
it('rührt eine Instanz ohne Maschine nicht an', function () {
|
|
// Ein Datensatz ohne VMID hat nichts, was sich sichern oder löschen ließe.
|
|
// Er wird nicht stillschweigend als abgebaut gebucht: das wäre ein Haken an
|
|
// einer Maschine, die vielleicht doch irgendwo läuft.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$instance = abgelaufeneInstanz(vmid: 201);
|
|
$instance->update(['vmid' => null]);
|
|
|
|
expect(app(ArchiveAndTearDown::class)($instance))->toBeFalse()
|
|
->and($pve->deletedVmids)->toBe([])
|
|
->and($instance->fresh()->torn_down_at)->toBeNull()
|
|
->and($instance->fresh()->teardown_error)->not->toBeNull();
|
|
});
|