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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c484596702
commit
59e7224297
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Attrappe der Storage-Schicht.
|
||||
|
||||
pvesnap ruft `perl -e "<Skript>" <op> <args>` auf, um an PVE::Storage zu
|
||||
kommen. Hier wird nur der Teil beantwortet, den die Bildschirmfotos brauchen -
|
||||
Groessen und Pfade. Nichts wird angelegt oder geloescht.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Groessen der Datentraeger in der Kulisse, in Byte.
|
||||
GROESSEN = {
|
||||
"vm-100-disk-0": 60, "vm-101-disk-0": 400, "vm-102-disk-0": 2048,
|
||||
"vm-105-disk-0": 250, "vm-130-disk-0": 80, "vm-120-disk-0": 120,
|
||||
"subvol-110-disk-0": 16, "subvol-111-disk-0": 64,
|
||||
"vm-9101-disk-0": 400, "vm-9102-disk-0": 250,
|
||||
}
|
||||
RAM = 32 * 1024 ** 3 # der gespeicherte Arbeitsspeicher von db01
|
||||
|
||||
|
||||
def main(argv):
|
||||
# argv: -e <skript> <op> <rest...>
|
||||
rest = argv[2:] if len(argv) > 2 and argv[0] == "-e" else argv
|
||||
if not rest:
|
||||
return 1
|
||||
op, args = rest[0], rest[1:]
|
||||
|
||||
if op == "size":
|
||||
volid = args[0] if args else ""
|
||||
name = volid.split(":", 1)[-1]
|
||||
if "-state-" in name:
|
||||
print("RESULT %d raw" % RAM)
|
||||
else:
|
||||
gigabyte = GROESSEN.get(name, 32)
|
||||
print("RESULT %d raw" % (gigabyte * 1024 ** 3))
|
||||
return 0
|
||||
|
||||
if op == "path":
|
||||
volid = args[0] if args else ""
|
||||
storage, _, name = volid.partition(":")
|
||||
print("RESULT /dev/rbd/%s/%s" % (storage, name))
|
||||
return 0
|
||||
|
||||
if op in ("activate", "deactivate", "free"):
|
||||
print("RESULT ok")
|
||||
return 0
|
||||
|
||||
if op == "clone":
|
||||
volid = args[0] if args else ""
|
||||
storage = volid.split(":", 1)[0]
|
||||
print("RESULT %s:vm-%s-disk-0" % (storage, args[1] if len(args) > 1 else "0"))
|
||||
return 0
|
||||
|
||||
if op == "alloc":
|
||||
print("RESULT %s:vm-%s-disk-9" % (args[0], args[1]))
|
||||
return 0
|
||||
|
||||
print("unbekannte Operation: %s" % op, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Attrappe von `pvesh` - beantwortet Abfragen aus demo/state.json.
|
||||
|
||||
Nur so viel, wie die Oberflaechen fuer die Bildschirmfotos brauchen. Alles,
|
||||
was etwas veraendern wuerde, gibt brav eine UPID zurueck und tut nichts.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
STATE = os.environ.get("PVESNAP_DEMO_STATE", "")
|
||||
|
||||
|
||||
def load():
|
||||
with open(STATE, "r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
return 1
|
||||
verb, path = argv[0], argv[1]
|
||||
args = {}
|
||||
rest = argv[2:]
|
||||
index = 0
|
||||
while index < len(rest):
|
||||
if rest[index].startswith("--"):
|
||||
key = rest[index][2:]
|
||||
value = rest[index + 1] if index + 1 < len(rest) else ""
|
||||
args[key] = value
|
||||
index += 2
|
||||
else:
|
||||
index += 1
|
||||
|
||||
data = load()
|
||||
parts = [p for p in path.strip("/").split("/") if p]
|
||||
|
||||
if verb == "get":
|
||||
result = handle_get(parts, args, data)
|
||||
if result is _MISS:
|
||||
print("no such resource '%s'" % path, file=sys.stderr)
|
||||
return 2
|
||||
if args.get("output-format") == "json":
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(result)
|
||||
return 0
|
||||
|
||||
# Alles Veraendernde: Proxmox liefert eine UPID, der Aufrufer wartet darauf.
|
||||
node = parts[1] if len(parts) > 1 and parts[0] == "nodes" else "pve1"
|
||||
if parts[-1] == "spiceproxy":
|
||||
print(json.dumps({
|
||||
"type": "spice", "host": "pvespiceproxy:63f4a2b1:100:pve1::abc",
|
||||
"proxy": "http://10.20.0.11", "tls-port": 61000,
|
||||
"password": "aG9jaGdlaGVpbQ==", "ca": "-----BEGIN CERTIFICATE-----\\n"
|
||||
"MIIFxTCCA62gAwIBAgIB...\\n-----END CERTIFICATE-----\\n",
|
||||
"host-subject": "OU=PVE Cluster Node,O=Proxmox Virtual Environment,"
|
||||
"CN=pve1.hausnetz.lan",
|
||||
"delete-this-file": 1, "secure-attention": "Ctrl+Alt+Ins",
|
||||
"toggle-fullscreen": "Shift+F11", "release-cursor": "Ctrl+Alt+R",
|
||||
"title": "VM 9102 - warenwirtschaft-w",
|
||||
}))
|
||||
return 0
|
||||
print("UPID:%s:00001A2B:0BC3D4E5:68970000:qmsnapshot:100:root@pam:" % node)
|
||||
return 0
|
||||
|
||||
|
||||
_MISS = object()
|
||||
|
||||
|
||||
def handle_get(parts, args, data):
|
||||
if parts == ["cluster", "resources"]:
|
||||
return data["resources"]
|
||||
if len(parts) == 2 and parts[0] == "storage":
|
||||
return {"ceph-vm": {"type": "rbd", "pool": "vmdaten", "shared": 1,
|
||||
"content": "images", "storage": "ceph-vm",
|
||||
"krbd": 0, "monhost": "10.20.0.11 10.20.0.12"},
|
||||
"ceph-ct": {"type": "rbd", "pool": "ctdaten", "shared": 1,
|
||||
"content": "rootdir", "storage": "ceph-ct"},
|
||||
"local": {"type": "dir", "shared": 0, "storage": "local",
|
||||
"content": "iso,vztmpl,backup"},
|
||||
}.get(parts[1], _MISS)
|
||||
if parts == ["storage"]:
|
||||
return [{"storage": "ceph-vm", "type": "rbd", "shared": 1},
|
||||
{"storage": "ceph-ct", "type": "rbd", "shared": 1},
|
||||
{"storage": "local", "type": "dir", "shared": 0}]
|
||||
if parts == ["cluster", "nextid"]:
|
||||
wanted = args.get("vmid")
|
||||
if wanted:
|
||||
taken = {str(r["vmid"]) for r in data["resources"]}
|
||||
if str(wanted) in taken:
|
||||
print("VM %s already exists" % wanted, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
return int(wanted)
|
||||
return data["nextid"]
|
||||
|
||||
# Muss vor dem allgemeinen /nodes-Zweig stehen: sonst wird "tasks" fuer
|
||||
# eine Gastart gehalten und /status liefert einen leeren Datensatz - die
|
||||
# Warteschleife auf den Task laeuft dann bis zum Zeitlimit.
|
||||
if len(parts) >= 3 and parts[0] == "nodes" and parts[2] == "tasks":
|
||||
if parts[-1] == "log":
|
||||
return [{"n": 1, "t": "TASK OK"}]
|
||||
return {"status": "stopped", "exitstatus": "OK", "type": "qmconfig",
|
||||
"upid": parts[3] if len(parts) > 3 else "", "node": parts[1]}
|
||||
|
||||
if len(parts) >= 4 and parts[0] == "nodes":
|
||||
node, kind, vmid = parts[1], parts[2], parts[3]
|
||||
key = "%s/%s/%s" % (node, kind, vmid)
|
||||
tail = parts[4:]
|
||||
|
||||
if tail == ["snapshot"]:
|
||||
entries = list(data["snapshots"].get(key, []))
|
||||
entries.append({"name": "current", "digest": "0" * 40,
|
||||
"description": "You are here!",
|
||||
"parent": entries[-1]["name"] if entries else ""})
|
||||
return entries
|
||||
if tail == ["config"]:
|
||||
return _conf(data["configs"].get(key, ""))
|
||||
if len(tail) == 3 and tail[0] == "snapshot" and tail[2] == "config":
|
||||
base = _conf(data["configs"].get(key, ""))
|
||||
base["parent"] = tail[1]
|
||||
base["snaptime"] = next(
|
||||
(s["snaptime"] for s in data["snapshots"].get(key, [])
|
||||
if s["name"] == tail[1]), 0)
|
||||
# Ein Snapshot "mit RAM" traegt den Arbeitsspeicher als eigenes
|
||||
# Volume; genau daran erkennt pvesnap, dass es warm gehen kann.
|
||||
if tail[1].startswith("auto-stuendlich"):
|
||||
base["vmstate"] = "ceph-vm:vm-%s-state-%s" % (vmid, tail[1])
|
||||
base["runningmachine"] = "pc-i440fx-9.0+pve0"
|
||||
base["runningcpu"] = "x86-64-v2-AES,enforce"
|
||||
return base
|
||||
if tail == ["status", "current"]:
|
||||
return data["status"].get(key, {})
|
||||
if tail and tail[0] == "status":
|
||||
return data["status"].get(key, {})
|
||||
|
||||
return _MISS
|
||||
|
||||
|
||||
def _conf(text):
|
||||
result = {}
|
||||
for line in text.splitlines():
|
||||
if ":" in line and not line.startswith("#"):
|
||||
key, _, value = line.partition(":")
|
||||
result[key.strip()] = value.strip()
|
||||
for key in ("cores", "memory", "sockets", "numa", "agent"):
|
||||
if key in result:
|
||||
try:
|
||||
result[key] = int(result[key])
|
||||
except ValueError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Attrappe von `rbd` - nur lesende Auskuenfte fuer die Bildschirmfotos.
|
||||
|
||||
VM 9101 ist ein Linked Clone (haengt am Quell-Snapshot), VM 9102 ist bereits
|
||||
geloest. Genau diese beiden Zustaende soll das Handbuch zeigen.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
VERBUNDEN = "vm-9101-disk-0" # haengt noch am Snapshot
|
||||
QUELLE = "vmdaten/vm-101-disk-0@auto-stuendlich-20260809-100000"
|
||||
|
||||
|
||||
# Optionen, hinter denen noch ein Wert steht - sonst haelt man "-m <monhost>"
|
||||
# fuer den Befehl.
|
||||
MIT_WERT = {"-m", "--id", "--keyring", "-c", "--conf", "-p", "--pool",
|
||||
"--namespace", "-n", "--name"}
|
||||
|
||||
|
||||
def zerlege(argv):
|
||||
"""(Befehl, Abbild) aus der Kommandozeile."""
|
||||
rest = []
|
||||
index = 0
|
||||
while index < len(argv):
|
||||
wort = argv[index]
|
||||
if wort in MIT_WERT:
|
||||
index += 2
|
||||
continue
|
||||
if wort.startswith("-"):
|
||||
index += 1
|
||||
continue
|
||||
rest.append(wort)
|
||||
index += 1
|
||||
return (rest[0] if rest else ""), (rest[1] if len(rest) > 1 else "")
|
||||
|
||||
|
||||
def main(argv):
|
||||
if not argv:
|
||||
return 1
|
||||
befehl, ziel = zerlege(argv)
|
||||
name = ziel.split("/")[-1].split("@")[0]
|
||||
|
||||
if befehl == "info":
|
||||
print("rbd image '%s':" % name)
|
||||
print("\tsize 400 GiB in 102400 objects")
|
||||
print("\torder 22 (4 MiB objects)")
|
||||
print("\tsnapshot_count: 0")
|
||||
print("\tid: 1f5c3d9a7b2e")
|
||||
print("\tblock_name_prefix: rbd_data.1f5c3d9a7b2e")
|
||||
print("\tformat: 2")
|
||||
print("\tfeatures: layering, exclusive-lock")
|
||||
print("\top_features:")
|
||||
print("\tflags:")
|
||||
if name == VERBUNDEN:
|
||||
print("\tparent: %s" % QUELLE)
|
||||
print("\toverlap: 400 GiB")
|
||||
return 0
|
||||
|
||||
if befehl == "du":
|
||||
print("NAME PROVISIONED USED")
|
||||
print("%-19s 400 GiB 2.4 GiB" % name)
|
||||
return 0
|
||||
|
||||
if befehl in ("flatten", "snap"):
|
||||
return 0
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user