455 lines
20 KiB
PHP
455 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Actions;
|
|
|
|
use App\Models\Instance;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Sleep;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Die Maschine einer gekündigten Instanz archivieren und dann abbauen.
|
|
*
|
|
* Gekündigte Kundenmaschinen wurden bisher nie abgebaut. `EndInstanceService`
|
|
* nimmt der Instanz am Laufzeitende die Adresse weg — Router, DNS-Eintrag,
|
|
* Zertifikatsflaggen — und lässt die virtuelle Maschine ausdrücklich stehen,
|
|
* weil das Zerstören fremder Platten keine Entscheidung ist, die ein
|
|
* Adress-Abbau nebenbei trifft. Danach kam nichts mehr: die Maschine lief für
|
|
* immer weiter und belegte einen Platz auf dem Host, den niemand mehr verkaufen
|
|
* kann. Dies ist der Schritt danach.
|
|
*
|
|
* ## Die eine Regel
|
|
*
|
|
* **Niemals löschen, bevor das Archiv nachweislich existiert.** Ein `vzdump`
|
|
* kann mit einer Auftragskennung enden und trotzdem nichts hinterlassen — volle
|
|
* Ablage, abgebrochener Lauf, ein Fehler im Gast. Wer der Kennung glaubt statt
|
|
* nachzusehen, löscht die Maschine eines Kunden, dem zwölf Monate Aufbewahrung
|
|
* zugesagt sind, und hat kein Archiv. Das ist durch keinen Wiederholungslauf zu
|
|
* heilen. Deshalb steht zwischen dem Sichern und dem Löschen eine echte
|
|
* Nachfrage auf der Ablage (`backupsFor()`), und jeder Riegel davor lässt den
|
|
* Lauf lieber stehen, als eine Stufe zu überspringen.
|
|
*
|
|
* ## Die Reihenfolge, und warum jede Stufe da steht, wo sie steht
|
|
*
|
|
* 1. **Ablage klären**, bevor irgendetwas angefasst wird — siehe unten.
|
|
* 2. **Herunterfahren** und darauf warten. Eine laufende Datenbank zu sichern
|
|
* und danach zu löschen hiesse, ein Archiv mit halb geschriebenen Tabellen
|
|
* aufzubewahren. Es wird nie hart ausgeschaltet: `ProxmoxClient` bietet den
|
|
* Stromstecker bewusst nicht an (siehe dort), also ist die Frist eine
|
|
* Grenze fürs WARTEN und keine Zündschnur. Ein Gast, der nicht geht, ist ein
|
|
* Fall für einen Menschen.
|
|
* 3. **Sichern** über `backupNow()` und auf den Auftrag warten. Der Aufruf
|
|
* liefert nur eine Kennung; der Dump läuft danach noch minutenlang. Ohne
|
|
* dieses Warten fragte die Nachschau in Stufe 4 die Ablage, bevor das
|
|
* Archiv darauf liegt — der Abbau schlüge jedes Mal fehl und liesse bei
|
|
* jedem Lauf einen weiteren Dump auf der Ablage zurück.
|
|
* 4. **Nachsehen.** Liegt nichts da, wird nicht gelöscht.
|
|
* 5. **Löschen**, und erst dann `torn_down_at` schreiben.
|
|
*
|
|
* `archive_volid`/`archived_at` werden VOR dem Löschen geschrieben, nicht
|
|
* danach: das Archiv existiert ab Stufe 4 wirklich, und ein Absturz zwischen
|
|
* vier und fünf soll den bereits bezahlten Dump nicht vergessen. Genau dafür
|
|
* sind es zwei Zeitstempel (siehe die Wanderung der Spalten in der Migration).
|
|
*
|
|
* ## Die Ablage
|
|
*
|
|
* Der Name kommt aus der Einstellung `provisioning.proxmox.archive_storage`
|
|
* (Vorgabe `local`, wie sie die nächtlichen Sicherungen von `RegisterBackup`
|
|
* schon benutzen) UND wird gegen die Ablagen des Knotens geprüft: sie muss es
|
|
* geben, und sie muss `backup` tragen. Beides zusammen, weil beide Hälften
|
|
* allein nicht reichen — eine feste Zeichenkette wäre eine Annahme über die
|
|
* Einrichtung jedes Hosts, die erst beim ersten Abbau auffällt, und eine
|
|
* ungeprüfte Einstellung wäre dieselbe Annahme mit mehr Schritten. Fehlt die
|
|
* Ablage, bricht der Lauf VOR dem Herunterfahren ab, sagt im Klartext, welche
|
|
* Ablagen der Knoten stattdessen hat, und löscht nichts. Es wird ausdrücklich
|
|
* nicht geraten: eine Maschine auf eine Ablage zu sichern, die der Betreiber
|
|
* nicht gemeint hat, ist ein Archiv, das in zwölf Monaten niemand sucht.
|
|
*
|
|
* ## Was hier ausdrücklich NICHT passiert
|
|
*
|
|
* Nichts wirft. Dieser Ablauf läuft später in einem Zeitplan über viele
|
|
* Instanzen; eine geworfene Ausnahme risse die übrigen mit. Ein Fehlschlag ist
|
|
* `false` und ein Grund in `teardown_error` — dort, wo ein Betreiber ihn ohne
|
|
* Logdurchsicht findet.
|
|
*
|
|
* Das Archiv selbst wird nicht verwaltet. Die zwölf Monate Aufbewahrung und das
|
|
* Löschen des Dumps danach hängen an `archived_at` und gehören einem eigenen
|
|
* Lauf; hier wird nur festgehalten, wo das Archiv liegt.
|
|
*
|
|
* Adresse, DNS und Router werden nicht angefasst — das hat `EndInstanceService`
|
|
* beim Laufzeitende getan, und eine zweite Stelle, die dasselbe täte, würde
|
|
* beim ersten Auseinanderlaufen zur Frage, welche recht hat.
|
|
*/
|
|
class ArchiveAndTearDown
|
|
{
|
|
/**
|
|
* Wie lange eine gekündigte Instanz nach dem Laufzeitende stehen bleibt.
|
|
*
|
|
* Vierzehn Tage sind die Rückfahrkarte. Ein Mensch, dessen Paket gestern
|
|
* ablief, überlegt es sich vielleicht gerade anders, und bis hierher ist
|
|
* seine Maschine unverändert da. Ab hier ist sie es nicht mehr.
|
|
*/
|
|
public const GRACE_DAYS = 14;
|
|
|
|
/**
|
|
* Wie weit die Uhr des Hosts vorgehen darf, ohne dass ein frisches Archiv
|
|
* als altes gilt.
|
|
*
|
|
* `ctime` kommt von der Uhr des Proxmox-Knotens, die Startzeit dieses Laufs
|
|
* von der des Portals. Ohne Toleranz macht eine Sekunde Versatz aus dem
|
|
* gerade eben geschriebenen Dump ein „Archiv von gestern" und der Abbau
|
|
* bliebe für immer stehen. Fünf Minuten sind gross genug für jeden
|
|
* NTP-Versatz und klein genug, dass eine nächtliche Sicherung von gestern
|
|
* niemals hineinfällt.
|
|
*/
|
|
private const CLOCK_SKEW_SECONDS = 300;
|
|
|
|
/**
|
|
* Wie lange auf das Herunterfahren des Gastes gewartet wird.
|
|
*
|
|
* Zehn Minuten, dieselbe Frist wie in ShutDownVirtualMachine und aus
|
|
* demselben Grund: eine Nextcloud, die sauber stoppt, ist in Sekunden bis
|
|
* wenigen Minuten fertig; was danach noch läuft, tut etwas.
|
|
*/
|
|
public int $shutdownWaitSeconds = 600;
|
|
|
|
/**
|
|
* Wie lange auf den vzdump gewartet wird.
|
|
*
|
|
* Eine Stunde. Eine Kundenplatte von einigen hundert Gigabyte mit zstd
|
|
* gepackt liegt darunter; was länger braucht, hat ein Problem, das ein
|
|
* Betreiber ansehen soll. Öffentlich, damit ein Aufrufer (und eine Prüfung)
|
|
* die Grenze setzen kann, ohne die Uhr anzuhalten.
|
|
*/
|
|
public int $backupWaitSeconds = 3600;
|
|
|
|
/** Abstand zwischen zwei Nachfragen nach einem laufenden Auftrag. */
|
|
public int $pollSeconds = 15;
|
|
|
|
public function __construct(private ProxmoxClient $pve) {}
|
|
|
|
/**
|
|
* Ist diese Instanz wirklich fällig?
|
|
*
|
|
* Die Regel steht hier und nicht in der Abfrage des Zeitplans, damit die
|
|
* Auswahl und der Abbau dieselbe Frage stellen — zwei Fassungen von „ist
|
|
* es soweit" sind der Weg, auf dem die eine irgendwann einen Tag früher
|
|
* antwortet als die andere.
|
|
*
|
|
* `status === 'ended'` und nicht `cancellation_scheduled`: erst
|
|
* `EndInstanceService` setzt diesen Zustand, und er setzt ihn genau dann,
|
|
* wenn die Adresse schon weg ist. Eine Instanz ohne `service_ends_at` ist
|
|
* ein kaputter und kein abgelaufener Datensatz — ein geratenes Enddatum
|
|
* wäre genau der Zeitpunkt, den diese Frist schützen soll.
|
|
*/
|
|
public function isDue(Instance $instance, ?Carbon $at = null): bool
|
|
{
|
|
return $instance->status === 'ended'
|
|
&& $instance->torn_down_at === null
|
|
&& $instance->service_ends_at !== null
|
|
&& $instance->service_ends_at->lessThan(($at ?? now())->copy()->subDays(self::GRACE_DAYS));
|
|
}
|
|
|
|
/**
|
|
* Archivieren und abbauen, oder mit einem Grund stehen bleiben.
|
|
*
|
|
* @return bool ob diese Instanz jetzt abgebaut ist (auch dann, wenn ein
|
|
* früherer Lauf sie abgebaut hat)
|
|
*/
|
|
public function __invoke(Instance $instance): bool
|
|
{
|
|
// Wiederholbarkeit vor allem anderen: ein zweiter Lauf nach einem
|
|
// Absturz darf nicht versuchen, eine Maschine zu löschen, die es nicht
|
|
// mehr gibt.
|
|
if ($instance->torn_down_at !== null) {
|
|
return true;
|
|
}
|
|
|
|
if (! $this->isDue($instance)) {
|
|
return false;
|
|
}
|
|
|
|
// Zwei Läufe über dieselbe Instanz — 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. Niemand
|
|
// wartet auf die Sperre: wer sie nicht bekommt, hat nichts
|
|
// hinzuzufügen, weil der Lauf, der sie hält, dasselbe tut. Die Frist
|
|
// deckt den ganzen Dump ab, sonst liefe sie mitten im Sichern aus.
|
|
$lock = Cache::lock('instance-teardown:'.$instance->uuid, $this->backupWaitSeconds + 600);
|
|
|
|
if (! $lock->get()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return $this->tearDown($instance);
|
|
} catch (Throwable $e) {
|
|
// Keine Ausnahme entkommt: dieser Ablauf läuft im Zeitplan über
|
|
// viele Instanzen, und ein Wurf hier nähme die übrigen mit.
|
|
$this->recordFailure($instance, $e);
|
|
|
|
return false;
|
|
} finally {
|
|
$lock->release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Der eigentliche Ablauf. Jeder Riegel wirft, der Aufrufer oben fängt und
|
|
* schreibt den Grund an den Datensatz — ein Wurf ist hier Ablaufsteuerung
|
|
* und verlässt die Aktion nie.
|
|
*/
|
|
private function tearDown(Instance $instance): bool
|
|
{
|
|
$host = $instance->host;
|
|
$vmid = $instance->vmid;
|
|
|
|
if ($host === null || $vmid === null) {
|
|
throw new RuntimeException(
|
|
'Zu dieser Instanz gehört keine erreichbare Maschine mehr (Host oder VMID fehlt). '
|
|
.'Es wird nichts gesichert und nichts gelöscht.'
|
|
);
|
|
}
|
|
|
|
$node = (string) ($host->node ?? 'pve');
|
|
$pve = $this->pve->forHost($host);
|
|
$vmid = (int) $vmid;
|
|
|
|
// Der Weg zurück aus einem Absturz zwischen Löschen und Buchen: die
|
|
// Maschine ist fort, das Archiv steht aber schon am Datensatz. Ohne
|
|
// diesen Zweig versuchte jeder weitere Lauf, einen Geist
|
|
// herunterzufahren, und die Instanz bliebe für immer im Fehler stehen.
|
|
// Bewusst an `archive_volid` gebunden und nicht an `vmExists()` allein:
|
|
// die Frage beantwortet auch ein hakendes Proxmox mit „nein", und ohne
|
|
// ein verbrieftes Archiv wäre das „schon weg" eine Maschine, die
|
|
// niemand mehr anfasst.
|
|
if ($instance->archive_volid !== null && ! $pve->vmExists($node, $vmid)) {
|
|
$instance->update(['torn_down_at' => now(), 'teardown_error' => null]);
|
|
|
|
Log::warning('Abbau: die Maschine war schon fort, das Archiv stand bereits fest.', [
|
|
'instance' => $instance->uuid,
|
|
'vmid' => $vmid,
|
|
'archive_volid' => $instance->archive_volid,
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Vor dem Herunterfahren: eine Instanz, deren Archiv nirgends hin
|
|
// kann, wird gar nicht erst angefasst.
|
|
$storage = $this->archiveStorage($pve, $node);
|
|
|
|
$shutdownUpid = $pve->shutdownVm($node, $vmid, $this->shutdownWaitSeconds);
|
|
$this->awaitTask($pve, $node, $shutdownUpid, $this->shutdownWaitSeconds, 'Das Herunterfahren');
|
|
|
|
// Die Auftragskennung ist keine Zusage. Ein Gast, der ACPI ignoriert,
|
|
// lässt den Auftrag auslaufen und läuft weiter — gesichert wird eine
|
|
// laufende Datenbank hier nicht, und gelöscht schon gar nicht.
|
|
if (($pve->vmStatus($node, $vmid)['status'] ?? '') === 'running') {
|
|
throw new RuntimeException(
|
|
'Der Gast läuft nach der Bitte zum Herunterfahren weiter. Es wird nichts gesichert '
|
|
.'und nichts gelöscht; die Maschine wird bewusst nicht hart ausgeschaltet.'
|
|
);
|
|
}
|
|
|
|
// Vor dem Dump abgelesen, nicht danach: die Zeit ist die Grenze, an der
|
|
// sich später „mein Archiv" von „das von gestern Nacht" unterscheidet.
|
|
$begonnen = now();
|
|
|
|
$backupUpid = $pve->backupNow($node, $vmid, $storage);
|
|
$this->awaitTask($pve, $node, $backupUpid, $this->backupWaitSeconds, 'Die Sicherung');
|
|
|
|
$volid = $this->verifiedArchive($pve, $node, $vmid, $storage, $begonnen);
|
|
|
|
// Das Archiv ist eine Tatsache, sobald es daliegt — festgehalten, bevor
|
|
// gelöscht wird.
|
|
$instance->update(['archive_volid' => $volid, 'archived_at' => now()]);
|
|
|
|
// Zwischen Sichern und Löschen liegen Minuten. Was in dieser Zeit an der
|
|
// Instanz passiert ist, entscheidet: eine wiederbelebte Instanz
|
|
// (`status` nicht mehr `ended`) ist keine, die gelöscht werden darf.
|
|
// Der Sperre ist dabei nicht zu trauen — sie hält nur andere LÄUFE ab,
|
|
// nicht einen Menschen in der Konsole.
|
|
$frisch = $instance->fresh();
|
|
|
|
if ($frisch === null || $frisch->torn_down_at !== null || $frisch->status !== 'ended') {
|
|
throw new RuntimeException(
|
|
'Die Instanz hat sich während der Sicherung geändert (Zustand "'
|
|
.($frisch?->status ?? 'gelöscht').'"). Das Archiv liegt und ist vermerkt, '
|
|
.'gelöscht wird die Maschine nicht.'
|
|
);
|
|
}
|
|
|
|
$pve->deleteVm($node, $vmid);
|
|
|
|
$instance->update(['torn_down_at' => now(), 'teardown_error' => null]);
|
|
|
|
Log::info('Gekündigte Instanz archiviert und abgebaut.', [
|
|
'instance' => $instance->uuid,
|
|
'vmid' => $vmid,
|
|
'node' => $node,
|
|
'archive_volid' => $volid,
|
|
'service_ended_at' => $instance->service_ends_at?->toIso8601String(),
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Welche Ablage das Archiv trägt — als Name aus der Einstellung und als
|
|
* geprüfte Tatsache auf dem Knoten.
|
|
*/
|
|
private function archiveStorage(ProxmoxClient $pve, string $node): string
|
|
{
|
|
$name = (string) config('provisioning.proxmox.archive_storage', 'local');
|
|
|
|
$vorhanden = collect($pve->nodeStorage($node));
|
|
$treffer = $vorhanden->firstWhere('storage', $name);
|
|
|
|
if ($treffer === null) {
|
|
throw new RuntimeException(
|
|
'Die Ablage "'.$name.'" gibt es auf dem Knoten '.$node.' nicht (vorhanden: '
|
|
.($vorhanden->pluck('storage')->filter()->implode(', ') ?: 'keine')
|
|
.'). Ohne Ziel für das Archiv wird nichts gelöscht.'
|
|
);
|
|
}
|
|
|
|
// `content` zählt auf, was eine Ablage überhaupt aufnehmen darf. Eine
|
|
// Ablage ohne `backup` nimmt den Dump nicht an — das fiele sonst erst
|
|
// beim vzdump auf, mit einer heruntergefahrenen Kundenmaschine daneben.
|
|
if (! in_array('backup', explode(',', (string) ($treffer['content'] ?? '')), true)) {
|
|
throw new RuntimeException(
|
|
'Die Ablage "'.$name.'" auf dem Knoten '.$node.' nimmt keine Sicherungen auf (content: "'
|
|
.((string) ($treffer['content'] ?? '')).'"). Es wird nichts gelöscht.'
|
|
);
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Auf einen Proxmox-Auftrag warten, bis er wirklich fertig ist.
|
|
*
|
|
* Sowohl `shutdownVm()` als auch `backupNow()` liefern nur eine Kennung; die
|
|
* Arbeit läuft danach. Ein Aufrufer, der sofort weiterliest, sieht den
|
|
* Zustand von vorher — beim Herunterfahren eine laufende Maschine, beim
|
|
* Sichern eine Ablage ohne das Archiv, das gerade geschrieben wird.
|
|
*/
|
|
private function awaitTask(ProxmoxClient $pve, string $node, string $upid, int $waitSeconds, string $was): void
|
|
{
|
|
$frist = now()->addSeconds($waitSeconds);
|
|
|
|
while (true) {
|
|
$status = $pve->taskStatus($node, $upid);
|
|
|
|
if (($status['status'] ?? '') !== 'running') {
|
|
$ende = (string) ($status['exitstatus'] ?? 'OK');
|
|
|
|
// Proxmox beendet einen vzdump auch dann mit "WARNINGS: n", wenn
|
|
// das Archiv sauber geschrieben wurde (eine übersprungene Platte,
|
|
// ein Hinweis aus dem Gast). Das als Fehlschlag zu lesen liesse
|
|
// den Abbau an einer Meldung hängen, die kein Archiv verhindert —
|
|
// und der Beweis, auf den es ankommt, ist ohnehin die Nachschau
|
|
// auf der Ablage, nicht diese Zeichenkette.
|
|
if ($ende !== 'OK' && ! str_starts_with($ende, 'WARNINGS')) {
|
|
throw new RuntimeException($was.' endete mit "'.$ende.'". Es wird nichts gelöscht.');
|
|
}
|
|
|
|
if ($ende !== 'OK') {
|
|
Log::warning('Abbau: Proxmox-Auftrag mit Hinweisen beendet.', [
|
|
'node' => $node,
|
|
'upid' => $upid,
|
|
'exitstatus' => $ende,
|
|
]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (now()->greaterThanOrEqualTo($frist)) {
|
|
throw new RuntimeException(
|
|
$was.' lief nach '.$waitSeconds.' Sekunden noch. Es wird nichts gelöscht; '
|
|
.'der nächste Lauf sieht erneut nach.'
|
|
);
|
|
}
|
|
|
|
Sleep::for($this->pollSeconds)->seconds();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Nachsehen, was wirklich auf der Ablage liegt — der Riegel, an dem alles
|
|
* hängt.
|
|
*
|
|
* @return string die Kennung des Archivs (volid)
|
|
*/
|
|
private function verifiedArchive(ProxmoxClient $pve, string $node, int $vmid, string $storage, Carbon $begonnen): string
|
|
{
|
|
$eintraege = $pve->backupsFor($node, $vmid, $storage);
|
|
|
|
if ($eintraege === []) {
|
|
throw new RuntimeException(
|
|
'Nach der Sicherung liegt auf der Ablage "'.$storage.'" kein Archiv für VM '.$vmid.'. '
|
|
.'Die Maschine bleibt stehen und wird nicht gelöscht.'
|
|
);
|
|
}
|
|
|
|
/** @var array<string, mixed> $juengstes */
|
|
$juengstes = collect($eintraege)->sortBy(fn (array $e) => (int) ($e['ctime'] ?? 0))->last();
|
|
|
|
$ctime = $juengstes['ctime'] ?? null;
|
|
|
|
if ($ctime === null) {
|
|
// Eine echte Ablage nennt zu jedem Eintrag ein `ctime`; fehlt es,
|
|
// ist das Alter des Archivs schlicht nicht feststellbar. Dann wird
|
|
// es angenommen statt abgelehnt — es LIEGT ein Archiv dieser VM da,
|
|
// und der Fall, den diese Aktion zu verhindern hat, ist das Löschen
|
|
// ohne jedes Archiv. Laut genug, dass es auffällt, wenn es je öfter
|
|
// vorkommt.
|
|
Log::warning('Abbau: das Archiv nennt keinen Zeitpunkt, sein Alter ist ungeprüft.', [
|
|
'vmid' => $vmid,
|
|
'storage' => $storage,
|
|
]);
|
|
} elseif ((int) $ctime < $begonnen->getTimestamp() - self::CLOCK_SKEW_SECONDS) {
|
|
// Der Fall, den die reine Ja/Nein-Frage „liegt da etwas?" durchlässt:
|
|
// die nächtliche Sicherung von gestern. Gegen sie zu löschen hiesse,
|
|
// ein Archiv aufzubewahren, dem der letzte Tag fehlt — und niemand
|
|
// würde es je merken.
|
|
throw new RuntimeException(
|
|
'Das jüngste Archiv für VM '.$vmid.' auf "'.$storage.'" stammt vom '
|
|
.Carbon::createFromTimestamp((int) $ctime)->toIso8601String()
|
|
.' und damit von vor diesem Lauf. Die Sicherung dieses Laufs ist nicht auffindbar; '
|
|
.'es wird nichts gelöscht.'
|
|
);
|
|
}
|
|
|
|
$volid = (string) ($juengstes['volid'] ?? '');
|
|
|
|
if ($volid === '') {
|
|
throw new RuntimeException(
|
|
'Das Archiv für VM '.$vmid.' auf "'.$storage.'" hat keine Kennung (volid). '
|
|
.'Ein Archiv, dessen Ort nicht notiert werden kann, ist in zwölf Monaten keins — '
|
|
.'es wird nichts gelöscht.'
|
|
);
|
|
}
|
|
|
|
return $volid;
|
|
}
|
|
|
|
/** Der Grund gehört an den Datensatz, wo ein Betreiber ihn ohne Logdurchsicht findet. */
|
|
private function recordFailure(Instance $instance, Throwable $e): void
|
|
{
|
|
$instance->update(['teardown_error' => $e->getMessage()]);
|
|
|
|
Log::error('Abbau einer gekündigten Instanz gestoppt.', [
|
|
'instance' => $instance->uuid,
|
|
'vmid' => $instance->vmid,
|
|
'reason' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|