From dfec117e6b6e4ff212598cf3d7e5a0919fc64f77 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Wed, 22 Jul 2026 21:58:12 +0200 Subject: [PATCH] TP15: Portfakten + Governance-Ausnahme CRS326-Bring-up + Handoff-Prompt Portplan-Widerspruch aufgeloest (User): Core #15 = ap1 (Ziel kuenftig #4), #11 = cudy1. Damit gilt die TP4-Variante (gi16 Arris -> gi15 Trunk -> ap1); 03-trunk-ap-ports.cli (gi1 = ap1) ist STALE, vom Edge-Cutover 2026-07-01 ueberholt. gi16/gi8 bleiben unverifiziert und sind in Nautobot als solche markiert. Kernrisiko dokumentiert: crs326-core ersetzt sw1 UND wird L3-Router, an sw1 haengt ap1 (#15) mit Gateway/DHCP/DNS/WAN-Trunk -> Core-Tausch und L3-Umzug sind dasselbe Ereignis und bedeuten ein Vollausfall-Fenster. Entscheid: Branch zuerst, Core eigene Session. Rettungspfade liegen guenstig getrennt (Notebook-WLAN via cudy1 am Core #11, Kabel-Plan-B am sw2/Branch P4). Governance-Ausnahme #2 erteilt (User): CRS326-Grundconfig direkt via MikroMCP/Winbox (Henne-Ei - GC kann nicht auf ein Geraet ohne IP pushen, Templates fehlen). Auflage: Ist-Stand danach als intended config nach Nautobot. Gilt nicht fuer ap1/ap2. Nautobot: cudy1/cudy2 als Cudy WR3600E V1.0 (Firmware 2.3.15-20250905-191956), cudy1 -> Technik UG, cudy2 -> Technik OG/Decommissioning; Platzhalter-DeviceType entfernt; sw1 gi8/gi11/gi15/gi16; crs326-core ether4 als Zielport fuer ap1. Alle 8 Devices haben jetzt einen Bereich. Neu: docs/tp15-crs326-session-handoff-prompt.md - kopierfertiger Auftrag fuer die naechste Session (beide CRS326 bring-up, Portplan, NUR Branch-Tausch). Co-Authored-By: Claude Opus 4.8 --- docs/teilprojekte.md | 12 +- docs/tp15-crs326-session-handoff-prompt.md | 126 +++++++++++++++++++ nautobot/ingest-2026-07-22-restdaten.py | 133 +++++++++++++++++++++ 3 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 docs/tp15-crs326-session-handoff-prompt.md create mode 100644 nautobot/ingest-2026-07-22-restdaten.py diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 6ebf078..4c1c1cb 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -29,6 +29,7 @@ Kontrolle: MikroMCP (MikroTik) live · ALLNET = Klickanleitungen · Omada → Co > Folge: **native RouterOS-Features bevorzugen** (z.B. Built-in-mDNS statt Container); Container (mDNS-Reflector, BIND9-DNS, Omada-Controller) laufen auf **NAS/pve**, nicht am hAP. > 🔒 **Governance-Entscheid (User, 2026-07-16, ausgeweitet 2026-07-16): Config ausschließlich über Nautobot — keine Direkt-Changes mehr an einzelnen im Netz verstreuten Geräten.** Ursprünglich als ap1/ap2-Config-Freeze bis TP21 formuliert, noch am selben Tag verallgemeinert: **jede** Netz-Gerätekonfiguration (Router, APs, künftig auch die CRS326-Switches) läuft ab jetzt exklusiv über Nautobot Golden Config (TP21), sobald das steht — keine ad-hoc MikroMCP-Writes/UI-Klicks mehr an Einzelgeräten. **TP19.1 war die letzte Ausnahme** (ap1-DNS-Fix + ap2/sw1/sw2-Mgmt-VLAN-Umzug, s.u.) — davon ist nur der risikolose ap1-Teil noch direkt gemacht worden, ap2 wurde bewusst auf „nach Nautobot" vertagt. **Macht Nautobot + alle Prerequisites (TP18→TP20→TP20.1→TP21, jetzt auch Komodo) zum aktuell kritischen Pfad**, vor TP5/TP6. Betrifft nicht Software-Migrationen auf NAS/pve (anderes Substrat) und die Frage, ob sw1/sw2 vor der CRS326-Ablösung noch angefasst werden (offen, s. TP15/TP19.1). > ✅ **Genehmigte Ausnahme (User, 2026-07-17): der zentrale CF-Tunnel-Zugang auf ap1 (TP16/TP16.1).** Alle für den zentralen Cloudflare-Zugang **auf ap1** notwendigen (Vor-)Arbeiten dürfen direkt via MikroMCP/CF-API gemacht werden, ohne auf Nautobot GC zu warten — konkret die nativen WireGuard-Mesh-Zeilen aus TP16.1 (und analoge cloudflared/WG-Anpassungen am ap1-Edge). Begründung: Remote-Erreichbarkeit ist die Voraussetzung, um überhaupt sicher am Netz zu arbeiten (Henne-Ei zu Nautobot). **Scope-Grenze:** Die Ausnahme gilt **nur für ap1**. Sobald der zentrale Zugang später auf die **CRS326** wandert (TP15), gilt die Ausnahme dafür **vorerst NICHT** — CRS326-Config läuft dann regulär über Nautobot GC (oder braucht eine eigene, dann neu zu erteilende Ausnahme). Nachtrag der ap1-Ausnahmezeilen in die GC als Follow-up, sobald Nautobot steht. +> ✅ **Genehmigte Ausnahme #2 (User, 2026-07-22): der CRS326-Bring-up (TP15).** Die **Grundkonfiguration** beider CRS326 — Management-IP, Bridge, VLAN-Filtering, Trunks/Access-Ports — darf **direkt** via MikroMCP/Winbox gemacht werden. **Begründung = Henne-Ei:** Golden Config kann nicht auf ein Gerät pushen, das im Werkszustand keine erreichbare IP hat, und die GC-Templates existieren noch nicht (TP21 „offen: Templates/intended config"). **Auflage:** der so entstandene Ist-Stand wird anschließend als **intended config nach Nautobot zurückgeschrieben**, danach übernimmt GC — die Ausnahme deckt das Bootstrapping, nicht den Dauerbetrieb. **Scope-Grenze:** gilt für die beiden CRS326 im Bring-up. Sie hebt die Regel für ap1/ap2 **nicht** auf (deren Neuprovisionierung läuft weiterhin über GC) und verlängert sich nicht automatisch auf spätere CRS326-Änderungen. Damit ist die frühere Formulierung in TP16.1 („CRS326-Config läuft regulär über Nautobot GC oder braucht eine eigene, dann neu zu erteilende Ausnahme") **erfüllt** — dies ist diese Ausnahme. --- @@ -362,11 +363,16 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". | Bereich | heute | wird abgelöst durch | |---|---|---| | **Technik UG** (virtueller Bereich in der Diele, **kein Keller**) | sw1 (ALLNET, 16 Ports) | **crs326-core** (= neuer L3-Router) | - | **Technik OG** | sw2 (ES208G, 8 Ports) + ap2 | **crs326-branch** | + | **Technik OG** | sw2 (ES208G, 8 Ports) + ap2 + cudy2 | **crs326-branch** | | **Wohnzimmer Sideboard** | ap1 | — (bleibt, wird reiner AP) | - Kapazität unkritisch: 16+8 belegte Ports heute gegen 2× 24 GbE + 2× SFP+ künftig. In Nautobot gepflegt. `cudy1` hat noch keinen Bereich. + Kapazität unkritisch: 16+8 belegte Ports heute gegen 2× 24 GbE + 2× SFP+ künftig. Vollständig in Nautobot gepflegt (alle 8 Devices haben einen Bereich). +- **📻 cudy-Hardware (User 2026-07-22):** beide sind **Cudy WR3600E V1.0**, Firmware `2.3.15-20250905-191956`. `cudy1` = Technik UG, am Core `#11`, aktiv als Übergangs-WLAN (`elektrosmog.c`, `10.40.10.246`) — **und Christophs aktueller WLAN-Pfad**. `cudy2` = Technik OG, außer Betrieb (Nautobot-Status `Decommissioning`), wird nicht reaktiviert. - ⚠️ **Etappe 1 (Erstzugriff auf die CRS326) ist NICHT kritisch** (User 2026-07-22): Werkszustand, Gerät steht griffbereit neben ihm → der IP-Konflikt auf `192.168.88.1` wird physisch/seriell aufgelöst, kein Lockout-Szenario. -- 🚧 **Blocker für den Portplan: sw1s Ist-Belegung ist nirgends verlässlich erfasst.** `switch/state-backups/sw1-running-config-20260716-*.txt` sind 214-Byte-Fehlschläge (`Incorrect User Name!!` — toter `claudecode`-Login), `switch/cli/running-PRE.txt` ist der Stand **vor** TP1. Aus den angewendeten CLI-Dateien + Doku ergibt sich ein **Widerspruch**: `03-trunk-ap-ports.cli` sagt **gi1 = ap1 / gi2 = ap2**, der WAN-Cutover in TP4 sagt **gi16 = Arris Access VLAN100 → gi15 Trunk → ap1 ether2**. Ungeklärt, ob ap1 von gi1 auf gi15 umgezogen ist oder gi15 ein zweiter Link ist. **Muss vom Gerät gelesen werden** (User macht sw1 ohnehin selbst im UI/CLI), bevor der CRS326-Portplan entstehen kann. +- ✅ **Portplan-Widerspruch AUFGELÖST (User 2026-07-22): am Core ist `#15 = ap1` und `#11 = cudy1`.** Damit gilt die TP4-Variante (`gi16` Arris/VLAN100 → `gi15` Trunk → ap1 `ether2`); **`switch/cli/03-trunk-ap-ports.cli` (gi1 = ap1 / gi2 = ap2) ist STALE** — die Datei bezeichnet sich selbst als „provisorisch, 2026-06-09" und wurde vom Edge-Cutover am 2026-07-01 überholt. Nicht mehr als Quelle verwenden. + - **Zielport nach dem Tausch: ap1 wandert auf `crs326-core` Port 4** (User-Vorgabe). Gleichzeitig wandert ap1s Uplink geräteseitig von `ether2` auf `ether1` (PoE-Port, s.o.). + - ⚠️ **Nach wie vor NICHT vom Gerät gelesen:** die restliche sw1-Belegung. `switch/state-backups/sw1-running-config-20260716-*.txt` sind 214-Byte-Fehlschläge (`Incorrect User Name!!`, toter `claudecode`-Login), `switch/cli/running-PRE.txt` ist Pre-TP1. `gi16` (Arris) und `gi8` (Trunk → sw2) stehen in Nautobot daher **explizit als „UNVERIFIZIERT"** markiert. +- 🔴 **Betriebliches Kernrisiko (Analyse 2026-07-22): der Core-Tausch ist KEIN Switch-Tausch, sondern ein Vollausfall.** `crs326-core` ersetzt sw1 **und** wird gleichzeitig L3-Router — und an sw1 hängt auf `#15` ap1, also Gateway, DHCP, DNS und der WAN-Trunk fürs gesamte Netz. Core-Tausch und L3-Umzug sind damit **faktisch dasselbe Ereignis** und brauchen ein geplantes Ausfallfenster. **→ Reihenfolge-Entscheid (User 2026-07-22): `crs326-branch` zuerst (gegen sw2, Technik OG), `crs326-core` in einer eigenen, späteren Session.** +- 🛟 **Rettungspfade liegen günstig auf verschiedenen Switches:** Christophs Notebook hängt per **WLAN über cudy1 → Core `#11`**; der kabelgebundene Plan B (Dock/Monitor Arbeitszimmer) hängt am **sw2/Branch (P4)**. Beim **Branch**-Tausch stirbt also der Kabelweg, das WLAN überlebt. (Beim Core-Tausch stirbt beides — s. Kernrisiko oben.) - **Hinweis:** die A/B/C/D-Switch-Varianten in `hw-refresh-requirements.md` sind durch die CRS326-Beschaffung überholt (Doc = Recherche-Archiv + USV/Energie-Referenz). - Abh.: TP14 ✅ (IP-Migration). Verzahnt: **TP20** (Platform-DB), **TP21** (Nautobot GC), **TP2.1** (sw2-Standalone-Recovery). diff --git a/docs/tp15-crs326-session-handoff-prompt.md b/docs/tp15-crs326-session-handoff-prompt.md new file mode 100644 index 0000000..c1dc860 --- /dev/null +++ b/docs/tp15-crs326-session-handoff-prompt.md @@ -0,0 +1,126 @@ +# TP15 — Handoff-Prompt: CRS326-Bring-up + Branch-Tausch (neue Session) + +> **Zweck:** Kopierfertiger Auftrag für eine frische Session, die die beiden CRS326 in Betrieb +> nimmt und **crs326-branch** produktiv gegen sw2 tauscht. Der Core-Tausch (= gleichzeitig der +> L3-Umzug) ist **bewusst NICHT** Teil dieses Auftrags. +> +> **Empfohlenes Modell:** Opus. Nicht wegen der Befehle — die sind Standard-RouterOS — sondern +> weil in der Session laufend zwischen „Ist-Zustand am Gerät", „Modell in Nautobot" und +> „Zielbild" abgeglichen werden muss und die sw1-Belegung nur teilweise verifiziert ist. +> +> **So übergibst du es:** Neue Session öffnen, als ersten Prompt schicken: +> „Lies `docs/tp15-crs326-session-handoff-prompt.md` und arbeite den Auftrag darin ab." + +--- PROMPT --- + +``` +TP15 — CRS326 in Betrieb nehmen: beide grundkonfigurieren, Portplan in Nautobot +modellieren, danach NUR crs326-branch produktiv gegen sw2 tauschen. + +KONTEXT ZUERST LESEN (in dieser Reihenfolge): +1. docs/teilprojekte.md — TP15 (Hardware/Standorte/Portfakten/Risiko), TP21 (Nautobot-Stand), + TP3 (WLAN-Neuaufbau ohne CAPsMAN) + die beiden Governance-Callouts im Kopfbereich. +2. CLAUDE.md (Projekt) — Hardware/Zugänge/Gotchas. +3. nautobot/README.md — die Nautobot-API-Fallen. NICHT überspringen: `tagged_vlans` liest + sich per REST als `null`, OBWOHL korrekt gespeichert. Wer das nicht weiß, sucht eine + Stunde einen Fehler, den es nicht gibt. Verifikation immer per GraphQL. +4. docs/tp16.1-warp-mesh-native-crs326.md — was später auf dem Core terminieren soll. + +ZIEL DIESER SESSION (Scope-Grenze hart einhalten): + (a) beide CRS326 grundkonfigurieren (Mgmt-IP, Bridge, vlan-filtering, Trunks/Access), + (b) den Portplan beider Switches in Nautobot modellieren, + (c) crs326-branch physisch gegen sw2 tauschen (Technik OG). +NICHT in dieser Session: crs326-core produktiv setzen, L3-Umzug (Routing/DHCP/DNS von ap1), +CAPsMAN-Abbau, ap2-Neuprovisionierung. Das sind eigene, spätere Sessions. + +‼️ WARUM DER CORE TABU IST — DAS IST KEINE VORSICHT, DAS IST DIE TOPOLOGIE ‼️ +crs326-core ersetzt sw1 UND wird der neue L3-Router. An sw1 hängt auf Port 15 ap1 — also +Gateway, DHCP, DNS und der WAN-Trunk fürs GESAMTE Netz. Wer sw1 anfasst, nimmt das ganze +Haus offline, inklusive des eigenen Management-Pfads. Core-Tausch und L3-Umzug sind faktisch +dasselbe Ereignis und brauchen ein geplantes Ausfallfenster. Deshalb: Branch zuerst. + +GOVERNANCE — AUSNAHME BEREITS ERTEILT (Christoph, 2026-07-22): +Die Regel "Config ausschließlich über Nautobot" gilt weiter, ABER für den CRS326-BRING-UP +hat Christoph eine Ausnahme erteilt: Mgmt-IP, Bridge, VLAN-Filtering, Trunks/Access-Ports +dürfen direkt via MikroMCP/Winbox gesetzt werden (Henne-Ei: GC kann nicht auf ein Gerät ohne +erreichbare IP pushen, und die GC-Templates existieren noch nicht). +AUFLAGE: den entstandenen Ist-Stand danach als intended config nach Nautobot zurückschreiben. +Die Ausnahme deckt das Bootstrapping, NICHT den Dauerbetrieb, und gilt NICHT für ap1/ap2. + +GERÄTE-IDENTITÄT (beide sind bereits am Netz, RouterOS 7.19.5, Werkszustand): + crs326-core = MAC 04:F4:1C:B4:17:76, software-id 4W04-URHT + crs326-branch = MAC 04:F4:1C:B4:15:58, software-id RGZJ-UK0T +⚠️ BEIDE hören auf die Werks-IP 192.168.88.1 im SELBEN L2-Segment = IP-Konflikt. + Immer nur eines nach dem anderen adressieren; Unterscheidung ausschließlich per MAC + (MAC-Telnet/Winbox-Neighbor). Christoph hat die Geräte griffbereit neben sich, der + Erstzugriff ist deshalb unkritisch — kein Lockout-Szenario, notfalls physisch. +⚠️ crs326-branch ist BEREITS PHYSISCH INLINE: ap2 hängt an dessen Port 24 (deshalb war es + überhaupt per MNDP von ap1 aus sichtbar). Das ist kein Greenfield-Einbau. + +ZIELBILD BRANCH = die heutige sw2-Belegung (aus TP2) 1:1 übernehmen: + P1 Uplink-Trunk (→ sw1 gi8) | P2 ap2 | P3 frei | P4 Dock/Monitor | P5 Drucker + P6 frei | P7 Mac mini | P8 NAS (2. Port) + ⚠️ P4/P7/P8 sind in TP2 ausdrücklich als "nicht anfassen" markiert (fixe IPs, Lifeline). + VLANs laut kanonischem Modell: Uplink+ap2 = Trunk all; Rest Access auf dem jeweiligen VLAN. + +CONNECTIVITY — VOR DEM KABELZIEHEN LESEN: +Christophs Notebook hängt aktuell NUR per WLAN über cudy1, und cudy1 hängt am CORE (Port 11). +Der kabelgebundene Plan B (Dock/Monitor im Arbeitszimmer) hängt am sw2/BRANCH (P4). +Beim Branch-Tausch stirbt also der Kabelweg, das WLAN über cudy1 überlebt — die Pfade liegen +günstig auf verschiedenen Switches. Trotzdem: vor dem Ziehen von sw2 ansagen. + +WAS VERIFIZIERT IST UND WAS NICHT (nicht raten, nicht als Fakt ausgeben): + ✅ sw1 #15 = ap1, sw1 #11 = cudy1 (von Christoph bestätigt 2026-07-22) + ⚠️ sw1 gi16 (Arris/VLAN100) und gi8 (Trunk → sw2) stammen aus der Doku, sind NICHT am + Gerät gelesen — in Nautobot als "UNVERIFIZIERT" markiert. Falls für den Branch-Uplink + relevant: von Christoph bestätigen lassen, er macht sw1 ohnehin selbst. + ❌ switch/cli/03-trunk-ap-ports.cli (gi1 = ap1) ist STALE — vom Edge-Cutover 2026-07-01 + überholt. Nicht als Quelle verwenden. + ❌ switch/state-backups/sw1-running-config-*.txt sind 214-Byte-Fehlschläge (toter + claudecode-Login), switch/cli/running-PRE.txt ist Pre-TP1. + +NAUTOBOT (SoT, http://10.40.20.30:8080): + Token: NAUTOBOT_SUPERUSER_API_TOKEN in docs/nas-vlan20/stacks/210-nautobot.yml (gitignored). + Bereits gepflegt: 8 Devices mit Bereich/Rolle/Platform, 9 VLANs, 8 Prefixes, ap1 vollständig + (26 Interfaces + IPs), 6 SSIDs mit VLAN-Bindung, sw1 gi8/gi11/gi15/gi16, crs326-core ether4. + Wiederverwenden: nautobot/ingest-baseline.py (idempotentes goc()/patch()-Muster) — + nicht neu erfinden. Neue Ports als Interfaces am jeweiligen Device anlegen. + +ROUTEROS-FAKTEN, DIE ZEIT SPAREN: +- Interface-Nummerierung beginnt bei 1 (ether1…ether24). Es gibt KEIN ether0. +- hAP ax²: PoE-Port ist ether1, aber PASSIV 18–28 V, NICHT 802.3af/at. Der CRS326 hat gar + kein PoE → Strom käme aus einem Inline-Injektor. ether1 am hAP steht auf poe-out=auto-on; + wenn ether1 künftig der versorgte Uplink wird, muss poe-out dort auf off. + (PoE ist laut Christoph "vorerst kein Thema", die Config soll es nur vorsehen.) +- MikroMCP kennt in routers.yaml nur ap1/ap2 → die CRS326 dort ergänzen, sonst kein + MCP-Zugriff. Writes immer erst dryRun/plan_changes, dann apply_plan. + +NICHT ANFASSEN: +- ap1-Config (der CAPsMAN-Abbau kommt erst NACH dem Core-Tausch, s. TP3). +- ap2 (aktuell unerreichbar, meldet 10.40.40.2 = falsches VLAN; wird ohnehin neu + provisioniert — nicht diagnostizieren, nicht reparieren). +- sw1 (Christoph macht sw1 selbst; siehe Feedback-Memory). + +ABNAHME: +- Beide CRS326 unter ihrer neuen Mgmt-IP erreichbar, per MAC eindeutig zugeordnet, IP-Konflikt + aufgelöst. +- Nach dem Branch-Tausch: ap2-Port, Dock/Monitor, Drucker, Mac mini und NAS wieder erreichbar; + jeder in seinem korrekten VLAN (nicht ungetaggt in VLAN1 — das ist die Fehlerklasse, die + dieses Projekt seit Wochen verfolgt). +- Nautobot bildet die tatsächliche Portbelegung ab; Verifikation per GraphQL, nicht REST. + +DOKU (Pflicht am Ende): teilprojekte.md TP15 fortschreiben (As-built Portplan, was tatsächlich +lief/nicht lief), Nautobot-Stand nachziehen, Gotchas festhalten. Danach committen + pushen +(Commit-Msg-Konvention siehe git log: deutsch, Bullet-Struktur, Co-Authored-By). +``` + +--- ENDE PROMPT --- + +## Was Christoph zur Session parat haben sollte +- **Beide CRS326 griffbereit** (Erstzugriff/IP-Konflikt wird physisch aufgelöst). +- **Bestätigung der sw1-Ports `gi16` und `gi8`**, falls der Branch-Uplink daran hängt — + aktuell nur aus der Doku, nicht vom Gerät gelesen. +- **Zeitfenster für den sw2-Ausfall** (Dock/Monitor, Drucker, Mac mini, NAS-2. Port sind + währenddessen weg; WLAN über cudy1 bleibt). +- Klarheit, ob der **NAS-2.-Port (sw2 P8)** und der **Mac mini (P7)** während des Tauschs + ausfallen dürfen — beide sind in TP2 als „nicht anfassen" markiert. diff --git a/nautobot/ingest-2026-07-22-restdaten.py b/nautobot/ingest-2026-07-22-restdaten.py new file mode 100644 index 0000000..9259cbf --- /dev/null +++ b/nautobot/ingest-2026-07-22-restdaten.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Restdaten-Ingest 2026-07-22: cudy1/cudy2, sw1-Ports, crs326-core Zielport.""" +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "http://10.40.20.30:8080/api" +TOKEN = os.environ["NAUTOBOT_TOKEN"] +DRY = "--apply" not in sys.argv +log = [] + + +def req(method, path, data=None): + url = path if path.startswith("http") else f"{BASE}{path}" + body = json.dumps(data).encode() if data is not None else None + r = urllib.request.Request(url, data=body, method=method) + r.add_header("Authorization", f"Token {TOKEN}") + r.add_header("Content-Type", "application/json") + r.add_header("Accept", "application/json") + try: + with urllib.request.urlopen(r, timeout=30) as resp: + raw = resp.read() + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + print(f"!! {method} {url}\n {json.dumps(data)}\n -> {e.code} {e.read().decode()[:400]}") + raise + + +def find(path, **params): + q = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items()) + res = req("GET", f"{path}?{q}&limit=2") + return res["results"][0] if res.get("count") else None + + +def goc(path, match, payload, label=None): + name = label or payload.get("name") or payload.get("model") + obj = find(path, **match) + if obj: + log.append(f" = {name}") + return obj + if DRY: + log.append(f" + {name} (dry)") + return {"id": f"DRY-{name}"} + log.append(f" + {name}") + return req("POST", path + "/", payload) + + +def patch(path, obj, payload, label): + delta = {} + for k, v in payload.items(): + cur = obj.get(k) + cur = cur.get("id") if isinstance(cur, dict) else cur + if cur != v: + delta[k] = v + if not delta: + log.append(f" = {label} (unveraendert)") + return obj + if DRY: + log.append(f" ~ {label}: {list(delta)} (dry)") + return obj + log.append(f" ~ {label}: {list(delta)}") + return req("PATCH", f"{path}/{obj['id']}/", delta) + + +sid = lambda n: find("/extras/statuses", name=n)["id"] +S_ACTIVE, S_DECOM = sid("Active"), sid("Decommissioning") +FW = "Cudy WR3600E V1.0, Firmware 2.3.15-20250905-191956." + +print("== 1. Cudy: echter DeviceType statt Platzhalter") +cudy_mfr = find("/dcim/manufacturers", name="Cudy") +dt_new = goc("/dcim/device-types", {"model": "WR3600E V1.0"}, { + "manufacturer": cudy_mfr["id"], "model": "WR3600E V1.0", "u_height": 0, + "is_full_depth": False, "comments": "Cudy WR3600E V1.0 — Uebergangs-WLAN waehrend der Umstellung."}) + +LOC = {n: find("/dcim/locations", name=n) for n in ("Technik UG", "Technik OG")} +role_ap = find("/extras/roles", name="Wireless AP") + +print("== 2. cudy1 / cudy2") +c1 = find("/dcim/devices", name="cudy1") +patch("/dcim/devices", c1, { + "device_type": dt_new["id"], "location": LOC["Technik UG"]["id"], "status": S_ACTIVE}, "cudy1") +if not DRY: + req("PATCH", f"/dcim/devices/{c1['id']}/", {"comments": + f"{FW} Standalone-WLAN 'elektrosmog.c' auf 10.40.10.246, haengt an sw1 Port 11. " + "Uebergangssender, kein Abbauziel mehr (TP19). WICHTIG: Christophs Notebook haengt " + "per WLAN hierueber — Rettungspfad beim Branch-Tausch."}) + +goc("/dcim/devices", {"name": "cudy2"}, { + "name": "cudy2", "device_type": dt_new["id"], "role": role_ap["id"], + "location": LOC["Technik OG"]["id"], "status": S_DECOM, + "comments": f"{FW} Ausser Betrieb, wird nicht reaktiviert (TP19, ARP-Sweep bestaetigt " + "keine Praesenz mehr). Frueher 10.40.41.247."}) + +print("== 3. Platzhalter-DeviceType entfernen") +old = find("/dcim/device-types", model="Cudy AP (Modell TBD)") +if old and not DRY: + if req("GET", f"/dcim/devices/?device_type={old['id']}&limit=1")["count"] == 0: + req("DELETE", f"/dcim/device-types/{old['id']}/") + log.append(" - Cudy AP (Modell TBD) geloescht") + else: + log.append(" ! Platzhalter noch in Benutzung — nicht geloescht") + +print("== 4. sw1-Ports (nur Gesichertes + markiert Unverifiziertes)") +sw1 = find("/dcim/devices", name="sw1") +V = {v: find("/ipam/vlans", vid=v) for v in (1, 10, 20, 30, 40, 50, 90, 99, 100)} +PORTS = [ + ("gi15", "ap1-Trunk (bestaetigt User 2026-07-22). Ziel: wandert auf crs326-core ether4.", + "tagged", 1, [10, 20, 30, 40, 50, 90, 99, 100]), + ("gi11", "cudy1 (bestaetigt User 2026-07-22). Christophs WLAN-Pfad haengt hier dran.", + None, None, None), + ("gi16", "Arris-Kabelmodem, Access VLAN100 (aus TP4-Doku, UNVERIFIZIERT am Geraet).", + "access", 100, None), + ("gi8", "Trunk zu sw2 (aus TP1/TP2-Doku, UNVERIFIZIERT am Geraet).", + "tagged", 1, [10, 20, 30, 40, 50, 90, 99, 100]), +] +for nm, desc, mode, untag, tags in PORTS: + i = goc("/dcim/interfaces", {"device": "sw1", "name": nm}, { + "device": sw1["id"], "name": nm, "type": "1000base-t", + "status": S_ACTIVE, "description": desc}, label=f"sw1:{nm}") + if not DRY and mode: + p = {"mode": mode, "untagged_vlan": V[untag]["id"]} + if tags: + p["tagged_vlans"] = [V[t]["id"] for t in tags] + req("PATCH", f"/dcim/interfaces/{i['id']}/", p) + +print("== 5. crs326-core: Zielport fuer ap1") +core = find("/dcim/devices", name="crs326-core") +goc("/dcim/interfaces", {"device": "crs326-core", "name": "ether4"}, { + "device": core["id"], "name": "ether4", "type": "1000base-t", "status": S_ACTIVE, + "description": "Ziel-Port fuer ap1 (heute sw1 #15). User-Vorgabe 2026-07-22. " + "ap1-Uplink wandert dabei von ether2 auf ether1 (PoE-Port)."}, + label="crs326-core:ether4") + +print("\n".join(log)) +print("\n" + ("DRY-RUN — nichts geschrieben." if DRY else "ANGEWENDET."))