Files
proxmox-snapshot-service/handbuch/werkstatt/run.py
T
duffyduckandClaude Opus 5 59e7224297 Handbuch auf MkDocs-Basis, mit Bildschirmfotos aus dem Programm
26 Kapitel in vier Teilen, in der Reihenfolge, in der man sie braucht:
erst sichern, dann Dateien holen, dann ganze Maschinen wiederherstellen,
dahinter der Nachschlagteil.

Gebaut wird mit MkDocs + Material. Bewusst ohne Netzabhaengigkeiten:
keine Schriften vom CDN (font: false), Volltextsuche mit deutschem
Stemming liegt neben den Seiten. Im Notfall steht vielleicht das halbe
Netz - dann nuetzt eine Doku im Internet nichts.

  handbuch/bauen.sh              baut handbuch/site/
  handbuch/bauen.sh ansehen      Vorschau auf 127.0.0.1:8000

install.sh nimmt das gebaute Handbuch mit nach
/usr/share/doc/pvesnap/handbuch/ - falls es vorliegt. Auf dem Host selbst
wird nichts gebaut, mkdocs gehoert nicht auf einen Hypervisor.

Die 28 Bildschirmfotos sind nicht abfotografiert, sondern erzeugt: Der
echte Programmcode laeuft in einem Pseudo-Terminal gegen einen erfundenen
Proxmox-Host (Attrappen fuer pvesh, perl und rbd), pyte baut den Bildschirm
nach, heraus faellt ein SVG. Damit stimmen sie garantiert mit dem Programm
ueberein, sind reproduzierbar und enthalten keine echten Daten. Die
Werkstatt dafuer liegt unter handbuch/werkstatt/ samt LIESMICH.md.

Nebenbei: die Schlussmeldung von install.sh warb noch mit --exchange,
das mit dem eingebauten Austauschlaufwerk weggefallen ist.

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

