Files
proxmox-snapshot-service/pvesnap/recovery_ui.py
T
duffyduckandClaude Opus 5 fb3be26c33 Neustart: auf das Herunterfahren im Gast warten statt es zu erzwingen
ACPI klappt bei Linux, bei Windows haeufig nicht - dort blockiert gern eine
Anwendung den Vorgang oder der Shutdown Event Tracker fragt nach einem Grund.
Deshalb gibt es jetzt die Wahl:

    Ich fahre im Gast herunter - pvesnap wartet und startet dann
    Proxmox herunterfahren lassen (ACPI) - Windows blockt das oft
    Gar nicht - spaeter selbst

Der erste Weg ist der verlaessliche: pvesnap zeigt, was im Gast zu tun ist,
verfolgt den Zustand und startet die Maschine von selbst wieder, sobald sie
aus ist. Abbrechen geht jederzeit, dann laeuft sie einfach weiter und die
Aenderung greift beim naechsten Start.

Dieselbe Falle steckte in der Taste h: shutdown_guest lief mit force=True und
hat den Gast nach 180 Sekunden hart abgeschaltet - ein Stromausfall, still und
ohne Rueckfrage. Die Vorgabe ist jetzt force=False, und bei Widerstand kommt
dieselbe Wahl: warten, hart ausschalten (mit ausdruecklicher Bestaetigung) oder
abbrechen.

Geprueft: der Wartebildschirm erkennt das Herunterfahren und meldet es zurueck,
der Auswahldialog zeigt alle drei Wege.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:01:47 +02:00

1369 lines
56 KiB
Python

