diff --git a/pvesnap/proxmox.py b/pvesnap/proxmox.py index 2e270fd..0c60cb8 100644 --- a/pvesnap/proxmox.py +++ b/pvesnap/proxmox.py @@ -247,8 +247,14 @@ class Proxmox: return self._guest_task(guest, "create", "status/start", "Starten von %s" % guest.label) - def shutdown_guest(self, guest, timeout=180, force=True): - """Sauberes Herunterfahren; `force` schaltet nach Ablauf hart ab.""" + def shutdown_guest(self, guest, timeout=180, force=False): + """Sauberes Herunterfahren. + + `force` schaltet nach Ablauf hart ab - das ist ein Stromausfall und + bleibt deshalb ausdruecklich abzufragen. Windows ignoriert den + ACPI-Aus-Knopf gern, wenn eine Anwendung blockiert; dann ist Warten + auf ein Herunterfahren von innen der richtige Weg, nicht Gewalt. + """ extra = ["--timeout", str(int(timeout))] if force: extra += ["--forceStop", "1"] diff --git a/pvesnap/recovery_ui.py b/pvesnap/recovery_ui.py index ecdeba0..42f3a87 100644 --- a/pvesnap/recovery_ui.py +++ b/pvesnap/recovery_ui.py @@ -19,6 +19,7 @@ import curses import logging import os import sys +import time from datetime import datetime from . import __version__ @@ -549,8 +550,7 @@ def _details(stdscr, proxmox, instance): _do(stdscr, "Starte %s ..." % instance.label, lambda: start(proxmox, instance)) elif key in ("h", "H"): - _do(stdscr, "Fahre %s herunter ..." % instance.label, - lambda: proxmox.shutdown_guest(instance.guest)) + _herunterfahren(stdscr, proxmox, instance) elif key in ("e", "E"): _swap(stdscr, proxmox, instance, steckplaetze) elif key in ("v", "V"): @@ -564,6 +564,32 @@ def _details(stdscr, proxmox, instance): return +def _herunterfahren(stdscr, proxmox, instance): + """Sauber herunterfahren - und bei Widerstand nicht einfach zuschlagen.""" + _wait_screen(stdscr, "Fahre %s herunter ..." % instance.label) + try: + proxmox.shutdown_guest(instance.guest) + return + except (ProxmoxError, RecoveryError) as exc: + grund = str(exc) + + weg = choose(stdscr, "%s faehrt nicht herunter" % instance.label, [ + ("warten", "Im Gast selbst herunterfahren - pvesnap wartet"), + ("hart", "Hart ausschalten - wie Stecker ziehen"), + (None, "Abbrechen, laeuft weiter"), + ]) + if weg == "warten": + if _warte_bis_aus(stdscr, proxmox, instance): + message(stdscr, "%s ist aus." % instance.label) + return + if weg == "hart": + if confirm(stdscr, "Wirklich hart ausschalten? Daten koennen verloren gehen."): + _do(stdscr, "Schalte %s aus ..." % instance.label, + lambda: proxmox.stop_guest(instance.guest)) + return + message(stdscr, grund, error=True) + + def _do(stdscr, text, action): _wait_screen(stdscr, text) try: @@ -575,6 +601,81 @@ def _do(stdscr, text, action): message(stdscr, hint, error=True) +def _warte_bis_aus(stdscr, proxmox, instance, minuten=30): + """Warten, bis der Gast von innen heruntergefahren ist. + + Der verlaesslichere Weg als ACPI: Windows ignoriert den Aus-Knopf gern, + wenn eine Anwendung den Herunterfahrvorgang blockiert oder der Shutdown + Event Tracker nach einem Grund fragt. Wer im Gast selbst herunterfaehrt, + umgeht das - und pvesnap sieht am Zustand, wann es soweit ist. + """ + curses.halfdelay(20) # get_wch wartet hoechstens 2s + begonnen = time.time() + try: + while time.time() - begonnen < minuten * 60: + try: + zustand = proxmox.guest_status(instance.guest).get("status") or "?" + except ProxmoxError: + zustand = "?" + if zustand != "running": + return True + + height, width = stdscr.getmaxyx() + stdscr.erase() + fill(stdscr, 0, " Warte auf das Herunterfahren von %s" % instance.label, + curses.color_pair(C_HEADER) | curses.A_BOLD) + zeilen = [ + "Bitte jetzt IM GAST herunterfahren.", + "", + " Windows Start -> Ein/Aus -> Herunterfahren", + " Linux poweroff bzw. shutdown -h now", + "", + "Sobald die Maschine aus ist, startet pvesnap sie von selbst", + "wieder - dann mit der neuen Ausstattung.", + "", + "Zustand: %s seit %d:%02d" + % (zustand, int(time.time() - begonnen) // 60, + int(time.time() - begonnen) % 60), + ] + for zeile, text in enumerate(zeilen): + put(stdscr, zeile + 2, 3, truncate(text, width - 6), + curses.A_BOLD if zeile == 0 else 0) + fill(stdscr, height - 1, " q oder Esc = Abbrechen (die Maschine " + "laeuft dann einfach weiter)", curses.color_pair(C_FOOTER)) + stdscr.refresh() + + key = read_key(stdscr) + if key in ("q", "Q") or is_escape(key): + return False + finally: + curses.cbreak() # halbe Verzoegerung zuruecknehmen + return False + + +def _neu_starten(stdscr, proxmox, instance): + """Neustart, damit vorgemerkte Aenderungen wirksam werden.""" + weg = choose(stdscr, "Wie soll %s neu starten?" % instance.label, [ + ("warten", "Ich fahre im Gast herunter - pvesnap wartet und startet dann"), + ("acpi", "Proxmox herunterfahren lassen (ACPI) - Windows blockt das oft"), + (None, "Gar nicht - spaeter selbst"), + ]) + if weg is None: + message(stdscr, "Eingetragen - wirksam beim naechsten Start.") + return + + if weg == "acpi": + _do(stdscr, "Fahre %s herunter und wieder hoch ..." % instance.label, + lambda: proxmox.reboot_guest(instance.guest)) + return + + if not _warte_bis_aus(stdscr, proxmox, instance): + message(stdscr, "Abgebrochen - die Aenderung greift beim naechsten " + "Start. Dann hier mit s starten.") + return + _do(stdscr, "Ist aus - starte %s wieder ..." % instance.label, + lambda: start(proxmox, instance)) + + def _display(stdscr, proxmox, instance): """SPICE-Anzeige und USB-Weiterleitung nachtraeglich einstellen.""" if instance.type != "qemu": @@ -604,9 +705,10 @@ def _display(stdscr, proxmox, instance): warnung = ["Dafuer muss %s neu starten - die Grafikkarte laesst sich " "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.", + "Gleich kannst du waehlen, wie: entweder du faehrst im Gast " + "selbst herunter und pvesnap startet ihn danach wieder, oder " + "Proxmox versucht es per ACPI. Der erste Weg ist der " + "verlaesslichere - Windows ignoriert den Aus-Knopf gern.", "", "Ein Neustart von innen im Gast wuerde nichts bringen - dabei " "setzt sich nur die Maschine zurueck, der QEMU-Prozess laeuft " @@ -633,12 +735,8 @@ def _display(stdscr, proxmox, instance): if not geaendert: message(stdscr, "War schon so eingestellt.") return - 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. Im Gast " - "herunterfahren, dann hier mit s starten.") + if neustart: + _neu_starten(stdscr, proxmox, instance) else: message(stdscr, "Uebernommen.") @@ -796,8 +894,7 @@ def _overview(stdscr, proxmox): lambda: start(proxmox, current)) reload_ = True elif key in ("h", "H"): - _do(stdscr, "Fahre %s herunter ..." % current.label, - lambda: proxmox.shutdown_guest(current.guest)) + _herunterfahren(stdscr, proxmox, current) reload_ = True elif key in ("x", "X"): _destroy(stdscr, proxmox, current)