217 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Startet eine Oberflaeche von pvesnap gegen die Demo-Kulisse.
Aufruf: run.py <szene> [argumente]
Der eigentliche Trick steckt in _patch(): alle Pfade, die sonst auf einen
echten Proxmox-Host zeigen, werden auf demo/ umgebogen. Der Programmcode
selbst wird nicht angefasst - die Bildschirmfotos zeigen also wirklich das,
was auf dem Host auch zu sehen ist.
"""
from __future__ import annotations
import curses
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
DEMO = os.path.join(HERE, "demo")
sys.path.insert(0, os.environ.get("PVESNAP_SRC", ""))
# Wo die Kulisse haengt, wenn stage.sh sie eingehaengt hat. Ohne Namensraum
# (beim schnellen Nachsehen) wird auf die Verzeichnisse selbst zurueckgefallen.
SNAP_MNT = "/run/pvesnap/mnt/101-auto-taeglich-20260809-023000"
TRANSFER_MNT = "/run/pvesnap/transfer/dumps"
LOKAL = "/root"
def _oder(pfad, ersatz):
return pfad if os.path.isdir(pfad) and os.listdir(pfad) else ersatz
def _patch():
from pvesnap import preflight, recovery, transfer, tui
recovery.STATE_DIR = os.path.join(DEMO, "lib")
recovery.REGISTRY = os.path.join(DEMO, "lib", "recovery.json")
recovery.CONF_ROOT = os.path.join(DEMO, "pve", "nodes")
transfer.TRANSFER_DIR = os.path.join(DEMO, "lib", "transfer")
transfer.INDEX = os.path.join(DEMO, "lib", "transfer", "index.json")
transfer.MOUNT_ROOT = "/run/pvesnap/transfer"
transfer.CONF_ROOT = recovery.CONF_ROOT
# Loop-Geraete und Einhaengepunkte gibt es hier nicht - also erzaehlen wir
# sie. "dumps" haengt in der Maschine, "austausch" gerade am Host.
abbild = lambda name: os.path.join(transfer.TRANSFER_DIR, name + ".img")
transfer._loop_devices = lambda: {abbild("dumps"): "/dev/loop3",
abbild("austausch"): "/dev/loop5"}
transfer._mounts = lambda: {"/dev/loop5p1": os.path.join(transfer.MOUNT_ROOT,
"austausch")}
with open(os.path.join(DEMO, "pve", ".members"), encoding="utf-8") as handle:
mitglieder = json.load(handle)
recovery._ADDRESSES.update({n: e["ip"] for n, e
in mitglieder["nodelist"].items()})
preflight.check_environment = lambda require_pve=True: []
tui.service_state = lambda: (True, "active")
tui.service_text = lambda: "Dienst: laeuft"
def _explorer_panes(links_pfad, links_titel, untertitel, links_schreibbar=False):
from pvesnap.explorer import Explorer, Pane
links = Pane(path=links_pfad, root=links_pfad,
readonly=not links_schreibbar, title=links_titel)
rechts = Pane(path=_oder(LOKAL, os.path.join(DEMO, "lokal")), root="/",
readonly=False, title="Lokaler Rechner")
return Explorer(links, rechts, subtitle=untertitel)
# ---------------------------------------------------------------------------
# Szenen
# ---------------------------------------------------------------------------
def szene_config(stdscr):
from pvesnap.tui import Editor
pfad = "/etc/pvesnap.conf"
editor = Editor(pfad if os.path.exists(pfad)
else os.path.join(DEMO, "pvesnap.conf"))
editor.load()
editor.run(stdscr)
def szene_recovery(stdscr):
from pvesnap.proxmox import Proxmox
from pvesnap.recovery_ui import _overview
_overview(stdscr, Proxmox())
def szene_transfer(stdscr):
from pvesnap import transfer_ui
from pvesnap.recovery_ui import _browse_transfer
transfer_ui.screen(stdscr, browse=_browse_transfer)
def szene_explorer(stdscr):
_explorer_panes(_oder(SNAP_MNT, os.path.join(DEMO, "snapshot")),
"VM 101 (db01) @ auto-taeglich-20260809-023000",
"VM 101 (db01) - Snapshot auto-taeglich-20260809-023000"
).run(stdscr)
def szene_explorer_transfer(stdscr):
_explorer_panes(_oder(TRANSFER_MNT, os.path.join(DEMO, "transfer-dumps")),
"Transfer: dumps (DUMPS)",
"Transfer-Laufwerk dumps - beide Seiten beschreibbar",
links_schreibbar=True).run(stdscr)
def szene_gastauswahl(stdscr):
from pvesnap.explorer import _pick_guest
from pvesnap.proxmox import Proxmox
_pick_guest(stdscr, Proxmox().inventory())
def szene_snapshotauswahl(stdscr):
from pvesnap.explorer import _pick_snapshot
from pvesnap.proxmox import Proxmox
from pvesnap.snapfs import list_snapshots
proxmox = Proxmox()
gast = next(g for g in proxmox.inventory() if g.vmid == 101)
_pick_snapshot(stdscr, [s for s in list_snapshots(proxmox, gast) if s.complete])
def szene_neu(stdscr):
"""Die Optionsmaske des Assistenten - ohne etwas anzulegen."""
from pvesnap.proxmox import Proxmox
from pvesnap.recovery_ui import _options
from pvesnap.recovery import Spec
proxmox = Proxmox()
gast = next(g for g in proxmox.inventory() if g.vmid == 101)
spec = Spec(mode="live")
spec.snapshot = "auto-stuendlich-20260809-100000"
spec.newid = 9103
spec.node = "pve1"
spec.transfers = ["werkzeuge"]
_options(stdscr, gast, spec)
def _vorhaben(stdscr, vmid, snapshot, **werte):
from pvesnap.curses_util import init_colors
from pvesnap.proxmox import Proxmox
from pvesnap.recovery import Spec, plan
from pvesnap.recovery_ui import _pages, summary_lines
init_colors()
proxmox = Proxmox()
gast = next(g for g in proxmox.inventory() if g.vmid == vmid)
spec = Spec(mode=werte.pop("mode", "live"))
for schluessel, wert in werte.items():
setattr(spec, schluessel, wert)
_pages(stdscr, summary_lines(plan(proxmox, gast, snapshot, spec)),
"Vorhaben pruefen", "j = anlegen | n = abbrechen")
def szene_vorhaben(stdscr):
"""Der Normalfall: abgeschottet hineinschauen, mit Arbeitsspeicher."""
_vorhaben(stdscr, 101, "auto-stuendlich-20260809-100000",
newid=9103, transfers=["werkzeuge"])
def szene_vorhaben_dongle(stdscr):
"""Die Windows-Maschine mit SPICE und USB - fuer das Schutzmodul."""
_vorhaben(stdscr, 105, "auto-taeglich-20260809-023000",
mode="live", newid=9104, transfers=["werkzeuge"], spice=True, usb=2)
def szene_laufwerkswahl(stdscr):
from pvesnap import transfer_ui
transfer_ui.choose(stdscr, preselected=["werkzeuge"])
SZENEN = {
"config": szene_config,
"recovery": szene_recovery,
"transfer": szene_transfer,
"explorer": szene_explorer,
"explorer-transfer": szene_explorer_transfer,
"gastauswahl": szene_gastauswahl,
"snapshotauswahl": szene_snapshotauswahl,
"neu": szene_neu,
"vorhaben": szene_vorhaben,
"vorhaben-dongle": szene_vorhaben_dongle,
"laufwerkswahl": szene_laufwerkswahl,
}
def main():
if len(sys.argv) < 2 or sys.argv[1] not in SZENEN:
print("Szenen: %s" % ", ".join(sorted(SZENEN)), file=sys.stderr)
return 1
_patch()
from pvesnap.curses_util import init_colors
szene = SZENEN[sys.argv[1]]
def start(stdscr):
curses.curs_set(0)
if hasattr(curses, "set_escdelay"):
curses.set_escdelay(25)
init_colors()
stdscr.keypad(True)
return szene(stdscr)
try:
curses.wrapper(start)
except Exception: # beim Bauen will man den Grund sehen
import traceback
traceback.print_exc()
return 2
return 0
if __name__ == "__main__":
sys.exit(main())