"""pvesnap-recovery - einen Snapshot als Maschine starten.
Ohne Argumente kommt die ncurses-Oberflaeche, alles geht aber auch direkt:
pvesnap-recovery Uebersicht und Assistent
pvesnap-recovery list was gerade laeuft
pvesnap-recovery live 802 neuesten Snapshot ohne Netz starten
pvesnap-recovery live 802 <snapshot> --transfer dumps
pvesnap-recovery recover 802 <snapshot> mit Netz, gleiche Identitaet
pvesnap-recovery console 9802 noVNC-Adresse anzeigen
pvesnap-recovery Taste v: Transfer-Laufwerke
pvesnap-recovery destroy 9802 restlos verwerfen
"""
from __future__ import annotations
import argparse
import curses
import logging
import os
import sys
import time
from datetime import datetime
from . import __version__
from .curses_util import (C_DIM, C_FOOTER, C_HEADER, C_MARK, C_OK, C_SEL, C_WARN,
choose, confirm, fill, init_colors, is_enter, is_escape,
keybar, message, prompt, put, read_key)
from .proxmox import Proxmox, ProxmoxError, pvesh_available
from . import transfer, transfer_ui
from .recovery import (RecoveryError, Spec, create, destroy, eject_transfer,
find_orphans, flatten, flatten_cost, flatten_hint,
human_bytes, insert_transfer, linked_volumes,
display_state, list_instances, node_address, plan,
remove_orphans, set_display,
spice_file, start, transfer_state)
from .snapfs import list_snapshots
from .util import truncate
log = logging.getLogger("pvesnap.recovery_ui")
KEYS = [
("Enter", "Details", "Detail"),
("n", "Neu", "Neu"),
("s", "Starten", "Start"),
("h", "Herunterfahren", "Stop"),
("x", "Verwerfen", "Verwerf."),
("v", "Laufwerke", "Laufw."),
("r", "Aktualisieren", "Aktual."),
("q", "Beenden", "Ende"),
]
NET_CHOICES = (
("", "automatisch (nach Modus)"),
("none", "keine Netzwerkkarte"),
("down", "Karte vorhanden, abgeklemmt"),
("on", "voll am Netz - Vorsicht!"),
)
RESUME_CHOICES = (
(None, "wenn im Snapshot vorhanden"),
(True, "ja, Arbeitsspeicher laden"),
(False, "nein, kalt starten"),
)
# ---------------------------------------------------------------------------
# Kleine Bausteine
# ---------------------------------------------------------------------------
def _wait_screen(win, text, title="pvesnap-recovery"):
height, width = win.getmaxyx()
win.erase()
fill(win, 0, " " + title, curses.color_pair(C_HEADER) | curses.A_BOLD)
put(win, height // 2, max(2, (width - len(text)) // 2), text, curses.A_BOLD)
win.refresh()
def _pages(win, lines, title, footer="beliebige Taste = weiter"):
"""Einen Textblock anzeigen, bei Bedarf seitenweise."""
height, width = win.getmaxyx()
rows = max(1, height - 4)
top = 0
while True:
win.erase()
fill(win, 0, " " + title, curses.color_pair(C_HEADER) | curses.A_BOLD)
for row, (text, attr) in enumerate(lines[top:top + rows]):
put(win, row + 2, 2, truncate(text, width - 4), attr)
more = " (%d-%d von %d)" % (top + 1, min(top + rows, len(lines)), len(lines)) \
if len(lines) > rows else ""
# Eine Liste wird wie ueberall sonst auf die Breite umgebrochen; ein
# fester Text wuerde auf schmalen Terminals hinten abgeschnitten.
zeilen = keybar(width, footer) if isinstance(footer, list) else [footer]
if more:
# Die Seitenangabe gehoert ans Ende, egal welche Form die
# Fusszeile hat - sonst weiss man bei langen Inhalten nicht,
# dass es weitergeht.
zeilen[-1] = zeilen[-1] + more
for offset, zeile in enumerate(zeilen[-2:]):
fill(win, height - len(zeilen[-2:]) + offset, " " + zeile,
curses.color_pair(C_FOOTER))
win.refresh()
key = read_key(win)
if key == curses.KEY_DOWN and top + rows < len(lines):
top += 1
elif key == curses.KEY_UP and top > 0:
top -= 1
elif key == curses.KEY_NPAGE and top + rows < len(lines):
top = min(len(lines) - rows, top + rows)
elif key == curses.KEY_PPAGE:
top = max(0, top - rows)
else:
return key
def _plain(lines):
return [(text, 0) for text in lines]
class _Progress:
"""Fortschrittsanzeige waehrend des Anlegens."""
def __init__(self, win, title):
self.win = win
self.title = title
self.lines = []
def __call__(self, text):
self.lines.append(text)
height, width = self.win.getmaxyx()
self.win.erase()
fill(self.win, 0, " " + self.title, curses.color_pair(C_HEADER) | curses.A_BOLD)
visible = self.lines[-(height - 4):]
for row, line in enumerate(visible):
attr = curses.A_BOLD if line is visible[-1] else curses.color_pair(C_DIM)
put(self.win, row + 2, 2, truncate(line, width - 4), attr)
fill(self.win, height - 1, " bitte warten ...", curses.color_pair(C_FOOTER))
self.win.refresh()
# ---------------------------------------------------------------------------
# Auswahl
# ---------------------------------------------------------------------------
def _pick_guest(win, guests):
entries = [(g, "%s %-6d %-24s %-8s %s"
% ("LXC" if g.type == "lxc" else "VM ", g.vmid,
truncate(g.name or "-", 24), g.status, g.node))
for g in guests]
return choose(win, "Von welchem Gast? (Esc = zurueck)", entries)
def _pick_snapshot(win, snapshots):
entries = []
for snap in snapshots:
when = (datetime.fromtimestamp(snap.snaptime).strftime("%d.%m.%Y %H:%M")
if snap.snaptime else "?")
ram = "RAM" if "vmstate" in (snap.description or "") else ""
entries.append((snap.name, "%-34s %s %-4s %s"
% (truncate(snap.name, 34), when, ram,
truncate((snap.description or "").replace("\n", " "), 34))))
return choose(win, "Welcher Snapshot? (Esc = zurueck)", entries)
# ---------------------------------------------------------------------------
# Optionsmaske
# ---------------------------------------------------------------------------
def _fields(guest):
"""Die Eingabefelder - je nach Gasttyp etwas andere."""
rows = [
("mode", "Betriebsart", "choice",
(("live", "live - abgeschottet, ohne Netzwerk"),
("recovery", "recovery - mit Netz, gleiche Identitaet"))),
("net", "Netzwerk", "choice", NET_CHOICES),
]
if guest.type == "qemu":
rows.append(("resume", "Arbeitsspeicher", "choice", RESUME_CHOICES))
rows.append(("transfers", "Transfer-Laufwerke", "transfers", ()))
if guest.type == "qemu":
rows.append(("spice", "SPICE-Anzeige", "choice",
((False, "nein - nur noVNC im Browser"),
(True, "ja - zusaetzlich remote-viewer"))))
rows.append(("usb", "USB-Weiterleitung", "choice",
((0, "keine"), (1, "1 Anschluss"), (2, "2 Anschluesse"),
(4, "4 Anschluesse"))))
rows += [
("newid", "Neue VMID", "text", ()),
("node", "Node", "text", ()),
("start", "Danach starten", "choice", ((True, "ja"), (False, "nein"))),
]
return rows
def _field_text(spec, key, kind, choices):
value = getattr(spec, key)
if kind == "choice":
for candidate, label in choices:
if candidate == value:
return label
return str(value)
if key == "newid":
return str(value) if value else "automatisch (naechste freie)"
if key == "transfers":
return transfer_ui.summary(value)
if key == "node":
return value or "wie das Original"
return str(value or "")
def _options(win, guest, spec):
"""Maske fuer die Einstellungen. True = anlegen, False = abbrechen."""
rows = _fields(guest)
index = 0
while True:
height, width = win.getmaxyx()
win.erase()
fill(win, 0, " Wiederherstellung einrichten - %s" % guest.label,
curses.color_pair(C_HEADER) | curses.A_BOLD)
for row, (key, label, kind, choices) in enumerate(rows):
selected = row == index
attr = curses.color_pair(C_SEL) if selected else 0
text = " %-26s %s" % (label + ":", _field_text(spec, key, kind, choices))
fill(win, row + 2, text.ljust(width - 2), attr)
note = {
"mode": "live = zum Hineinschauen. recovery = die Maschine wirklich "
"wieder in Betrieb nehmen.",
"net": "Ohne Netz kommt nichts hinein und nichts hinaus - dafuer ist "
"das Austauschlaufwerk da.",
"resume": "Snapshots mit Arbeitsspeicher laufen genau dort weiter, wo "
"sie standen - ohne Crash-Recovery.",
"transfers": "Vorbereitete Austauschmedien. Mit v anlegen und "
"befuellen, mit Enter auswaehlen.",
"spice": "Zweiter Zugang ueber remote-viewer. Achtung: vertraegt "
"sich nicht mit geladenem Arbeitsspeicher.",
"usb": "Ein USB-Geraet vom eigenen Rechner in den Gast reichen - "
"etwa einen Dongle. Laeuft ueber SPICE, nicht ueber das Netz.",
"newid": "VMID der neuen Maschine.",
"node": "Auf welchem Node sie laufen soll.",
"start": "Sofort hochfahren.",
}.get(rows[index][0], "")
for offset, line in enumerate(_wrap(note, width - 6)[:3]):
put(win, len(rows) + 4 + offset, 3, line, curses.color_pair(C_DIM))
for offset, line in enumerate(keybar(
width, [("Pfeile", "Auswaehlen", "Wahl"),
("Enter/Leer", "Aendern", "Aendern"),
("v", "Laufwerke verwalten", "Laufw."),
("F10", "Anlegen", "Anlegen"),
("q", "Abbrechen", "Abbr.")])):
fill(win, height - 2 + offset, " " + line, curses.color_pair(C_FOOTER))
win.refresh()
key = read_key(win)
current_key, _label, kind, choices = rows[index]
if key in ("v", "V"):
# Von hier aus verwalten statt zwei Ebenen zurueckzugehen: wer
# merkt, dass ihm ein Laufwerk fehlt, merkt es genau an dieser
# Stelle. Danach steht der Balken auf der Auswahlzeile.
transfer_ui.screen(win, browse=_browse_transfer)
index = next((i for i, r in enumerate(rows) if r[2] == "transfers"),
index)
elif key == curses.KEY_UP:
index = (index - 1) % len(rows)
elif key == curses.KEY_DOWN:
index = (index + 1) % len(rows)
elif key in (curses.KEY_F10,) or key == "\x06":
return True
elif is_escape(key) or key == "q":
return False
elif is_enter(key) or key == " " or key in (curses.KEY_RIGHT, curses.KEY_LEFT):
if kind == "transfers":
spec.transfers = transfer_ui.choose(win, spec.transfers)
else:
_edit(win, spec, current_key, kind, choices,
back=key == curses.KEY_LEFT)
def _wrap(text, width):
words, lines, current = str(text).split(), [], ""
for word in words:
if len(current) + len(word) + 1 > width:
lines.append(current)
current = word
else:
current = (current + " " + word).strip()
if current:
lines.append(current)
return lines
def _edit(win, spec, key, kind, choices, back=False):
if kind == "choice":
values = [c[0] for c in choices]
try:
position = values.index(getattr(spec, key))
except ValueError:
position = 0
position = (position - 1 if back else position + 1) % len(values)
setattr(spec, key, values[position])
if key == "mode":
# Die Betriebsart gibt die Vorgabe fuer das Netz vor.
spec.net = ""
return
label = {"newid": "Neue VMID (leer = automatisch)",
"node": "Node (leer = wie das Original)"}.get(key, key)
answer = prompt(win, label, str(getattr(spec, key) or ""))
if answer is None:
return
if key == "newid":
spec.newid = int(answer) if answer.isdigit() else 0
else:
setattr(spec, key, answer)
# ---------------------------------------------------------------------------
# Zusammenfassung
# ---------------------------------------------------------------------------
def summary_lines(plan_):
"""Was passieren wird - als Liste von (Text, Attribut)."""
guest, spec = plan_.guest, plan_.spec
lines = [
("Quelle: %s auf %s" % (guest.label, guest.node), 0),
("Snapshot: %s" % plan_.snapshot, 0),
("Neu: %s auf %s" % (plan_.label, plan_.node), curses.A_BOLD),
("Betriebsart: %s" % ("recovery - mit Netz, gleiche Identitaet"
if spec.mode == "recovery"
else "live - abgeschottet"), 0),
("", 0),
("Datentraeger (Copy-on-Write-Klone, das Original bleibt unberuehrt):", 0),
]
for key, volid, size in plan_.disks:
lines.append((" %-9s %s%s" % (key, volid, (" %s" % size) if size else ""), 0))
lines.append(("", 0))
netzwerk = {"none": "keine Netzwerkkarte",
"down": "Karte vorhanden, Leitung abgeklemmt",
"on": "AM NETZ - mit den MAC-Adressen des Originals"}
lines.append(("Netzwerk: %s" % netzwerk.get(plan_.net_mode, plan_.net_mode),
curses.color_pair(C_WARN) if plan_.net_mode == "on" else 0))
if plan_.nets:
for key, value in plan_.nets:
lines.append((" %-9s %s" % (key, truncate(value, 60)), 0))
if plan_.vmstate:
lines.append(("Arbeitsspeicher: %s"
% ("wird geladen (%s) - laeuft weiter statt zu booten"
% (human_bytes(plan_.vmstate_bytes) or "?")
if plan_.resume else "vorhanden, wird aber nicht geladen"),
curses.color_pair(C_OK) if plan_.resume else 0))
if plan_.spice:
lines.append(("Anzeige: noVNC und SPICE (vga: qxl)",
curses.color_pair(C_OK)))
if plan_.usb:
lines.append(("USB: %d Weiterleitung(en) ueber SPICE" % plan_.usb,
curses.color_pair(C_OK)))
if plan_.iso:
lines.append(("CD: %s" % plan_.iso, 0))
for volume in plan_.transfers:
lines.append(("Transfer: %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label),
curses.color_pair(C_OK)))
if plan_.notes:
lines.append(("", 0))
for note in plan_.notes:
for line in _wrap("- " + note, 74):
lines.append((line, curses.color_pair(C_DIM)))
for entry in plan_.warnings + plan_.critical:
lines.append(("", 0))
for line in _wrap("!! " + entry, 74):
lines.append((line, curses.color_pair(C_WARN) | curses.A_BOLD))
return lines
# ---------------------------------------------------------------------------
# Assistent
# ---------------------------------------------------------------------------
def _new(stdscr, proxmox):
_wait_screen(stdscr, "Frage Proxmox nach VMs und Containern ...")
try:
guests = proxmox.inventory(refresh=True)
except ProxmoxError as exc:
message(stdscr, str(exc), error=True)
return
while True:
guest = _pick_guest(stdscr, guests)
if guest is None:
return
_wait_screen(stdscr, "Lese Snapshots von %s ..." % guest.label)
try:
snapshots = [s for s in list_snapshots(proxmox, guest) if s.complete]
except ProxmoxError as exc:
message(stdscr, str(exc), error=True)
continue
if not snapshots:
message(stdscr, "%s hat keine brauchbaren Snapshots." % guest.label,
error=True)
continue
snapname = _pick_snapshot(stdscr, snapshots)
if snapname is None:
continue
spec = Spec()
while True:
if not _options(stdscr, guest, spec):
break
_wait_screen(stdscr, "Pruefe, was noetig ist ...")
try:
plan_ = plan(proxmox, guest, snapname, spec)
except (RecoveryError, ProxmoxError) as exc:
_pages(stdscr, [(line, curses.color_pair(C_WARN))
for line in _wrap(str(exc), 74)],
"Geht so nicht")
continue
key = _pages(stdscr, summary_lines(plan_),
"Zusammenfassung - noch ist nichts passiert",
"j = anlegen | beliebige andere Taste = zurueck")
if key not in ("j", "J", "y", "Y"):
continue
if plan_.critical:
# Beim Namen nennen, worum es geht - "Warnungen gelesen?"
# klickt man weg, ohne hinzusehen.
if not confirm(stdscr, truncate(plan_.critical[0], 60)
+ " Trotzdem?"):
continue
elif plan_.warnings and not confirm(
stdscr, "Warnungen gelesen und trotzdem anlegen?"):
continue
progress = _Progress(stdscr, "Richte %s ein" % plan_.label)
try:
instance = create(proxmox, plan_, progress=progress)
except (RecoveryError, ProxmoxError, OSError) as exc:
_pages(stdscr, [(line, curses.color_pair(C_WARN))
for line in _wrap(str(exc), 74)]
+ [("", 0)] + _plain(progress.lines),
"Fehlgeschlagen")
break
if spec.start:
progress("Starte %s ..." % instance.label)
try:
hint = start(proxmox, instance, progress=progress)
except (ProxmoxError, RecoveryError) as exc:
message(stdscr, "Angelegt, aber nicht gestartet: %s" % exc,
error=True)
else:
if hint:
_pages(stdscr, [(line, curses.color_pair(C_WARN))
for line in _wrap(hint, 74)],
"Angelegt und gestartet - mit einem Hinweis")
_details(stdscr, proxmox, instance)
return
# ---------------------------------------------------------------------------
# Detailansicht
# ---------------------------------------------------------------------------
def _details(stdscr, proxmox, instance):
while True:
status = proxmox.guest_status(instance.guest)
running = status.get("status") == "running"
lines = [
("Maschine: %s auf %s" % (instance.label, instance.node), curses.A_BOLD),
("Zustand: %s" % (status.get("status") or "unbekannt"),
curses.color_pair(C_OK if running else C_DIM)),
("Herkunft: %s" % instance.origin, 0),
("Betriebsart: %s" % instance.mode, 0),
("Angelegt: %s" % (datetime.fromtimestamp(instance.created)
.strftime("%d.%m.%Y %H:%M")
if instance.created else "?"), 0),
("", 0),
("Konsole im Browser:", 0),
]
for line in _wrap(instance.console_url(), 74):
lines.append((" " + line, curses.color_pair(C_MARK)))
if instance.type == "lxc":
lines.append((" oder auf dem Host: pct enter %d" % instance.vmid,
curses.color_pair(C_MARK)))
else:
lines.append((" SPICE (remote-viewer): pvesnap-recovery spice %d"
% instance.vmid, curses.color_pair(C_MARK)))
if instance.resumed:
lines.append(("", 0))
lines.append(("Der Arbeitsspeicher des Snapshots wurde geladen - beim "
"ersten Start lief die Maschine genau weiter.",
curses.color_pair(C_DIM)))
steckplaetze = transfer_state(proxmox, instance)
if steckplaetze:
lines.append(("", 0))
lines.append(("Transfer-Laufwerke:", 0))
for entry, drin in steckplaetze:
if entry.get("kind") == "bind":
zustand = "durchgereicht (Host und Gast gleichzeitig)"
farbe = C_OK
else:
zustand = ("eingesteckt als %s" % entry["key"]) if drin \
else "ausgeworfen - gehoert dem Host"
farbe = C_OK if drin else C_MARK
lines.append((" %-14s %s" % (entry["name"], zustand),
curses.color_pair(farbe)))
if any(e.get("kind") != "bind" for e, _ in steckplaetze):
lines.append((" e = auswerfen / einklinken, im laufenden "
"Betrieb", curses.color_pair(C_DIM)))
try:
linked = linked_volumes(proxmox, instance)
except (RecoveryError, ProxmoxError):
linked = []
lines.append(("", 0))
if linked:
lines.append(("Datentraeger: Linked Clone - haengt am Quell-Snapshot",
curses.color_pair(C_MARK)))
lines.append((" Belegt nur, was seither geschrieben wurde. Der "
"Quell-Snapshot ist dafuer unloeschbar.",
curses.color_pair(C_DIM)))
lines.append((" Fuer den Dauerbetrieb mit f loesen (kopiert die "
"Daten wirklich).", curses.color_pair(C_DIM)))
elif instance.volumes:
lines.append(("Datentraeger: eigenstaendig - haengt an keinem Snapshot",
curses.color_pair(C_OK)))
# "Auswerfen" allein verschweigt die Haelfte - dieselbe Taste holt das
# Laufwerk auch wieder herein. Und v gehoert hierher, weil ein
# ausgeworfenes Laufwerk beim Host liegt und genau jetzt befuellt
# werden will.
footer = [("s", "Starten", "Start"), ("h", "Herunterfahren", "Stop"),
("e", "Auswerfen/Einklinken", "Ausw."),
("v", "Laufwerke", "Laufw."), ("p", "SPICE/USB", "SPICE"),
("f", "Loesen", "Loesen"), ("x", "Verwerfen", "Verw."),
("q", "zurueck", "zurueck")]
key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer)
if key in ("q", "\x1b") or is_escape(key):
return
if key in ("s", "S"):
_do(stdscr, "Starte %s ..." % instance.label,
lambda: start(proxmox, instance))
elif key in ("h", "H"):
_herunterfahren(stdscr, proxmox, instance)
elif key in ("e", "E"):
_swap(stdscr, proxmox, instance, steckplaetze)
elif key in ("v", "V"):
transfer_ui.screen(stdscr, browse=_browse_transfer)
elif key in ("p", "P"):
_display(stdscr, proxmox, instance)
elif key in ("f", "F"):
_flatten(stdscr, proxmox, instance, linked)
elif key in ("x", "X"):
if _destroy(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:
hint = action()
except (ProxmoxError, RecoveryError) as exc:
message(stdscr, str(exc), error=True)
return
if isinstance(hint, str) and hint:
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":
message(stdscr, "Bei Containern gibt es kein SPICE - das ist eine "
"reine Sache virtueller Maschinen.", error=True)
return
spice, usb = display_state(proxmox, instance)
ziel = choose(stdscr, "SPICE-Anzeige (jetzt: %s)" % ("ein" if spice else "aus"),
[(True, "ein - zusaetzlich remote-viewer, noetig fuer USB"),
(False, "aus - nur noVNC im Browser")])
if ziel is None:
return
anzahl = usb
if ziel:
anzahl = choose(stdscr, "USB-Weiterleitung (jetzt: %d)" % usb,
[(0, "keine"), (1, "1 Anschluss"), (2, "2 Anschluesse"),
(4, "4 Anschluesse")])
if anzahl is None:
return
laeuft = proxmox.guest_status(instance.guest).get("status") == "running"
if laeuft and ziel != spice:
# Die Grafikkarte laesst sich nur beim Start wechseln. Und der
# 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,
"",
"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 "
"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.")
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")
if key not in ("j", "J", "y", "Y"):
return
progress = _Progress(stdscr, "Stelle Anzeige um")
try:
geaendert, neustart = set_display(proxmox, instance, ziel, anzahl,
progress=progress)
except (RecoveryError, ProxmoxError) as exc:
_pages(stdscr, [(line, curses.color_pair(C_WARN))
for line in _wrap(str(exc), 74)], "Fehlgeschlagen")
return
if not geaendert:
message(stdscr, "War schon so eingestellt.")
return
if neustart:
_neu_starten(stdscr, proxmox, instance)
else:
message(stdscr, "Uebernommen.")
def _swap(stdscr, proxmox, instance, steckplaetze):
"""Transfer-Laufwerk im laufenden Betrieb abziehen oder wieder einklinken."""
wechselbar = [(e, drin) for e, drin in steckplaetze if e.get("kind") != "bind"]
if not wechselbar:
message(stdscr, "Hier gibt es nichts zu wechseln - durchgereichte "
"Verzeichnisse bleiben, wie sie sind.", error=True)
return
if len(wechselbar) == 1:
entry, drin = wechselbar[0]
else:
gewaehlt = choose(stdscr, "Welches Laufwerk?",
[((e, d), "%-14s %s"
% (e["name"], "eingesteckt" if d else "ausgeworfen"))
for e, d in wechselbar])
if gewaehlt is None:
return
entry, drin = gewaehlt
name = entry["name"]
if drin:
if not confirm(stdscr, "%r auswerfen? Im Gast vorher aushaengen!" % name):
return
_do(stdscr, "Werfe %r aus ..." % name,
lambda: eject_transfer(proxmox, instance, name))
else:
_do(stdscr, "Klinke %r ein ..." % name,
lambda: insert_transfer(proxmox, instance, name))
def _flatten(stdscr, proxmox, instance, linked):
if not linked:
message(stdscr, "%s haengt an keinem Snapshot mehr." % instance.label)
return
_wait_screen(stdscr, "Ermittle, wieviel kopiert werden muss ...")
try:
cost = flatten_cost(proxmox, instance)
except (RecoveryError, ProxmoxError):
cost = 0
if not confirm(stdscr, "%s kopieren und dauerhaft loesen?"
% (human_bytes(cost) if cost else "%d Datentraeger" % len(linked))):
return
progress = _Progress(stdscr, "Loese %s vom Quell-Snapshot" % instance.label)
try:
done, _already, other = flatten(proxmox, instance, progress=progress)
except (RecoveryError, ProxmoxError) as exc:
_pages(stdscr, [(line, curses.color_pair(C_WARN))
for line in _wrap(str(exc), 74)] + [("", 0)]
+ _plain(progress.lines), "Fehlgeschlagen")
return
lines = ["%d Datentraeger geloest." % len(done), ""]
for volid in other:
lines += _wrap("%s: %s" % (volid, flatten_hint(proxmox, volid)), 74)
if not linked_volumes(proxmox, instance):
lines += ["", "%s ist jetzt eigenstaendig. Die Snapshots des Originals "
"lassen sich wieder loeschen." % instance.label]
_pages(stdscr, _plain(lines), "Fertig")
def _destroy(stdscr, proxmox, instance):
if not confirm(stdscr, "%s wirklich verwerfen? Alle Aenderungen darin sind weg."
% instance.label):
return False
progress = _Progress(stdscr, "Verwerfe %s" % instance.label)
try:
destroy(proxmox, instance, progress=progress)
except (RecoveryError, ProxmoxError) as exc:
_pages(stdscr, [(line, curses.color_pair(C_WARN))
for line in _wrap(str(exc), 74)] + [("", 0)]
+ _plain(progress.lines), "Fehlgeschlagen")
return False
return True
# ---------------------------------------------------------------------------
# Uebersicht
# ---------------------------------------------------------------------------
def _overview(stdscr, proxmox):
index = 0
instances = []
reload_ = True
while True:
if reload_:
_wait_screen(stdscr, "Frage nach vorhandenen Wiederherstellungen ...")
instances = list_instances(proxmox)
reload_ = False
index = min(index, max(0, len(instances) - 1))
height, width = stdscr.getmaxyx()
stdscr.erase()
fill(stdscr, 0, " pvesnap-recovery %s - Snapshots als Maschine starten"
% __version__, curses.color_pair(C_HEADER) | curses.A_BOLD)
if not instances:
put(stdscr, 3, 3, "Zurzeit laeuft keine Wiederherstellung.", curses.A_BOLD)
put(stdscr, 5, 3, "Mit n eine neue anlegen:", 0)
put(stdscr, 6, 5, "live Snapshot abgeschottet starten, ohne Netzwerk -",
curses.color_pair(C_DIM))
put(stdscr, 7, 5, " zum Hineinschauen und Daten herausholen.",
curses.color_pair(C_DIM))
put(stdscr, 8, 5, "recovery Snapshot mit Netzwerk und gleicher Identitaet",
curses.color_pair(C_DIM))
put(stdscr, 9, 5, " wieder in Betrieb nehmen.",
curses.color_pair(C_DIM))
else:
put(stdscr, 2, 2, "%-10s %-9s %-22s %-28s %s"
% ("Maschine", "Zustand", "Node", "Herkunft", "Modus"),
curses.A_BOLD | curses.A_UNDERLINE)
rows = max(1, height - 6)
top = max(0, min(index - rows + 1, len(instances) - rows))
for row in range(min(rows, len(instances) - top)):
instance = instances[top + row]
attr = curses.color_pair(C_SEL) if top + row == index else 0
if not attr and instance.status == "running":
attr = curses.color_pair(C_OK)
text = " %-10s %-9s %-22s %-28s %s" % (
instance.label, instance.status or "?",
truncate(instance.node or "?", 22),
truncate(instance.origin, 28), instance.mode)
fill(stdscr, row + 3, text, attr)
for offset, line in enumerate(keybar(width, KEYS)):
fill(stdscr, height - 2 + offset, " " + line, curses.color_pair(C_FOOTER))
stdscr.refresh()
key = read_key(stdscr)
current = instances[index] if instances else None
if key in ("q", "Q") or is_escape(key):
return 0
if key in ("n", "N"):
_new(stdscr, proxmox)
reload_ = True
elif key in ("v", "V"):
transfer_ui.screen(stdscr, browse=_browse_transfer)
reload_ = True
elif key in ("r", "R"):
reload_ = True
elif key == curses.KEY_UP and instances:
index = (index - 1) % len(instances)
elif key == curses.KEY_DOWN and instances:
index = (index + 1) % len(instances)
elif current is None:
continue
elif is_enter(key):
_details(stdscr, proxmox, current)
reload_ = True
elif key in ("s", "S"):
_do(stdscr, "Starte %s ..." % current.label,
lambda: start(proxmox, current))
reload_ = True
elif key in ("h", "H"):
_herunterfahren(stdscr, proxmox, current)
reload_ = True
elif key in ("x", "X"):
_destroy(stdscr, proxmox, current)
reload_ = True
def _browse_transfer(win, path, volume):
"""Den Zwei-Fenster-Commander auf ein Transfer-Laufwerk loslassen.
Links das Laufwerk, rechts der lokale Rechner - also genau der Weg, um
Werkzeuge hineinzukopieren, bevor die Maschine startet, und den Dump
hinterher wieder herauszuholen.
"""
from .explorer import Explorer, Pane
links = Pane(path=path, root=path, readonly=False,
title="Transfer: %s (%s)" % (volume.name, volume.label))
rechts = Pane(path=os.getcwd(), root="/", readonly=False,
title="Lokaler Rechner")
Explorer(links, rechts,
subtitle="Transfer-Laufwerk %s - beide Seiten beschreibbar"
% volume.name).run(win)
def _main(stdscr, _args):
curses.curs_set(0)
if hasattr(curses, "set_escdelay"):
curses.set_escdelay(25)
init_colors()
stdscr.keypad(True)
return _overview(stdscr, Proxmox())
# ---------------------------------------------------------------------------
# Befehlszeile
# ---------------------------------------------------------------------------
def _find_guest(proxmox, vmid):
guest = next((g for g in proxmox.inventory() if g.vmid == int(vmid)), None)
if guest is None:
raise RecoveryError("Gast %s gibt es nicht." % vmid)
return guest
def _find_instance(proxmox, vmid):
instance = next((i for i in list_instances(proxmox) if i.vmid == int(vmid)), None)
if instance is None:
raise RecoveryError("%s ist keine von pvesnap angelegte Wiederherstellung. "
"Vorhandene zeigt: pvesnap-recovery list" % vmid)
return instance
def _newest_snapshot(proxmox, guest):
snapshots = [s for s in list_snapshots(proxmox, guest) if s.complete]
if not snapshots:
raise RecoveryError("%s hat keine brauchbaren Snapshots." % guest.label)
return snapshots[0].name
def _spec_from_args(args, mode):
spec = Spec(mode=mode)
spec.newid = args.newid or 0
spec.node = args.node or ""
spec.net = args.net or ""
spec.resume = args.resume
spec.transfers = list(args.transfer or [])
spec.spice = bool(getattr(args, "spice", False))
spec.usb = int(getattr(args, "usb", 0) or 0)
spec.iso = getattr(args, "iso", "") or ""
spec.memory = args.memory or 0
spec.cores = args.cores or 0
spec.name = args.name or ""
spec.keep_binds = args.keep_binds
spec.start = not args.no_start
return spec
def _print_summary(plan_):
print(" Quelle : %s auf %s" % (plan_.guest.label, plan_.guest.node))
print(" Snapshot : %s" % plan_.snapshot)
print(" Neu : %s auf %s" % (plan_.label, plan_.node))
for key, volid, size in plan_.disks:
print(" Klon : %-9s %s%s" % (key, volid, (" %s" % size) if size else ""))
netzwerk = {"none": "keine Netzwerkkarte",
"down": "Karte vorhanden, Leitung abgeklemmt",
"on": "AM NETZ mit den MAC-Adressen des Originals"}
print(" Netzwerk : %s" % netzwerk.get(plan_.net_mode, plan_.net_mode))
if plan_.resume:
print(" Arbeitsspei.: wird geladen (%s)"
% (human_bytes(plan_.vmstate_bytes) or "?"))
for volume in plan_.transfers:
print(" Transfer : %-14s %8s im Gast als %s"
% (volume.name, human_bytes(volume.size), volume.label))
for note in plan_.notes:
print(" Hinweis : %s" % note)
for warning in plan_.warnings:
print(" ACHTUNG : %s" % warning)
for reason in plan_.critical:
print(" GEFAHR : %s" % reason)
def _ask(question):
"""Frage am Terminal. Ohne Eingabemoeglichkeit gilt: nein.
Wird die Ausgabe in ein Skript geleitet, liefert input() ein EOFError -
das darf keinen Stacktrace geben, sondern muss als Absage zaehlen.
"""
try:
return input(question).strip().lower()
except (EOFError, KeyboardInterrupt):
print()
return ""
def cmd_create(proxmox, args, mode):
guest = _find_guest(proxmox, args.vmid)
snapname = args.snapshot or _newest_snapshot(proxmox, guest)
spec = _spec_from_args(args, mode)
plan_ = plan(proxmox, guest, snapname, spec)
print("Vorhaben:")
_print_summary(plan_)
# Gefaehrliches laesst sich nicht mit -y wegwinken. -y heisst "keine
# Routinefragen", nicht "frag mich auch dann nicht, wenn es weh tut".
if plan_.critical and not args.force:
# Sonst ueberholt die Fehlerausgabe die gepufferte Zusammenfassung,
# sobald jemand die Ausgabe umleitet - und die Begruendung stuende
# ueber dem, worauf sie sich bezieht.
sys.stdout.flush()
print("\nDas geht so nicht:", file=sys.stderr)
for reason in plan_.critical:
print(" - %s" % reason, file=sys.stderr)
if args.yes:
print("\n-y reicht dafuer nicht. Entweder das Original stoppen oder\n"
"bewusst --force setzen.", file=sys.stderr)
return 2
print("\nWer das trotzdem will, tippt jetzt das Wort ja aus.")
if _ask("Trotzdem anlegen? [ja/nein] ") not in ("ja", "yes"):
print("Abgebrochen.")
return 1
elif not args.yes:
if plan_.warnings:
print("\nEs gibt Warnungen - bitte oben lesen.")
answer = _ask("\nAnlegen? [j/N] ")
if answer not in ("j", "y", "ja", "yes"):
print("Abgebrochen.")
return 1
instance = create(proxmox, plan_, progress=lambda text: print(" " + text))
print("\n%s ist eingerichtet." % instance.label)
if spec.start:
print(" Starte ...")
hint = start(proxmox, instance, progress=lambda text: print(" " + text))
print(" laeuft." if not hint else " ACHTUNG: %s" % hint)
_print_access(instance)
return 0
def _print_access(instance):
print("\n Konsole im Browser:")
print(" %s" % instance.console_url())
if instance.type == "lxc":
print(" oder auf dem Host: pct enter %d" % instance.vmid)
print("\n Wieder verwerfen: pvesnap-recovery destroy %d" % instance.vmid)
def cmd_list(proxmox, _args):
instances = list_instances(proxmox)
if not instances:
print("Zurzeit laeuft keine Wiederherstellung.")
return 0
print("%-10s %-9s %-14s %-30s %s"
% ("Maschine", "Zustand", "Node", "Herkunft", "Modus"))
for instance in instances:
print("%-10s %-9s %-14s %-30s %s"
% (instance.label, instance.status or "?", instance.node or "?",
instance.origin, instance.mode))
return 0
def cmd_destroy(proxmox, args):
instance = _find_instance(proxmox, args.vmid)
if not args.yes:
print("%s (%s) wird restlos entfernt - alle Aenderungen darin sind weg."
% (instance.label, instance.origin))
if _ask("Wirklich? [j/N] ") not in ("j", "y", "ja", "yes"):
print("Abgebrochen.")
return 1
destroy(proxmox, instance, progress=lambda text: print(" " + text))
return 0
def cmd_flatten(proxmox, args):
instance = _find_instance(proxmox, args.vmid)
linked = linked_volumes(proxmox, instance)
if not linked:
print("%s haengt an keinem Snapshot mehr - nichts zu tun." % instance.label)
return 0
print("%s loest sich von %s." % (instance.label, instance.origin))
for volid in linked:
print(" %s" % volid)
cost = flatten_cost(proxmox, instance)
print("\nDabei werden die Daten wirklich kopiert%s."
% ((" - rund %s" % human_bytes(cost)) if cost else ""))
if not args.yes:
if _ask("Jetzt loesen? [j/N] ") not in ("j", "y", "ja", "yes"):
print("Abgebrochen.")
return 1
done, already, other = flatten(proxmox, instance, stream=True, force=args.force,
progress=lambda text: print(" " + text))
print("\n%d Datentraeger geloest." % len(done))
for volid in already:
print(" war schon eigenstaendig: %s" % volid)
for volid in other:
print(" %s: %s" % (volid, flatten_hint(proxmox, volid)))
if not linked_volumes(proxmox, instance):
print("\n%s ist jetzt eigenstaendig. Die Snapshots des Originals lassen "
"sich wieder loeschen." % instance.label)
return 0
def cmd_cleanup(proxmox, args):
orphans = find_orphans(proxmox)
if not orphans:
print("Nichts liegengeblieben.")
return 0
clones = [e for e in orphans if e["parent"]]
plain = [e for e in orphans if not e["parent"]]
print("Datentraeger, zu denen es keinen Gast mehr gibt:\n")
for entry in clones:
print(" %-26s %8s Klon von %s"
% (entry["volid"], human_bytes(entry["bytes"]), entry["parent"]))
for entry in plain:
print(" %-26s %8s kein Klon"
% (entry["volid"], human_bytes(entry["bytes"])))
wanted = orphans if args.all else clones
if not wanted:
print("\nNur Datentraeger ohne Elternteil gefunden. Die werden nicht von\n"
"selbst entfernt - mit --all auch diese.")
return 0
print("\nEntfernt werden %d Datentraeger%s."
% (len(wanted), "" if args.all else " (nur Klone; --all nimmt auch die anderen)"))
print("Der Schutz der Quell-Snapshots wird dabei geloest, sofern kein\n"
"anderer Klon mehr daran haengt.")
if not args.yes:
if _ask("\nEntfernen? [j/N] ") not in ("j", "y", "ja", "yes"):
print("Abgebrochen.")
return 1
removed, failed = remove_orphans(proxmox, wanted,
progress=lambda text: print(" " + text))
print("\n%d entfernt." % len(removed))
for problem in failed:
print(" fehlgeschlagen: %s" % problem, file=sys.stderr)
return 1 if failed else 0
def cmd_spice(proxmox, args):
instance = _find_instance(proxmox, args.vmid)
text = spice_file(proxmox, instance)
if args.output:
with open(args.output, "w", encoding="utf-8") as handle:
handle.write(text)
os.chmod(args.output, 0o600)
print(args.output)
print("\n Auf dem eigenen Rechner oeffnen:", file=sys.stderr)
print(" scp root@%s:%s . && remote-viewer %s"
% (node_address(instance.node), args.output,
os.path.basename(args.output)), file=sys.stderr)
else:
sys.stdout.write(text)
print("\n Umleiten und mit remote-viewer oeffnen:", file=sys.stderr)
print(" pvesnap-recovery spice %d -o vm.vv" % instance.vmid,
file=sys.stderr)
print(" Das Kennwort darin ist ein Einmal-Kennwort und laeuft bald ab.",
file=sys.stderr)
return 0
def cmd_console(proxmox, args):
instance = _find_instance(proxmox, args.vmid)
print(instance.console_url())
if instance.type == "lxc":
print("pct enter %d" % instance.vmid, file=sys.stderr)
return 0
def cmd_start(proxmox, args):
hint = start(proxmox, _find_instance(proxmox, args.vmid),
progress=lambda text: print(" " + text))
print("laeuft." if not hint else "ACHTUNG: %s" % hint)
return 0
def cmd_stop(proxmox, args):
proxmox.shutdown_guest(_find_instance(proxmox, args.vmid).guest)
print("heruntergefahren.")
return 0
def _add_create_options(parser):
parser.add_argument("vmid", type=int, help="VMID des Originals")
parser.add_argument("snapshot", nargs="?",
help="Name des Snapshots (Vorgabe: der neueste)")
parser.add_argument("--newid", type=int, help="VMID der neuen Maschine")
parser.add_argument("--node", help="Node, auf dem sie laufen soll")
parser.add_argument("--net", choices=("none", "down", "on"),
help="none = keine Karte, down = Karte ohne Leitung, "
"on = voll am Netz")
parser.add_argument("--resume", dest="resume", action="store_true", default=None,
help="Arbeitsspeicher des Snapshots laden")
parser.add_argument("--no-resume", dest="resume", action="store_false",
help="kalt starten, auch wenn der Snapshot RAM enthaelt")
parser.add_argument("--spice", action="store_true",
help="SPICE-Anzeige einschalten (vga: qxl) - vertraegt "
"sich nicht mit geladenem Arbeitsspeicher")
parser.add_argument("--usb", type=int, default=0, metavar="ANZAHL",
help="so viele USB-Weiterleitungen ueber SPICE, z.B. "
"fuer einen Dongle (geht auch mit RAM-Zustand)")
parser.add_argument("--iso", metavar="VOLID",
help="Abbild als CD einlegen, z.B. local:iso/virtio-win.iso")
parser.add_argument("--transfer", action="append", metavar="NAME", default=[],
help="vorbereitetes Transfer-Laufwerk anhaengen "
"(mehrfach moeglich)")
parser.add_argument("--memory", type=int, help="Arbeitsspeicher in MB abweichend")
parser.add_argument("--cores", type=int, help="Kerne abweichend")
parser.add_argument("--name", help="Name der neuen Maschine")
parser.add_argument("--keep-binds", action="store_true",
help="durchgereichte Host-Verzeichnisse des Originals "
"uebernehmen (Vorgabe: weglassen)")
parser.add_argument("--no-start", action="store_true",
help="nur einrichten, nicht starten")
parser.add_argument("-y", "--yes", action="store_true",
help="Routinefragen ueberspringen (deckt --force nicht ab)")
parser.add_argument("--force", action="store_true",
help="auch dann anlegen, wenn das Original noch laeuft - "
"gleiche MAC und IP zweimal im Netz")
def build_parser():
parser = argparse.ArgumentParser(
prog="pvesnap-recovery",
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Einen Proxmox-Snapshot als eigenstaendige Maschine starten.",
epilog="""Beispiele:
pvesnap-recovery
ncurses-Oberflaeche mit Uebersicht und Assistent
pvesnap-recovery live 802 --transfer dumps
neuesten Snapshot von VM 802 ohne Netzwerk starten, mit dem
vorbereiteten Transfer-Laufwerk "dumps"
pvesnap-recovery live 100 --transfer dumps --transfer werkzeuge
Container 100 abgeschottet starten, mit zwei Laufwerken
pvesnap-recovery
Taste v: Transfer-Laufwerke anlegen und befuellen (Commander)
Taste e: im laufenden Betrieb auswerfen und wieder einklinken
pvesnap-recovery recover 802 autosnap-... --newid 9802
mit Netzwerk und gleicher Identitaet - nur wenn das Original steht!
pvesnap-recovery pull 9802
Austauschlaufwerk nach dem Herunterfahren auf den Host holen
pvesnap-recovery destroy 9802
restlos verwerfen: Maschine weg, Klone weg, Schutz der Snapshots geloest
""")
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-V", "--version", action="version",
version="pvesnap-recovery %s" % __version__)
subs = parser.add_subparsers(dest="command", metavar="BEFEHL")
live = subs.add_parser("live", help="Snapshot abgeschottet starten (ohne Netz)")
_add_create_options(live)
recover = subs.add_parser("recover",
help="Snapshot mit Netz und gleicher Identitaet starten")
_add_create_options(recover)
subs.add_parser("list", help="vorhandene Wiederherstellungen zeigen")
for name, helptext in (("start", "hochfahren"), ("stop", "herunterfahren"),
("console", "noVNC-Adresse zeigen")):
sub = subs.add_parser(name, help=helptext)
sub.add_argument("vmid", type=int)
spice = subs.add_parser("spice",
help="SPICE-Verbindungsdatei fuer remote-viewer")
spice.add_argument("vmid", type=int)
spice.add_argument("-o", "--output", metavar="DATEI",
help="hineinschreiben statt auf die Ausgabe")
flatten_ = subs.add_parser(
"flatten", help="vom Quell-Snapshot loesen - fuer den Dauerbetrieb")
flatten_.add_argument("vmid", type=int)
flatten_.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen")
flatten_.add_argument("--force", action="store_true",
help="auch starten, wenn der Storage dafuer zu voll ist")
cleanup = subs.add_parser(
"cleanup", help="Klone entfernen, zu denen es keinen Gast mehr gibt")
cleanup.add_argument("--all", action="store_true",
help="auch Datentraeger ohne Elternteil entfernen")
cleanup.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen")
destroy_ = subs.add_parser("destroy", help="Wiederherstellung restlos verwerfen")
destroy_.add_argument("vmid", type=int)
destroy_.add_argument("-y", "--yes", action="store_true", help="nicht nachfragen")
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING,
format="%(levelname)s %(message)s")
if not pvesh_available():
print("'pvesh' nicht gefunden - pvesnap-recovery laeuft auf dem "
"Proxmox-VE-Host.", file=sys.stderr)
return 3
if os.geteuid() != 0:
print("pvesnap-recovery braucht root-Rechte (Klone anlegen, Maschinen "
"einrichten).", file=sys.stderr)
return 4
proxmox = Proxmox()
handlers = {
"live": lambda: cmd_create(proxmox, args, "live"),
"recover": lambda: cmd_create(proxmox, args, "recovery"),
"list": lambda: cmd_list(proxmox, args),
"destroy": lambda: cmd_destroy(proxmox, args),
"flatten": lambda: cmd_flatten(proxmox, args),
"cleanup": lambda: cmd_cleanup(proxmox, args),
"console": lambda: cmd_console(proxmox, args),
"spice": lambda: cmd_spice(proxmox, args),
"start": lambda: cmd_start(proxmox, args),
"stop": lambda: cmd_stop(proxmox, args),
}
try:
if args.command in handlers:
return handlers[args.command]()
except (RecoveryError, ProxmoxError) as exc:
print("Fehler: %s" % exc, file=sys.stderr)
return 1
except KeyboardInterrupt:
print("\nAbgebrochen.", file=sys.stderr)
return 130
# Ohne Befehl: Oberflaeche. Dabei darf nichts ins Terminal protokolliert werden.
logging_handlers = logging.getLogger().handlers[:]
if not args.verbose:
logging.getLogger().handlers = []
try:
return curses.wrapper(_main, args)
except KeyboardInterrupt:
return 130
finally:
logging.getLogger().handlers = logging_handlers
if __name__ == "__main__": # pragma: no cover
sys.exit(main())