TP45 Schicht B2 Ruecknahme: VLAN30 vollstaendig auf den Stand vor dem Umzug

Zwei Christoph-Direktiven am 2026-09-21, in dieser Reihenfolge:
1. "mach die shellys sofort rueckgaengig" -- HA bindet sie per IP ein.
2. "wir sollten alle vlan30-aenderungen rueckgaengig machen" -- Sperr-Risiko
   Nuki: ein Fehlgriff haette aussperren koennen.

Neu in tp45-leases.py: die Tabellen SHELLY/REVERT und das Kommando `revert`
(nur die 26 Shellys) sowie `restore` (alles zurueck auf vor B2, inkl.
NSPanelPro-Reservierung und wiederangelegter CCU-Zeile). In gc-configplan.py
die Jobs tp45-v30-revert und tp45-v30-restore; _lease_move nimmt jetzt ein
tag, damit die :log-Zeilen den Job unterscheidbar machen.

Abnahme: Geraete-Read (alle 36 Adressen zurueck, NSPanelPro ohne Reservierung,
CCU .232 wieder da) und --refresh-compliance 145/145, 0 Drifts.

Zwei Funde, die als Merkregel bleiben: `make-dynamic` gibt es in RouterOS 7.24
nicht (der Rueckweg ist `remove`), und der GC-Plan-Status beweist nichts --
beide Laeufe meldeten Failed, waehrend der Job-:log alle 36 Statements
protokollierte.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:22:22 +02:00
parent 180c5b6c57
commit 3f774b7b17
2 changed files with 226 additions and 4 deletions

View File

