Auf dem Proxmox-Host loest der Node-Name auf, am Arbeitsplatz meist nicht -
und dann ist der noVNC-Verweis wertlos. Die Adresse kommt jetzt aus
/etc/pve/.members (lokale Datei, kein API-Aufruf); ist dort nichts zu finden,
bleibt es beim Namen.
vorher https://pvetest01:8006/?console=kvm&novnc=1&vmid=102&node=pvetest01
nachher https://172.0.4.1:8006/?console=kvm&novnc=1&vmid=102&node=pvetest01
Der Node bleibt als Name im Verweis stehen - so erwartet Proxmox ihn.
Dasselbe betraf zwei weitere Stellen: den scp-Hinweis beim SPICE-Befehl und
den proxy-Eintrag in der .vv-Datei. Letzterer trug ebenfalls den Hostnamen,
womit auch remote-viewer nicht durchgekommen waere. Die TLS-Pruefung laeuft
ueber "host-subject" und bleibt davon unberuehrt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1121 lines
45 KiB
Python
1121 lines
45 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
|
|
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,
|
|
list_instances, node_address, plan, remove_orphans,
|
|
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 ""
|
|
fill(win, height - 1, " " + footer + more, 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)))
|
|
|
|
footer = ("s Start | h Stop | e Auswerfen | f Loesen | "
|
|
"x Verwerfen | q 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"):
|
|
_do(stdscr, "Fahre %s herunter ..." % instance.label,
|
|
lambda: proxmox.shutdown_guest(instance.guest))
|
|
elif key in ("e", "E"):
|
|
_swap(stdscr, proxmox, instance, steckplaetze)
|
|
elif key in ("f", "F"):
|
|
_flatten(stdscr, proxmox, instance, linked)
|
|
elif key in ("x", "X"):
|
|
if _destroy(stdscr, proxmox, instance):
|
|
return
|
|
|
|
|
|
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 _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"):
|
|
_do(stdscr, "Fahre %s herunter ..." % current.label,
|
|
lambda: proxmox.shutdown_guest(current.guest))
|
|
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_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())
|