recovery: flatten, cleanup und eine Platzpruefung, die weh getan hat
flatten Linked Clones haengen fuer immer am Quell-Snapshot: der wird unloeschbar, und die Original-Platte muss bestehen bleiben. Fuer "mal reinschauen" egal, fuer eine dauerhaft uebernommene Maschine nicht. `pvesnap-recovery flatten` loest sie ueber `rbd flatten`, hebt danach den Schutz auf und meldet die Maschine als eigenstaendig. In der Oberflaeche Taste f; die Detailansicht zeigt an, ob eine Instanz noch verlinkt ist. Vorher wird gemessen, wieviel kopiert werden muss - `rbd du` zeigt je Snapshot nur den Zuwachs, kopiert wird aber der gesamte sichtbare Inhalt. Im Test: Snapshot-Zeile 56 MiB, tatsaechlicher Bedarf ueber 8 GB. Und es wird geprueft, ob der Platz reicht. Der Grund ist Erfahrung: laeuft ein Ceph-Pool beim Kopieren voll, blockiert er alle Schreibvorgaenge. Dann steht jede VM auf dem Storage - und Aufraeumen geht auch nicht mehr, weil Loeschen ebenfalls ein Schreibvorgang ist. cleanup Wird eine Wiederherstellung in der Proxmox-Oberflaeche geloescht statt mit destroy, bleiben Klon und Snapshot-Schutz liegen. `cleanup` findet das. Als Wahrheit ueber vergebene VMIDs dienen die Konfigurationsdateien unter /etc/pve/nodes/* - nicht /cluster/resources: dort fehlt ein Gast auf einem abgemeldeten Node moeglicherweise, und dann wuerden wir die Platten einer lebenden Maschine loeschen. Ohne Elternteil wird nichts angefasst, und der Schutz faellt nur, wenn kein Klon mehr daran haengt. Nebenbei: Abfragen antworten bei fehlender Eingabe mit "nein", statt einen Stacktrace zu werfen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dc5c7d417f
commit
3a47bb0621
+155
-7
@@ -27,8 +27,9 @@ from .curses_util import (C_DIM, C_FOOTER, C_HEADER, C_MARK, C_OK, C_SEL, C_WARN
|
||||
keybar, message, prompt, put, read_key)
|
||||
from .proxmox import Proxmox, ProxmoxError, pvesh_available
|
||||
from .recovery import (EXCHANGE_GUEST_PATH, MKFS, RecoveryError, Spec, create,
|
||||
destroy, human_bytes, list_instances, plan, pull, release,
|
||||
start)
|
||||
destroy, find_orphans, flatten, flatten_cost, human_bytes,
|
||||
linked_volumes, list_instances, plan, pull, release,
|
||||
remove_orphans, start)
|
||||
from .snapfs import list_snapshots
|
||||
from .util import truncate
|
||||
|
||||
@@ -474,8 +475,25 @@ def _details(stdscr, proxmox, instance):
|
||||
"ersten Start lief die Maschine genau weiter.",
|
||||
curses.color_pair(C_DIM)))
|
||||
|
||||
footer = ("s Starten | h Herunterfahren | a Austausch | x Verwerfen | "
|
||||
"q zurueck")
|
||||
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 Starten | h Herunterfahren | a Austausch | f Loesen | "
|
||||
"x Verwerfen | q zurueck")
|
||||
key = _pages(stdscr, lines, "Wiederherstellung %s" % instance.label, footer)
|
||||
|
||||
if key in ("q", "\x1b") or is_escape(key):
|
||||
@@ -488,6 +506,8 @@ def _details(stdscr, proxmox, instance):
|
||||
lambda: proxmox.shutdown_guest(instance.guest))
|
||||
elif key in ("a", "A"):
|
||||
_pull(stdscr, proxmox, instance)
|
||||
elif key in ("f", "F"):
|
||||
_flatten(stdscr, proxmox, instance, linked)
|
||||
elif key in ("x", "X"):
|
||||
if _destroy(stdscr, proxmox, instance):
|
||||
return
|
||||
@@ -524,6 +544,34 @@ def _pull(stdscr, proxmox, instance):
|
||||
]), "Austauschlaufwerk")
|
||||
|
||||
|
||||
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), ""]
|
||||
lines += ["nicht behandelt (kein Ceph): %s" % volid for volid in other]
|
||||
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):
|
||||
@@ -703,6 +751,19 @@ def _print_summary(plan_):
|
||||
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)
|
||||
@@ -727,13 +788,13 @@ def cmd_create(proxmox, args, mode):
|
||||
"bewusst --force setzen.", file=sys.stderr)
|
||||
return 2
|
||||
print("\nWer das trotzdem will, tippt jetzt das Wort ja aus.")
|
||||
if input("Trotzdem anlegen? [ja/nein] ").strip().lower() not in ("ja", "yes"):
|
||||
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 = input("\nAnlegen? [j/N] ").strip().lower()
|
||||
answer = _ask("\nAnlegen? [j/N] ")
|
||||
if answer not in ("j", "y", "ja", "yes"):
|
||||
print("Abgebrochen.")
|
||||
return 1
|
||||
@@ -784,7 +845,7 @@ def cmd_destroy(proxmox, args):
|
||||
if not args.yes:
|
||||
print("%s (%s) wird restlos entfernt - alle Aenderungen darin sind weg."
|
||||
% (instance.label, instance.origin))
|
||||
if input("Wirklich? [j/N] ").strip().lower() not in ("j", "y", "ja", "yes"):
|
||||
if _ask("Wirklich? [j/N] ") not in ("j", "y", "ja", "yes"):
|
||||
print("Abgebrochen.")
|
||||
return 1
|
||||
destroy(proxmox, instance, progress=lambda text: print(" " + text))
|
||||
@@ -803,6 +864,78 @@ def cmd_pull(proxmox, args):
|
||||
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(" nicht behandelt (kein Ceph-Datentraeger): %s" % 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_release(_proxmox, args):
|
||||
print("geloest." if release(args.vmid) else "War nicht eingehaengt.")
|
||||
return 0
|
||||
@@ -924,6 +1057,19 @@ def build_parser():
|
||||
release_ = subs.add_parser("release", help="Austauschlaufwerk wieder loesen")
|
||||
release_.add_argument("vmid", type=int)
|
||||
|
||||
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")
|
||||
@@ -952,6 +1098,8 @@ def main(argv=None):
|
||||
"destroy": lambda: cmd_destroy(proxmox, args),
|
||||
"pull": lambda: cmd_pull(proxmox, args),
|
||||
"release": lambda: cmd_release(proxmox, args),
|
||||
"flatten": lambda: cmd_flatten(proxmox, args),
|
||||
"cleanup": lambda: cmd_cleanup(proxmox, args),
|
||||
"console": lambda: cmd_console(proxmox, args),
|
||||
"start": lambda: cmd_start(proxmox, args),
|
||||
"stop": lambda: cmd_stop(proxmox, args),
|
||||
|
||||
Reference in New Issue
Block a user