Zwei Luecken, die seit dem Umbau offenstanden.
1. Transfer-Laufwerke waren aus dem Explorer nicht erreichbar
F2 zeigte nur die Dateisysteme des Snapshots. Jetzt stehen die
Transfer-Laufwerke in derselben Liste - aus Sicht des Bedieners ist es
dieselbe Frage ("wo soll ich hinschauen?"), und der Weg ueber
pvesnap-recovery entfaellt.
Das gewaehlte Laufwerk landet im rechten Fenster, links bleibt der
Snapshot. Damit laesst sich direkt aus einem Snapshot auf das
Austauschmedium kopieren, das anschliessend in die wiederhergestellte
Maschine wandert - ohne Zwischenlager auf dem Host.
Ausgehaengt wird beim Verlassen, und zwar nur, was wir selbst eingehaengt
haben: ein Laufwerk, das schon vorher am Host hing, gehoert jemand
anderem. Das finally faengt auch Absturz und Strg-C ab - bliebe es
eingehaengt, gaelte es spaeter als belegt.
2. cleanup fand geschuetzte Snapshots ohne Klon nicht
Loescht jemand eine Wiederherstellung in der Proxmox-Oberflaeche, raeumt
Proxmox den Klon durchaus mit ab - aber nicht den Schutz seines
Quell-Snapshots (rbd snap protect, den setzt es beim Klonen selbst).
Zurueck bleibt ein geschuetzter Snapshot ohne Klon: die Vorhaltezeit
scheitert an ihm jede Nacht mit "snapshot is protected", und
find_orphans() findet prinzipiell nichts, weil es den Datentraeger, nach
dem es sucht, wirklich nicht mehr gibt. Dasselbe entsteht nach einem von
Hand ausgefuehrten rbd flatten.
find_stale_protections() sucht deshalb direkt nach der Ursache: jeder
geschuetzte Snapshot, an dem kein Klon haengt. Ein solcher Schutz hat
keinen Zweck - er existiert einzig dafuer, dass ein Klon seine Grundlage
behaelt. Geloest wird nur der Schutz, geloescht wird nichts. Vor jedem
Loesen wird ein zweites Mal nachgesehen, ob inzwischen doch ein Klon
daran haengt.
cleanup laeuft damit in zwei Durchgaengen und liegt neu auch in der
Oberflaeche auf Taste c - man sucht sonst lange nach einem Befehl, den
man nur im Notfall braucht.
Getestet gegen die Kulisse der Handbuch-Werkstatt: die rbd-Attrappe
kennt jetzt snap ls und children und enthaelt beide Zustaende
nebeneinander - ein geschuetzter Snapshot mit Klon (muss in Ruhe
gelassen werden) und einer ohne (muss gefunden werden).
Handbuch und README nachgezogen, zwei Bildschirmfotos dazu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
164 lines
6.3 KiB
Python
Executable File
164 lines
6.3 KiB
Python
Executable File
#!/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"]:
|
|
# Die Liste traegt dieselben Felder wie der Einzelabruf - besonders
|
|
# "pool". Ohne das wuerde jeder rbd-Aufruf im Standard-Pool landen.
|
|
return [{"storage": "ceph-vm", "type": "rbd", "shared": 1,
|
|
"pool": "vmdaten", "content": "images",
|
|
"monhost": "10.20.0.11 10.20.0.12"},
|
|
{"storage": "ceph-ct", "type": "rbd", "shared": 1,
|
|
"pool": "ctdaten", "content": "rootdir"},
|
|
{"storage": "local", "type": "dir", "shared": 0,
|
|
"content": "iso,vztmpl,backup"}]
|
|
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:]))
|