Kuendigung B1, Task 1: einzelner vzdump und Nachschau auf der Ablage

backupNow() faehrt einen einmaligen vzdump (mode=stop, weil der Abbau die
Maschine vorher ohnehin herunterfaehrt) und liefert die Auftragskennung.
backupsFor() ist das eigentliche Fundament fuer den spaeteren Abbau: sie
fragt nach, was auf der Ablage wirklich liegt, statt der Auftragskennung zu
vertrauen — ein vzdump kann enden und trotzdem kein Archiv hinterlassen.

$storage ist an beiden Methoden ein Pflichtparameter, keine Einstellung: der
Client raet nicht, welche Ablage gemeint ist, und wiederholt damit nicht den
fest verdrahteten "local"-Namen aus createBackupJob().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-04 10:48:58 +02:00
parent df64b17c77
commit 6ab868c522
4 changed files with 110 additions and 0 deletions

View File

@ -345,4 +345,30 @@ class FakeProxmoxClient implements ProxmoxClient
return $id;
}
/** @var array<int, array{node: string, vmid: int, storage: string}> jeder einzelne vzdump-Lauf, der angestossen wurde */
public array $backupCalls = [];
/**
* Was auf der Ablage wirklich liegt vom Auftrag getrennt gefuehrt, weil
* genau das der Punkt ist: backupNow() hinterlaesst hier standardmaessig
* NICHTS, ein Test muss das Archiv wie ein echtes vzdump extra eintragen.
* Schluessel ist die VMID als String, nicht die Ablage reicht fuer die
* Faelle, die dieser Fake abbilden muss.
*
* @var array<string, array<int, array<string, mixed>>>
*/
public array $backups = [];
public function backupNow(string $node, int $vmid, string $storage): string
{
$this->backupCalls[] = ['node' => $node, 'vmid' => $vmid, 'storage' => $storage];
return 'UPID:pve:vzdump:'.$vmid;
}
public function backupsFor(string $node, int $vmid, string $storage): array
{
return $this->backups[(string) $vmid] ?? [];
}
}

View File

@ -253,4 +253,35 @@ class HttpProxmoxClient implements ProxmoxClient
return $jobId;
}
public function backupNow(string $node, int $vmid, string $storage): string
{
// mode=stop, nicht snapshot: der Abbau faehrt die Maschine vorher
// ohnehin herunter (siehe ArchiveAndTearDown), und eine angehaltene
// Maschine liefert das saubere Abbild ohne jede Einfrier-Frage. Die
// naechtlichen Sicherungen oben (createBackupJob) laufen dagegen
// gegen eine noch laufende Maschine und brauchen deshalb snapshot.
return (string) $this->http()->asForm()
->post("/nodes/{$node}/vzdump", [
'vmid' => $vmid,
'storage' => $storage,
'mode' => 'stop',
'compress' => 'zstd',
])
->throw()->json('data');
}
public function backupsFor(string $node, int $vmid, string $storage): array
{
// content=backup allein liefert die Ablage aller VMs; Proxmox traegt
// die VMID jedes Eintrags im Feld `vmid`, darauf wird gefiltert statt
// dem Dateinamen (`volid`) etwas anzusehen.
$content = $this->http()
->get("/nodes/{$node}/storage/{$storage}/content", ['content' => 'backup'])
->throw()->json('data', []);
return collect($content)
->filter(fn (array $item) => (int) ($item['vmid'] ?? 0) === $vmid)
->values()->all();
}
}

View File

@ -118,4 +118,30 @@ interface ProxmoxClient
/** Create a scheduled vzdump backup job for the VM; returns the job id. */
public function createBackupJob(string $node, int $vmid, string $schedule): string;
/**
* Faehrt einen einzelnen vzdump fuer diese VM und liefert die Auftragskennung
* (UPID) das Gegenstueck zu createBackupJob() fuer einen einmaligen Lauf,
* wie ihn der Abbau einer gekuendigten Instanz braucht.
*
* `$storage` ist bewusst ein Parameter und keine Einstellung an diesem
* Client: welche Ablage fuer ein Archiv gemeint ist, weiss nur der
* Aufrufer (createBackupJob() schreibt heute fest verdrahtet nach
* "local" eine Annahme ueber die Einrichtung der Hosts, keine
* gemessene Tatsache). Ein hier geratener Name wuerde auf einem Host
* ohne diese Ablage erst beim ersten Abbau scheitern.
*/
public function backupNow(string $node, int $vmid, string $storage): string;
/**
* Was auf der Ablage tatsaechlich liegt nicht ob der Auftrag lief.
*
* Der Kern des Abbaus: ein vzdump kann mit einer Auftragskennung enden und
* trotzdem kein Archiv hinterlassen (volle Ablage, abgebrochener Lauf,
* Fehler im Gast). Diese Methode ist die Nachfrage, die vor jedem Loeschen
* gestellt wird, statt der Auftragskennung zu vertrauen.
*
* @return array<int, array<string, mixed>>
*/
public function backupsFor(string $node, int $vmid, string $storage): array;
}

View File

@ -0,0 +1,27 @@
<?php // tests/Feature/Provisioning/OneOffBackupTest.php
use App\Services\Proxmox\FakeProxmoxClient;
it('faehrt einen einzelnen vzdump und liefert die Auftragskennung', function () {
$pve = new FakeProxmoxClient;
$upid = $pve->backupNow('pve', 201, 'local');
expect($upid)->not->toBe('')
->and($pve->backupCalls)->toHaveCount(1)
->and($pve->backupCalls[0])->toMatchArray(['node' => 'pve', 'vmid' => 201, 'storage' => 'local']);
});
it('sagt, was auf der Ablage wirklich liegt', function () {
// Der Kern: nicht "der Auftrag lief", sondern "da liegt eine Datei".
// Ein vzdump, der mit einem Fehler endet, hinterlaesst auch eine
// Auftragskennung — nur eben kein Archiv.
$pve = new FakeProxmoxClient;
$pve->backups['201'] = [['volid' => 'local:backup/vzdump-qemu-201-2026_08_04.vma.zst', 'size' => 4096]];
expect($pve->backupsFor('pve', 201, 'local'))->toHaveCount(1);
});
it('liefert eine leere Liste, wenn nichts da ist', function () {
expect((new FakeProxmoxClient)->backupsFor('pve', 999, 'local'))->toBe([]);
});