recover: -y durfte die Warnung vor dem laufenden Original wegwinken

-y soll Routinefragen sparen, nicht die eine Frage abschalten, bei der es
weh tut. Der Fall "Original laeuft und die Wiederherstellung geht mit
denselben MAC-Adressen ans Netz" ist jetzt als kritisch gefuehrt:

  -y            wird abgewiesen (Exit 2), Hinweis auf --force
  interaktiv    das Wort "ja" muss ausgeschrieben werden, "j" reicht nicht
  --force       laeuft durch
  Oberflaeche   zweite Rueckfrage, die den Grund beim Namen nennt statt
                nur "Warnungen gelesen?" zu fragen

Ausserdem wird stdout vor der Fehlerausgabe geleert - sonst stand die
Begruendung beim Umleiten ueber dem, worauf sie sich bezieht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-31 11:56:46 +02:00
co-authored by Claude Opus 5
parent 3ee5cf3508
commit dc5c7d417f
3 changed files with 57 additions and 14 deletions
+16 -6
View File
@@ -681,11 +681,20 @@ Hier bleibt alles erhalten, was die Maschine ausmacht: SMBIOS-UUID, `vmgenid`,
MAC-Adressen, bei Containern der Hostname. Für alles im Netz ist sie dieselbe
Maschine — inklusive Lizenzbindungen, AD-Mitgliedschaft und DHCP-Reservierungen.
**Genau deshalb ist sie gefährlich.** `pvesnap-recovery` prüft vorher, ob das
Original läuft, und warnt unmissverständlich. Zwei Maschinen mit derselben MAC
und derselben IP im selben Netz geben Chaos. Das Original also **vorher
stoppen** — oder mit `--net down` starten und die Karte erst zuschalten, wenn
klar ist, dass die Luft rein ist.
**Genau deshalb ist sie gefährlich.** Zwei Maschinen mit derselben MAC und
derselben IP im selben Netz geben Chaos. `pvesnap-recovery` prüft deshalb
vorher, ob das Original läuft, und lässt sich in dem Fall nicht beiläufig
durchwinken:
| Situation | Verhalten |
|---|---|
| ncurses-Oberfläche | Zusammenfassung nur mit `j`, danach eine zweite Rückfrage, die den Grund beim Namen nennt |
| `recover …` im Terminal | das Wort **`ja`** muss ausgeschrieben werden — `j` reicht nicht |
| `recover … -y` | **wird abgewiesen** (Exit-Code 2). `-y` überspringt Routinefragen, nicht diese |
| `recover … --force` | läuft durch — die bewusste Entscheidung |
Das Original also **vorher stoppen** — oder mit `--net down` starten und die
Karte erst zuschalten, wenn klar ist, dass die Luft rein ist.
### Parameter
@@ -705,7 +714,8 @@ klar ist, dass die Luft rein ist.
| `--name <name>` | Name der neuen Maschine |
| `--keep-binds` | durchgereichte Host-Verzeichnisse des Originals übernehmen |
| `--no-start` | nur einrichten, nicht starten |
| `-y`, `--yes` | nicht nachfragen |
| `-y`, `--yes` | Routinefragen überspringen (deckt `--force` **nicht** ab) |
| `--force` | auch anlegen, wenn das Original noch läuft |
| Befehl | Bedeutung |
|---|---|
+4 -1
View File
@@ -307,6 +307,9 @@ class Plan:
exchange_kind: str = "" # "disk" | "bind" | ""
exchange_detail: str = ""
warnings: list = field(default_factory=list)
# Was nicht nur unschoen, sondern gefaehrlich ist. Hierfuer genuegt ein
# beilaeufiges "ja" nicht - siehe cmd_create() und die Oberflaeche.
critical: list = field(default_factory=list)
notes: list = field(default_factory=list)
dropped: list = field(default_factory=list)
@@ -559,7 +562,7 @@ def plan(proxmox, guest, snapname, spec):
"MAC-Adressen wie das Original.")
status = proxmox.guest_status(guest)
if status.get("status") == "running":
result.warnings.append(
result.critical.append(
"%s LAEUFT gerade. Zwei Maschinen mit gleicher MAC-Adresse, "
"gleicher IP und gleicher Identitaet im selben Netz geben Chaos - "
"erst das Original stoppen." % guest.label)
+37 -7
View File
@@ -338,11 +338,10 @@ def summary_lines(plan_):
for note in plan_.notes:
for line in _wrap("- " + note, 74):
lines.append((line, curses.color_pair(C_DIM)))
if plan_.warnings:
for entry in plan_.warnings + plan_.critical:
lines.append(("", 0))
for warning in plan_.warnings:
for line in _wrap("!! " + warning, 74):
lines.append((line, curses.color_pair(C_WARN) | curses.A_BOLD))
for line in _wrap("!! " + entry, 74):
lines.append((line, curses.color_pair(C_WARN) | curses.A_BOLD))
return lines
@@ -396,7 +395,13 @@ def _new(stdscr, proxmox):
"j = anlegen | beliebige andere Taste = zurueck")
if key not in ("j", "J", "y", "Y"):
continue
if plan_.warnings and not confirm(
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
@@ -694,6 +699,8 @@ def _print_summary(plan_):
print(" Hinweis : %s" % note)
for warning in plan_.warnings:
print(" ACHTUNG : %s" % warning)
for reason in plan_.critical:
print(" GEFAHR : %s" % reason)
def cmd_create(proxmox, args, mode):
@@ -704,7 +711,26 @@ def cmd_create(proxmox, args, mode):
print("Vorhaben:")
_print_summary(plan_)
if not args.yes:
# 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 input("Trotzdem anlegen? [ja/nein] ").strip().lower() 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()
@@ -834,7 +860,11 @@ def _add_create_options(parser):
"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="nicht nachfragen")
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():