From 9d5bf50f9911dbdfcfe24c933b078a89acc5be55 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 9 Aug 2026 10:38:10 +0200 Subject: [PATCH] Neustart sauber statt mit Stromausfall Beim Umstellen der Anzeige habe ich die Maschine mit stop+start neu gestartet - also mit "qm stop", einem virtuellen Stromausfall. Bei einer laufenden Maschine mit eingehaengten Dateisystemen genau das Falsche. Jetzt uebernimmt das Proxmox' eigener reboot-Endpunkt: er faehrt den Gast per ACPI herunter, der haengt seine Dateisysteme selbst aus, danach geht er wieder hoch - und dabei werden vorgemerkte Aenderungen wirksam ("Applies pending changes"). Antwortet der Gast nicht, schlaegt es fehl statt ihm den Strom zu ziehen; das ist die richtige Richtung. Der naheliegende Gedanke, im Gast selbst neu zu starten, hilft hier nicht: dabei setzt sich nur die Maschine zurueck, der QEMU-Prozess laeuft mit der alten Grafikkarte weiter und die Aenderung bliebe vorgemerkt liegen. Steht so auch im Warnhinweis. Gemessen: eintragen, Neustart in 5s, danach vga=qxl und der SPICE-Kanal da. Ausserdem reichte spice_file() das nackte "no spice port" von Proxmox durch. Das heisst schlicht, dass die Maschine ohne qxl laeuft - steht jetzt auch so da, mitsamt dem Weg dorthin. Co-Authored-By: Claude Opus 5 (1M context) --- pvesnap/proxmox.py | 14 ++++++++++++++ pvesnap/recovery.py | 20 ++++++++++++++++---- pvesnap/recovery_ui.py | 22 +++++++++++++++------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/pvesnap/proxmox.py b/pvesnap/proxmox.py index 406153e..2e270fd 100644 --- a/pvesnap/proxmox.py +++ b/pvesnap/proxmox.py @@ -273,6 +273,20 @@ class Proxmox: return self._wait_upid(guest.node, stdout, "Aendern von %s" % guest.label, guest) + def reboot_guest(self, guest, timeout=180): + """Sauber neu starten: Proxmox faehrt den Gast per ACPI herunter und + startet ihn wieder. Dabei werden vorgemerkte Aenderungen wirksam. + + Bewusst ohne Notabschaltung: antwortet der Gast nicht, schlaegt das + hier fehl, statt ihm den Strom zu ziehen. Ein Dateisystem, das gerade + beschrieben wird, nimmt einen Stromausfall uebel - und genau davor + soll das hier schuetzen. + """ + return self._guest_task(guest, "create", "status/reboot", + "Neustart von %s" % guest.label, + ["--timeout", str(int(timeout))], + timeout=timeout + 120) + def resume_guest(self, guest): return self._guest_task(guest, "create", "status/resume", "Fortsetzen von %s" % guest.label) diff --git a/pvesnap/recovery.py b/pvesnap/recovery.py index b0cba1f..81b70a5 100644 --- a/pvesnap/recovery.py +++ b/pvesnap/recovery.py @@ -1209,12 +1209,24 @@ def spice_file(proxmox, instance): die virt-viewer erwartet. Sie enthaelt ein Einmal-Kennwort und laeuft nach kurzer Zeit ab - also frisch erzeugen, wenn sie nicht mehr zieht. """ - data = proxmox._json(["create", "/nodes/%s/%s/%d/spiceproxy" - % (instance.node, instance.type, instance.vmid)]) + try: + data = proxmox._json(["create", "/nodes/%s/%s/%d/spiceproxy" + % (instance.node, instance.type, instance.vmid)]) + except ProxmoxError as exc: + # "no spice port" heisst schlicht: die Maschine laeuft ohne qxl. Das + # nackt durchzureichen hilft niemandem. + if "spice" in str(exc).lower(): + raise RecoveryError( + "%s laeuft ohne SPICE-Anzeige - deshalb gibt es keinen Kanal, " + "auf den sich remote-viewer verbinden koennte.\n" + "Einschalten in der Detailansicht mit p , danach neu starten " + "(die Grafikkarte laesst sich im Betrieb nicht wechseln)." + % instance.label) + raise RecoveryError("SPICE-Verbindung nicht abrufbar: %s" % exc) if not isinstance(data, dict) or not data.get("host"): raise RecoveryError( - "%s liefert keine SPICE-Verbindung. Laeuft die Maschine, und ist " - "die SPICE-Anzeige eingeschaltet (vga: qxl)?" % instance.label) + "%s liefert keine SPICE-Verbindung. Laeuft die Maschine ueberhaupt?" + % instance.label) # Derselbe Grund wie beim noVNC-Verweis: der Proxy-Eintrag traegt den # Hostnamen des Nodes, und wenn der Arbeitsplatz ihn nicht aufloest, diff --git a/pvesnap/recovery_ui.py b/pvesnap/recovery_ui.py index 3d8129a..1e12cac 100644 --- a/pvesnap/recovery_ui.py +++ b/pvesnap/recovery_ui.py @@ -589,12 +589,20 @@ def _display(stdscr, proxmox, instance): # geladene Arbeitsspeicher ist danach weg - Proxmox gibt ihn schon # beim ersten Start frei, ein zweiter startet also kalt. warnung = ["Dafuer muss %s neu starten - die Grafikkarte laesst sich " - "im Betrieb nicht wechseln." % instance.label] + "im Betrieb nicht wechseln." % instance.label, + "", + "Der Neustart laeuft sauber: Proxmox faehrt den Gast per ACPI " + "herunter, er haengt seine Dateisysteme selbst aus, danach " + "geht er wieder hoch. Kein Stromausfall.", + "", + "Ein Neustart von innen im Gast wuerde nichts bringen - dabei " + "setzt sich nur die Maschine zurueck, der QEMU-Prozess laeuft " + "weiter mit der alten Grafikkarte."] if instance.resumed: + warnung.append("") warnung.append("ACHTUNG: der geladene Arbeitsspeicher ist danach " "endgueltig weg. Die Maschine bootet kalt, offene " "Programme und ungespeicherte Daten sind verloren.") - warnung.append("Im Gast vorher sauber herunterfahren, was noch laeuft.") key = _pages(stdscr, [(line, curses.color_pair(C_WARN) | curses.A_BOLD) for eintrag in warnung for line in _wrap(eintrag, 74)], "Neustart noetig", "j = trotzdem | andere Taste = abbrechen") @@ -612,12 +620,12 @@ def _display(stdscr, proxmox, instance): if not geaendert: message(stdscr, "War schon so eingestellt.") return - if neustart and confirm(stdscr, "%s jetzt neu starten?" % instance.label): - _do(stdscr, "Starte %s neu ..." % instance.label, - lambda: (proxmox.stop_guest(instance.guest), - start(proxmox, instance))[1]) + if neustart and confirm(stdscr, "%s jetzt sauber neu starten?" % instance.label): + _do(stdscr, "Fahre %s herunter und wieder hoch ..." % instance.label, + lambda: proxmox.reboot_guest(instance.guest)) elif neustart: - message(stdscr, "Eingetragen - wirksam beim naechsten Start.") + message(stdscr, "Eingetragen - wirksam beim naechsten Start. Im Gast " + "herunterfahren, dann hier mit s starten.") else: message(stdscr, "Uebernommen.")