@@ -49,6 +49,10 @@ Drift mehr (genau der Grund, warum `ingest-core-firewall.py` manuell bleibt).
python3 nautobot/tp45-leases.py seed --force --apply # Neuschnitt, ueberschreibt
python3 nautobot/tp45-leases.py move # Dry-Run, s. MOVES
python3 nautobot/tp45-leases.py move --apply # B2
python3 nautobot/tp45-leases.py revert # Dry-Run, s. REVERT (nur Shellys)
python3 nautobot/tp45-leases.py revert --apply # Ruecknahme 2026-09-21
python3 nautobot/tp45-leases.py restore # Dry-Run: ALLES zurueck auf vor B2
python3 nautobot/tp45-leases.py restore --apply # Vollruecknahme 2026-09-21
Token: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored), NAUTOBOT_SUPERUSER_API_TOKEN.
Der Wert wird nie ausgegeben.
@@ -116,12 +120,49 @@ MOVES = {
# .150 (Ender3V3KE) und .180 (espressif/Airfryer) bleiben, wo sie sind.
}
# ---------------------------------------------------------------------------
# RUECKNAHME 2026-09-21 (Christoph-Direktive, sofort): NUR die Shellys.
#
# Die 26 Shelly-Reservierungen gehen auf ihre ALTEN Adressen zurueck. Grund:
# Home Assistant hat sie mit den alten Adressen eingebunden -- der Umzug haette
# die HA-Integrationen zerrissen. Die uebrigen 10 Umzuege (NukiBridge, P750W,
# DTUBI, NukiSmartlock, SwitchBot, Twinkly, Lichterketten, Withings) BLEIBEN.
#
# Bewusst als eigene Menge und nicht als Bereichs-Ableitung: welche MAC eine
# Shelly ist, steht nur im Kommentar der Soll-Zeile, nicht im MOVES-Key.
# ---------------------------------------------------------------------------
SHELLY = {
"E4:B0:63:D7:C7:A4", "28:37:2F:36:F4:7C", "E4:B0:63:E4:EF:7C",
"E4:B0:63:E4:0F:30", "D8:85:AC:0C:5C:C8", "D0:CF:13:D8:18:FC",
"D0:CF:13:D9:94:E8", "E4:B0:63:FA:FD:E0", "E8:F6:0A:61:88:08",
"E4:B0:63:FC:57:E4", "E4:B0:63:FA:6C:00", "48:F6:EE:C8:E6:AC",
"48:F6:EE:DD:6A:F4", "E4:B0:63:FC:59:48", "E4:B0:63:FA:D5:34",
"10:20:BA:FA:F5:40", "10:20:BA:FB:6D:64", "48:F6:EE:CC:16:E4",
"10:20:BA:FB:70:FC", "98:A3:16:7C:5F:EC", "20:6E:F1:01:F9:F8",
"A0:85:E3:C8:65:34", "AC:15:18:7B:62:20", "14:33:5C:D8:2D:7C",
"98:A3:16:79:38:50", "98:A3:16:7A:A3:54",
}
if len(SHELLY) != 26:
raise SystemExit("SHELLY hat %d MACs statt 26 -- Tabelle beschaedigt." % len(SHELLY))
# Schluessel = MAC, Wert = (aktuelle Soll-Adresse, Adresse davor) -- also die
# Umkehrung von MOVES, aber nur fuer den Shelly-Block.
REVERT = {mac: (soll, ist) for mac, (ist, soll) in MOVES.items() if mac in SHELLY}
if len(REVERT) != 26:
raise SystemExit("REVERT hat %d Eintraege statt 26 -- MOVES/SHELLY passen nicht zusammen."
% len(REVERT))
# CCU-Leiche: VLAN30-Reservierung `pve-homematic-raspi-ccu`, status=waiting,
# last-seen=never. Die CCU ist live in VLAN20 (10.40.20.232, ARP+HTTP belegt,
# Caddys ccu.bf proxyt dorthin) — dieser Eintrag ist ein Zombie aus dem
# Core-Cutover. Er wird ENTFERNT, nicht umgezogen.
REMOVE = {"BC:24:11:8E:0A:57"}
# Die Zeile, mit der B2 die CCU-Leiche entfernt hat -- wortgleich der Zustand
# VOR B2, Quelle `routeros/tp21-core-dhcp-from-ap1.rsc` Z. 70 (dort ohne
# `disabled`). Fuer den vollstaendigen Rueckweg wird sie wieder angelegt.
RESTORE_ADD = ("add address=10.40.30.232 comment=pve-homematic-raspi-ccu "
"mac-address=BC:24:11:8E:0A:57 server=dhcp-v30")
# SONOFF-NSPanelPro hat KEINE Reservierung, nur eine dynamische Lease auf .249.
# Wird in B2 per make-static + set address gepinnt. Zeile hier nur als Soll.
ADDS = [
@@ -207,7 +248,7 @@ def show(old, new):
def main():
argv = sys.argv[1:]
if not argv or argv[0] not in ("seed", "move", "check"):
if not argv or argv[0] not in ("seed", "move", "revert", "restore", "check"):
raise SystemExit(__doc__.strip().splitlines()[-2].strip())
cmd, apply_ = argv[0], "--apply" in argv
dev = device()
@@ -232,6 +273,57 @@ def main():
new, when = backup_lines()
print("== seed aus Backup %s (%d Zeilen)%s =="
% (when, len(new), " [--force, ueberschreibt %d Zeilen]" % len(soll) if soll else ""))
elif cmd == "restore":
if not soll:
raise SystemExit("Soll ist leer — nichts wiederherzustellen.")
drop = {mac_of(a) for a in ADDS}
new, seen, moved, dropped = [], set(), [], []
for line in soll:
mac = mac_of(line)
if mac in drop:
dropped.append(mac)
continue
if mac in MOVES:
ist_ip, soll_ip = MOVES[mac]
cur = re.search(r"address=(\S+)", line).group(1)
if cur != ist_ip:
if cur != soll_ip:
raise SystemExit("%s: Soll traegt weder %s noch %s — Soll, MOVES und "
"Wirklichkeit auseinander. Zeile: %s"
% (mac, ist_ip, soll_ip, line[:120]))
line = set_address(line, ist_ip)
moved.append(mac)
seen.add(mac)
new.append(line)
fehlend = set(MOVES) - seen
if fehlend:
raise SystemExit("MOVES nennt MACs, die im Soll fehlen: %s"
% ", ".join(sorted(fehlend)))
for mac in sorted(REMOVE):
if mac not in seen:
new.append(RESTORE_ADD)
moved.append(mac)
print("== restore: %d Adressen zurueck, %d Zeilen entfernt, %d wieder angelegt "
"(Soll jetzt %d Zeilen) ==" % (len(moved), len(dropped), len(REMOVE), len(new)))
elif cmd == "revert":
if not soll:
raise SystemExit("Soll ist leer — es gibt nichts zurueckzunehmen.")
new, seen = [], set()
for line in soll:
mac = mac_of(line)
if mac in REVERT:
neu_ip, alt_ip = REVERT[mac]
if re.search(r"address=(\S+)", line).group(1) != neu_ip:
raise SystemExit("%s: Soll traegt nicht %s — Soll und REVERT auseinander. "
"Zeile: %s" % (mac, neu_ip, line[:120]))
line = set_address(line, alt_ip)
seen.add(mac)
new.append(line)
if seen != set(REVERT):
raise SystemExit("REVERT nennt MACs, die im Soll fehlen: %s"
% ", ".join(sorted(set(REVERT) - seen)))
print("== revert: %d Shelly-Reservierungen zurueck auf die alten Adressen =="
% len(REVERT))
else:
if not soll:
raise SystemExit("Soll ist leer — erst `seed` laufen lassen (Schicht B1).")