diff --git a/app/Services/Proxmox/FakeProxmoxClient.php b/app/Services/Proxmox/FakeProxmoxClient.php index 7054676..b874b03 100644 --- a/app/Services/Proxmox/FakeProxmoxClient.php +++ b/app/Services/Proxmox/FakeProxmoxClient.php @@ -345,4 +345,30 @@ class FakeProxmoxClient implements ProxmoxClient return $id; } + + /** @var array 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>> + */ + 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] ?? []; + } } diff --git a/app/Services/Proxmox/HttpProxmoxClient.php b/app/Services/Proxmox/HttpProxmoxClient.php index fa0d952..3541168 100644 --- a/app/Services/Proxmox/HttpProxmoxClient.php +++ b/app/Services/Proxmox/HttpProxmoxClient.php @@ -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(); + } } diff --git a/app/Services/Proxmox/ProxmoxClient.php b/app/Services/Proxmox/ProxmoxClient.php index 7da0a79..09034a6 100644 --- a/app/Services/Proxmox/ProxmoxClient.php +++ b/app/Services/Proxmox/ProxmoxClient.php @@ -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> + */ + public function backupsFor(string $node, int $vmid, string $storage): array; } diff --git a/tests/Feature/Provisioning/OneOffBackupTest.php b/tests/Feature/Provisioning/OneOffBackupTest.php new file mode 100644 index 0000000..28cdfaa --- /dev/null +++ b/tests/Feature/Provisioning/OneOffBackupTest.php @@ -0,0 +1,27 @@ +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([]); +});