diff --git a/CLAUDE.md b/CLAUDE.md index 2833b0f..5318d68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,9 @@ GW je VLAN = `.1` · APs: ap1=`.1`, ap2=`.2` · Switches: ALLNET=`.11`, Omada=`. | Gerät | IP | Zugang | |---|---|---| | ALLNET Core-Switch (sw1) | 10.40.41.11 (interim, VLAN1) | `telnet 10.40.41.11` · User: `claudecode` · PW: `switch/.secrets` | -| Omada Access-Switch (sw2) | **10.40.99.12** (statisch, VLAN99 — TP19.1 ✅ 2026-07-16, Christoph selbst über `sw2/ui-guides/01-vlan99-mgmt-cutover.md`) | Web-UI HTTPS, **standalone ohne Controller** (TP2.1 ✅ 2026-07-16) · Creds `sw2/.secrets` (Host-Wert dort veraltet, IP hier maßgeblich) | +| ~~Omada Access-Switch (sw2)~~ | — | ❌ **abgebaut 2026-07-23** (TP15 Branch-Tausch) — durch `crs326-branch` in Technik OG ersetzt, physisch entfernt | +| **crs326-branch** (CRS326, Technik OG) | **10.40.99.13** (VLAN99, über sw1 #8) · VLAN1-Temp `.41.213` | RouterOS 7.19.5 · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.13/rest` · MikroMCP `routers.yaml` (live nach MCP-Neustart) · **TP15 ✅ 2026-07-23: ersetzt sw2**, volle Portbelegung (ether1 Uplink→sw1#8, 2/8 NAS, 3 Drucker, 4 Dock, 5/6 Sonos, 7 macmini, 24 ap2) · sw-id RGZJ-UK0T, MAC 04:F4:1C:B4:15:58 | +| **crs326-core** (CRS326, Technik UG) | **10.40.99.11** (VLAN99, erreichbar erst wenn sw1#12 99T trägt) · VLAN1-Temp `.41.211` (an sw1 #12) | RouterOS 7.19.5 · `admin`/`~/.secrets` (`CRS326_CORE_*`) · REST `http://10.40.41.211/rest` · **TP15 ✅ 2026-07-23 grundkonfiguriert, NICHT produktiv** — wird Core/L3-Router (ersetzt sw1) in eigener Session · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 | | hAP ax² #1 (ap1) | 10.40.10.1 (Mgmt-Zugriff) · `platform-pg.bf.lan`-artige DNS-Statics unter `bf.lan` | MikroMCP (live) · **TP4 ✅: L3-Router, GW je VLAN = .1**, vlan99-GW `10.40.99.1` seit TP4 live | | hAP ax² #2 (ap2) | 10.40.41.2 (VLAN1, TP19.1-Umzug auf `.99.2` vertagt bis TP21/Nautobot) | MikroMCP (live) · **Port 2 am Omada-Switch** | | NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) | diff --git a/docs/ip-plan.md b/docs/ip-plan.md index c4dbeda..60cdcc0 100644 --- a/docs/ip-plan.md +++ b/docs/ip-plan.md @@ -21,7 +21,9 @@ | ap1 (Router/hAP) | 10.40.41.1 | Router/GW | alle (.1) + Mgmt | bleibt `.1` je VLAN; Mgmt `10.40.99.1` | ⬜ | ist GW, kein „Umzug" | | ap2 (hAP) | 10.40.41.2 | AP | 99 MGMT | `10.40.99.2` | ⬜ | | | sw1 ALLNET (Core) | 10.40.41.11 | Switch-Mgmt | 99 MGMT | `10.40.99.11` | ⬜ | Mgmt-VLAN-Wechsel = Lockout-Risiko, sorgsam | -| sw2 Omada (ES208G) | ~~10.40.41.181~~ | Switch-Mgmt | 99 MGMT | `10.40.99.12` | ✅ **live** | TP19.1 2026-07-16, Christoph selbst über Klickanleitung | +| ~~sw2 Omada (ES208G)~~ | — | — | — | — | ❌ **abgebaut** | TP15 2026-07-23: physisch entfernt, durch crs326-branch ersetzt (Technik OG) | +| **crs326-branch** (CRS326) | 192.168.88.1 (Werk) | Switch-Mgmt (Technik OG) | 99 MGMT | `10.40.99.13` (Ziel `.99.12` nach Core-Tausch) | ✅ **live** | TP15 2026-07-23: ersetzt sw2; Bring-up-VLAN1-Temp `.41.213`; über sw1 #8 | +| **crs326-core** (CRS326) | 192.168.88.1 (Werk) | Switch-Mgmt/künftig L3-Router (Technik UG) | 99 MGMT | `10.40.99.11` | 🟡 **grundkonf.** | TP15 2026-07-23: grundkonfiguriert, nicht produktiv (ersetzt sw1, eigene Session); VLAN1-Temp `.41.211`, an sw1 #12 | | NAS nas-bf (DS923) | ~~10.40.41.10~~ | INFRA | 20 INFRA | `10.40.20.10` | ✅ **live** | TP14.1 2026-07-10; DNS `nas.bf.lan` nachgezogen 2026-07-16 | | ~~Omada-Controller~~ | — | Service (macvlan/DS923) | — | — | ❌ **entfernt** | User hat den Container 2026-07-16 gelöscht (overkill für 1 Switch auf der Abschussliste, s. TP2.1/TP15). DNS-Eintrag `omada.bf.lan` entfernt | | ~~omada-mcp~~ | — | Service (pve-LXC) | — | — | ❌ **entfernt** | mit dem Controller zusammen gelöscht 2026-07-16 (MCP-Server verwaist, in Claude-Settings noch registriert — Aufräumen offen). DNS-Eintrag `omada-mcp.bf.lan` entfernt | diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 4c1c1cb..ebc4d54 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -344,7 +344,15 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". ### TP14.5 — Session E: Freigabe-Gate → TP5-Firewall ▸ 🔜 (ZULETZT) - Erst wenn TP14.1–14.3 durch (alle Origins/Connectoren in VLAN20) → dann TP5 (Deny-by-default) starten. Reines Reihenfolge-Gate, kein eigener Change. -### TP15 — HW-Refresh: Switching + Router + DC-USV ▸ 🟡 Beschaffung läuft, **Lieferung 2026-07-17 (morgen)** — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc` +### TP15 — HW-Refresh: Switching + Router + DC-USV ▸ 🟢 **CRS326-Bring-up + Branch-Tausch DURCH (2026-07-23)**; Core-Tausch/L3-Umzug offen (eigene Session) — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc` +- ✅ **BRING-UP BEIDER CRS326 + BRANCH-TAUSCH gegen sw2 — DURCH (2026-07-23, Claude via REST unter Governance-Ausnahme #2).** Ablauf/As-built: + - **Zugriffsweg (Henne-Ei gelöst):** Erst-Reihenfolge war **core zuerst** (User-Korrektur — core hängt an sw1 und ist der L2-Pfad, über den branch überhaupt erreichbar wird; branch war isoliert). Zugriff auf die Werks-`192.168.88.1` über **temporäre ap1-Zweit-IP `192.168.88.2/24` + srcnat-Masquerade `192.168.88.0/24`** (beide `TP15-temp`, nach Bring-up wieder entfernt — Rückweg nötig, weil die Werks-Box keine Route zu VLAN10 hat). Config dann komplett per **REST** (`/system/script` PUT+run), kein Winbox-Paste. **Winbox-MAC-Connect scheiterte reproduzierbar** („macconnection sync timeout" über WLAN/quer durch den Fabric — braucht selben Draht). + - **Identitäten am Gerät verifiziert (nicht nur Doku):** `core` = sw-id **4W04-URHT**, MAC `04:F4:1C:B4:17:76`, Serial `HKA0AREXJFM`, PW `X9…`; `branch` = sw-id **RGZJ-UK0T**, MAC `04:F4:1C:B4:15:58`, Serial `HKA0AJJA52F`, PW `PHR…` (ap2 hängt an branch **ether24** — bridge-host bestätigt = definitiv branch). **PWs in `~/.secrets/credentials.env`.** + - **branch = produktiv, volle SOLL-Portbelegung** (User-manuell erhoben, maßgeblich): ether1 Uplink-Trunk→sw1 #8 (native VLAN1 + tagged 10/20/30/40/50/90/99, **kein 100**), ether2+ether8 = **NAS 2×tagged-VLAN20** (zwei unabhängige Links), ether3 Drucker/VLAN10, ether4 Dock/VLAN10, ether5+ether6 Sonos/VLAN40, ether7 Mac mini/VLAN20, ether24 ap2-Trunk; ether9–23+sfp disabled. vlan-filtering=yes. Mgmt **`10.40.99.13` (VLAN99)** erreichbar über sw1 #8 (+ VLAN1-Temp `.41.213` während Bring-up). **Post-Swap-Abnahme grün:** NAS `.20.10`, Mac mini `.20.167` (VLAN20), Dock `.10.194` (VLAN10), Sonos `.40.191`/`.40.192` (VLAN40). + - **core = grundkonfiguriert, NICHT produktiv** (bleibt an sw1 #12): identity/vlan-filtering/volle VLAN-Tabelle, ether1 Uplink, ether2 Temp-Downlink (war Config-Pfad für branch), Mgmt **`10.40.99.11`** (VLAN99, erreichbar erst wenn sw1 #12 99T trägt) + VLAN1-Temp `.41.211`. Port-Layout ab ether3 tentativ (final in Core-Swap-Session). + - **Beide in MikroMCP `routers.yaml`** (crs326-core/-branch, ENV `ROUTER_CRS326_*` in `~/.claude.json` — **live erst nach MCP-Neustart**, diese Session lief alles per REST-curl). **Nautobot** modelliert + GraphQL-verifiziert (`nautobot/ingest-crs326-ports.py`): branch Active + volle Ports + primary_ip4 `.99.13`; core Planned + tentatives Layout; **sw1 verifizierte 16-Port-Belegung** (löst „UNVERIFIZIERT"); **sw2 → Decommissioning**. + - 🐞 **Gotchas festgehalten:** (1) **Kabel ether3↔ether5 waren beim Umstecken vertauscht** → ein AZ-Sonos landete als Access-VLAN10 in `10.40.10.190` **und** blutete via SonosNet nach VLAN40 (= Dual-Homing-Muster, `project_dual_homing_slowness_incident`) → Kabeltausch gefixt, beide AZ-Sonos jetzt sauber VLAN40 (`.191`/`.192` bound). (2) RouterOS-Script-`policy`-Feld per REST wählerisch → **weglassen** (admin-Default = volle Policy). (3) Disable-Regex mit `^…$`-Ankern griff nicht → `[find name!="ether1" and …]` nutzen. (4) REST-`PUT`-Antwort mit mehrzeiligem `source` ist kein sauberes JSON → ID separat via `?.proplist=.id` holen. (5) Claude-Code-Auto-Mode-Classifier blockt Device-Config-*Writes* per Bash (Reads ok) → `autoMode.allow`-Regel in `.claude/settings.local.json` nötig (kann Claude sich nicht selbst setzen). +- **🔜 Als Nächstes (eigene Session, Übergabe): Core-Tausch = L3-Umzug** (Routing/DHCP/DNS/CF-Mesh von ap1 → crs326-core) — **Vollausfall-Fenster**, s. `docs/tp15-core-swap-session-handoff-prompt.md`. Danach WLAN-Neuprovisionierung (TP3, CAPsMAN-Abbau). Finaler Mgmt-Renumber dann: branch `.99.13`→`.99.12`, core bleibt `.99.11`. - 📦 **2× CRS326-24G-2S+RM werden morgen (2026-07-17) geliefert, Produktivsetzung spätestens Montag 2026-07-20 (User-Entscheid 2026-07-16).** Sequenzierung ggü. Nautobot geklärt: **TP20+TP21 werden bis dahin fast-getrackt** (TP18/Komodo als Blocker entfernt, s. TP20) — Ziel ist, dass Nautobot **vor** der CRS326-Produktivsetzung steht und die Switches **direkt als Nautobot-verwaltete Geräte adoptiert** werden, statt erst von Hand konfiguriert und später importiert. sw1/sw2-VLAN99-Umzug (TP19.1) läuft parallel dazu on-demand, unabhängig davon. - **🧭 Grundsatzentscheid (2026-07-14, korrigiert 2026-07-15): reines MikroTik-Ecosystem — Routing + Switching + WLAN.** **Omada bekommt KEINE Rolle** (weder Switching noch WLAN — der ursprüngliche „Omada=WLAN"-Satz war ein Fehlschluss beim Dokumentieren, User-Korrektur 2026-07-15: „WLAN war NIE von Omada die Rede"). Begründung Switching: Projektziel #1 **CAKE** nur RouterOS/OpenWrt/OPNsense (Omada-Gateway kann kein CAKE) → Switching = MikroTik CRS. WLAN bleibt auf hAP ax²/CAPsMAN (kein EAP-Kauf), Coverage-Fix bleibt Platzierungsfrage (TP3.3). Verworfene Vollvarianten (Omada-only, MikroTik-only) samt Begründung + Ansible/Nautobot-Recherche → Memory. **Anlass der Korrektur:** Omada-Controller↔sw2-Link beim VLAN20-Umzug (TP14.1) zerbrochen, versehentlicher Werks-Reset bei Rettungsversuch, fehlerhafter Wiederherstellungsversuch → sw2 wird standalone neu eingerichtet, s. **TP2.1**. - **🛒 Switches: 2× CRS326-24G-2S+RM bestellt (2026-07-14 — CSS326-Fehlbestellung storniert → CRS).** 24×1G + 2×SFP+, lüfterlos, DC-input (USV-tauglich). Lösen ALLNET-Core (sw1) + Omada-ES208G (sw2, s. TP2.1 — Solo/Interim bis CRS-Tausch) + Cudy ab. diff --git a/docs/tp15-core-swap-session-handoff-prompt.md b/docs/tp15-core-swap-session-handoff-prompt.md new file mode 100644 index 0000000..608db4c --- /dev/null +++ b/docs/tp15-core-swap-session-handoff-prompt.md @@ -0,0 +1,92 @@ +# TP15 — Handoff-Prompt: Core-Tausch = L3-Umzug ap1 → crs326-core (neue Session) + +> **Zweck:** Kopierfertiger Auftrag für eine frische Session, die `crs326-core` produktiv setzt: +> sw1 (ALLNET) physisch ablösen **und gleichzeitig** Routing/DHCP/DNS von ap1 auf den CRS326 umziehen. +> **Das ist ein geplantes Vollausfall-Ereignis** — nicht nebenbei, sondern mit Wartungsfenster. +> +> **Voraussetzung erfüllt (2026-07-23):** Branch-Tausch ist durch, `crs326-core` ist grundkonfiguriert und +> erreichbar. Diese Session ist der zweite, riskantere Teil. +> +> **So übergibst du es:** Neue Session, erster Prompt: +> „Lies `docs/tp15-core-swap-session-handoff-prompt.md` und arbeite den Auftrag darin ab." + +--- PROMPT --- + +``` +TP15 — Core-Tausch: crs326-core wird L3-Router (ersetzt sw1) + L3-Umzug von ap1. + +‼️ DAS IST EIN VOLLAUSFALL ‼️ +An sw1 hängt auf #15 ap1 = Gateway/DHCP/DNS/WAN fürs GANZE Haus. sw1 zu tauschen UND L3 von ap1 +wegzuziehen ist EIN Ereignis. Während des Cutovers ist das komplette Netz offline, inklusive des +eigenen Management-Pfads. Vorher: Wartungsfenster ansagen, Rettungspfad klären (Notebook-Zugang der +NICHT über den zu tauschenden Kern läuft), Rollback-Plan bereitlegen (sw1 + ap1-Config bleiben bis +zum Beweis unangetastet/zurückspielbar). + +KONTEXT ZUERST LESEN: +1. docs/teilprojekte.md — TP15 (As-built Bring-up/Branch-Tausch 2026-07-23), TP4 (ap1 heutige + L3/DHCP/DNS/NAT-Config = die Vorlage, die umzieht), TP16.1 (CF-Mesh terminiert künftig auf core), + TP5/TP6 (Firewall + CAKE landen auf core), TP3 (WLAN-Neuprovisionierung DANACH). +2. CLAUDE.md — Hardware/Zugänge (crs326-core = 10.40.99.11 / VLAN1-Temp 10.40.41.211, admin/~/.secrets). +3. nautobot/README.md — GraphQL-Verifikation. Nautobot hat core als "Planned" mit tentativem Layout. + +IST-STAND core (aus dem Bring-up 2026-07-23): +- crs326-core an sw1 #12, RouterOS 7.19.5, identity gesetzt, vlan-filtering=yes, volle Bridge-VLAN- + Tabelle 1/10/20/30/40/50/90/99/100 (ether1 Uplink-Trunk, ether2 war Temp-Downlink zu branch). +- Mgmt 10.40.99.11 (VLAN99) — erreichbar erst, wenn sw1 #12 VLAN99 tagged trägt (aktuell nur VLAN1); + Arbeitskanal diese Session bisher = 10.40.41.211 (VLAN1) über sw1 #12 native. +- NUR Switching. KEIN L3 (keine /ip address je VLAN außer Mgmt, kein DHCP, kein DNS, kein NAT). + +ZIEL-LAYOUT core (User-Survey, "neu"-Vorgabe 2026-07-22 — VLAN99 pro Port noch am Gerät gegenprüfen): +- #4 ap1-Uplink-Trunk (ap1 wandert von sw1 #15 auf core #4; ap1-seitig Uplink ether2→ether1/PoE-Port) +- #2/#3/#5 Sonos VLAN40 untagged (+ die weiteren Sonos-/Hybrid-/pve-/sonoff-Ports aus der sw1-Survey, + s. Nautobot sw1 GE1-16 Beschreibungen = die verifizierte Alt-Belegung, die auf core abgebildet wird) +- #16 Arris → WAN VLAN100 (Access); der WAN-Trunk-Pfad zu ap1 entfällt, WAN terminiert künftig auf core +- #8 → crs326-branch (Uplink zur OG-Branch) +- Kapazität/Ports final beim Umstecken festklopfen. + +WAS UMZIEHT (ap1 → core), Vorlage = ap1s heutige Config (TP4): +- VLAN-Subinterfaces vlan10..vlan99 auf der core-Bridge + GW-IP .1 je VLAN. +- DHCP-Server je VLAN (Pools .100-.200, GW+DNS = VLAN-GW) inkl. aller MAC-Reservierungen (Sonos, Shelly, + NAS/macmini/pve statisch, Drucker …) — 1:1 von ap1 exportieren. +- DNS-Resolver (allow-remote-requests, DoH-Upstream Cloudflare Gateway, 13 statische bf.lan-A-Records). +- NAT srcnat masquerade out=vlan100 (Public-Edge wandert auf core). +- Routing: Default via Liwest-GW auf vlan100; Connected-Routes je VLAN. +- WAN-Input-Firewall-Seal (TP10) + später Deny-by-default (TP5) + CAKE (TP6). +- CF-Mesh WG-Node (TP16.1) — NACH dem Cutover, eigener Schritt. + +ABLAUF-EMPFEHLUNG (reversibel bauen, dann in EINEM Fenster cutovern): +1. core L3 VORBEREITEN, solange ap1 noch Router ist: VLAN-Interfaces + IPs auf core anlegen, ABER die + GW-IPs .1 NICHT doppelt aktiv fahren (IP-Konflikt mit ap1). Trick: DHCP/DNS auf core bauen aber + disabled, VLAN-GW-IPs erst im Cutover-Moment aktivieren. Alternativ core-L3 komplett scripten und + als ein Import-Block bereitlegen. +2. sw1-Kabel-Umzugsplan festklopfen (welcher sw1-Port → welcher core-Port), ap1 #15→#4. +3. CUTOVER-Fenster: ap1 als Router stilllegen (DHCP/DNS/NAT/VLAN-GW-IPs runter ODER ap1 direkt auf + reinen AP zurückbauen), sw1-Kabel auf core umstecken, core-L3 aktivieren, WAN (Arris) auf core #16. +4. Verifizieren: je VLAN DHCP-Lease + GW-Ping + Internet; DNS bf.lan; NAT; branch weiterhin über core; + WARP/CF. Rollback = Kabel zurück auf sw1 + ap1-Config-Restore. +5. ap1/ap2 werden reine APs → danach WLAN-Neuprovisionierung ohne CAPsMAN (TP3, eigene Session). + +GOVERNANCE: Die Bring-up-Ausnahme #2 (2026-07-22) deckte NUR den Grundconfig-Bring-up. Für den +produktiven L3-Umzug braucht es entweder Nautobot Golden Config (TP21) ODER eine neu zu erteilende +Ausnahme (User fragen). Ergebnis danach als intended nach Nautobot zurückschreiben. + +MGMT-RENUMBER (User-Vorgabe): nach Austausch BEIDER Switches erben die neuen die Alt-IPs — +core bleibt 10.40.99.11, branch 10.40.99.13 → 10.40.99.12. VLAN1-Temp-IPs (.41.211/.41.213) entfernen. + +NICHT ANFASSEN bis Cutover: die produktive branch-Config (läuft), die Lifeline-Ports. + +ABNAHME: alle VLANs bekommen DHCP+GW+Internet über core; DNS/NAT/WAN auf core; branch + alle OG/UG- +Geräte erreichbar im korrekten VLAN; ap1/ap2 reine APs; Nautobot bildet core produktiv ab (GraphQL). + +DOKU (Pflicht): teilprojekte.md TP15/TP4 fortschreiben, CLAUDE.md/ip-plan.md (core produktiv, ap1 = AP), +Nautobot nachziehen, Gotchas. Commit + Push (deutsch, Bullet, Co-Authored-By). +``` + +--- ENDE PROMPT --- + +## Was Christoph zur Session parat haben sollte +- **Wartungsfenster** (komplettes Netz offline während des Cutovers). +- **Rettungspfad**, der NICHT über sw1/ap1 läuft (z.B. Notebook direkt an core, oder Handy-Hotspot). +- **Entscheidung Governance:** L3-Umzug via Nautobot GC (TP21) oder frische Direkt-Ausnahme. +- **sw1-Port→core-Port-Umzugsplan** final (ap1 #15→#4, Arris #16, pve, sonoff, cudy1, Sonos …). +- **DC-USV** (falls parallel geplant, TP15 HW-Teil). diff --git a/docs/tp15-crs326-session-handoff-prompt.md b/docs/tp15-crs326-session-handoff-prompt.md index c1dc860..8fd695a 100644 --- a/docs/tp15-crs326-session-handoff-prompt.md +++ b/docs/tp15-crs326-session-handoff-prompt.md @@ -1,5 +1,9 @@ # TP15 — Handoff-Prompt: CRS326-Bring-up + Branch-Tausch (neue Session) +> ✅ **ERLEDIGT am 2026-07-23** — beide CRS326 grundkonfiguriert, branch produktiv gegen sw2 getauscht, +> Nautobot modelliert. As-built s. `docs/teilprojekte.md` TP15. **Nächster Schritt = Core-Tausch/L3-Umzug: +> `docs/tp15-core-swap-session-handoff-prompt.md`.** (Dieses Dokument nur noch als Historie.) + > **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. diff --git a/mcp/mikromcp/config/routers.yaml b/mcp/mikromcp/config/routers.yaml index 879de10..260f4d0 100644 --- a/mcp/mikromcp/config/routers.yaml +++ b/mcp/mikromcp/config/routers.yaml @@ -28,3 +28,31 @@ routers: envPrefix: "ROUTER_AP2" # -> ROUTER_AP2_USER, ROUTER_AP2_PASS tags: ["bf", "wifi", "cap"] rosVersion: "7.23" + + # --- TP15 CRS326-Bring-up (2026-07-23) --- + # Mgmt auf VLAN99. www=plain HTTP:80 (wie ap1/ap2) -> tls.enabled:false. + # ENV in ~/.claude.json (mikromcp-Block): ROUTER_CRS326_CORE_USER/PASS, ROUTER_CRS326_BRANCH_USER/PASS. + # Wird erst nach MCP-Neustart (neue Session) live; diese Session Verify via direktem REST-curl + ap1-ping. + crs326-core: + host: "10.40.99.11" + port: 80 + tls: + enabled: false + rejectUnauthorized: true + credentials: + source: "env" + envPrefix: "ROUTER_CRS326_CORE" + tags: ["bf", "switch", "core", "future-l3-router"] + rosVersion: "7.19.5" + + crs326-branch: + host: "10.40.99.13" + port: 80 + tls: + enabled: false + rejectUnauthorized: true + credentials: + source: "env" + envPrefix: "ROUTER_CRS326_BRANCH" + tags: ["bf", "switch", "branch"] + rosVersion: "7.19.5" diff --git a/nautobot/ingest-crs326-ports.py b/nautobot/ingest-crs326-ports.py new file mode 100644 index 0000000..adf51a2 --- /dev/null +++ b/nautobot/ingest-crs326-ports.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""BF-Netzwerk -> Nautobot: CRS326-Portplan + sw1-Belegung (TP15 As-built, 2026-07-23). + +Ergaenzt ingest-baseline.py: schreibt den Ist-Stand des CRS326-Bring-ups (Governance-Auflage +"intended config zurueckschreiben") sowie die vom User verifizierte sw1-Belegung nach Nautobot. + + crs326-branch : volle SOLL-Portbelegung (produktiv), mgmt 10.40.99.13, Status Active + crs326-core : Bring-up-Interfaces + geplantes Layout (tentativ), mgmt 10.40.99.11, Status Planned + sw1 (ALLNET) : verifizierte 16-Port-Belegung (loest "UNVERIFIZIERT" auf) + sw2 (ES208G) : Status -> Decommissioning (wird durch crs326-branch ersetzt) + +Muster wie ingest-baseline.py: get-or-create + Delta-PATCH, idempotent. +Verifikation NUR per GraphQL (tagged_vlans liest sich per REST als null). +""" +import json, os, sys, urllib.request, urllib.error, urllib.parse + +BASE = "http://10.40.20.30:8080/api" +TOKEN = os.environ.get("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 payload={json.dumps(data)}\n -> {e.code} {e.read().decode()[:500]}") + 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 str(match) + obj = find(path, **match) + if obj: + log.append(f" = {name}") + return obj + if DRY: + log.append(f" + {name} (dry-run)") + return {"id": f"DRY-{name}"} + obj = req("POST", path + "/", payload) + log.append(f" + {name}") + return obj + + +def patch(path, obj, payload, label): + delta = {} + for k, v in payload.items(): + cur = obj.get(k) + cur_id = cur.get("id") if isinstance(cur, dict) else cur + if cur_id != v: + delta[k] = v + if not delta: + log.append(f" = {label} (unveraendert)") + return obj + if DRY: + log.append(f" ~ {label}: {list(delta)} (dry-run)") + return obj + out = req("PATCH", f"{path}/{obj['id']}/", delta) + log.append(f" ~ {label}: {list(delta)}") + return out + + +def sid(name): + return find("/extras/statuses", name=name)["id"] + + +ETH, SFP, VIRT, BR = "1000base-t", "10gbase-x-sfpp", "virtual", "bridge" + +S_ACTIVE = sid("Active") +S_PLANNED = sid("Planned") +S_DECOM = sid("Decommissioning") + +# VLAN-Objekte (aus ingest-baseline angelegt) +vg = find("/ipam/vlan-groups", name="BF") +V = {} +for vid in (1, 10, 20, 30, 40, 50, 90, 99, 100): + V[vid] = find("/ipam/vlans", vid=vid, vlan_group=vg["id"]) +ns_global = find("/ipam/namespaces", name="Global")["id"] + +D = {n: find("/dcim/devices", name=n) for n in ("crs326-branch", "crs326-core", "sw1", "sw2")} + + +def iface(dev, name, typ, desc, mode=None, untagged=None, tagged=None): + """Interface get-or-create + Attribute-PATCH (Mode/VLANs).""" + obj = goc("/dcim/interfaces", {"device": dev, "name": name}, + {"device": D[dev]["id"], "name": name, "type": typ, + "status": S_ACTIVE, "description": desc}, label=f"{dev}:{name}") + p = {"description": desc, "type": typ} + if mode: + p["mode"] = mode + if untagged is not None: + p["untagged_vlan"] = V[untagged]["id"] + if not DRY and not str(obj["id"]).startswith("DRY"): + obj = patch("/dcim/interfaces", obj, p, f"{dev}:{name}") + if tagged is not None: + req("PATCH", f"/dcim/interfaces/{obj['id']}/", + {"tagged_vlans": [V[t]["id"] for t in tagged]}) + return obj + + +def set_mgmt(dev, ifname, addr, desc): + ip = find("/ipam/ip-addresses", address=addr) + if not ip: + ip = goc("/ipam/ip-addresses", {"address": addr}, + {"address": addr, "status": S_ACTIVE, "description": desc, + "namespace": ns_global}, label=addr) + if DRY or str(ip["id"]).startswith("DRY"): + return + intf = find("/dcim/interfaces", device=dev, name=ifname) + if not find("/dcim/interfaces", device=dev, name=ifname, ip_addresses=ip["id"]): + try: + req("POST", "/ipam/ip-address-to-interface/", + {"ip_address": ip["id"], "interface": intf["id"]}) + log.append(f" + {addr} -> {dev}:{ifname}") + except urllib.error.HTTPError: + log.append(f" = {addr} -> {dev}:{ifname} (bereits)") + req("PATCH", f"/dcim/devices/{D[dev]['id']}/", {"primary_ip4": ip["id"]}) + + +TRUNK_BRANCH = [10, 20, 30, 40, 50, 90, 99] # kein VLAN100 auf branch (WAN nur zu ap1) + +print("== crs326-branch: SOLL-Portplan (produktiv)") +patch("/dcim/devices", D["crs326-branch"], {"status": S_ACTIVE}, "crs326-branch status") +iface("crs326-branch", "bridge", BR, "vlan-filtering=yes; VLAN1 untagged, 10-99 tagged") +iface("crs326-branch", "ether1", ETH, "Uplink-Trunk -> sw1 #8 (native VLAN1 + tagged 10/20/30/40/50/90/99, KEIN 100)", + mode="tagged", untagged=1, tagged=TRUNK_BRANCH) +iface("crs326-branch", "ether2", ETH, "NAS #1 (tagged VLAN20)", mode="tagged", tagged=[20]) +iface("crs326-branch", "ether3", ETH, "Drucker (Access VLAN10)", mode="access", untagged=10) +iface("crs326-branch", "ether4", ETH, "Monitor/Dock Laptop (Access VLAN10)", mode="access", untagged=10) +iface("crs326-branch", "ether5", ETH, "Sonos (Access VLAN40)", mode="access", untagged=40) +iface("crs326-branch", "ether6", ETH, "Sonos (Access VLAN40)", mode="access", untagged=40) +iface("crs326-branch", "ether7", ETH, "Mac mini (Access VLAN20)", mode="access", untagged=20) +iface("crs326-branch", "ether8", ETH, "NAS #2 (tagged VLAN20)", mode="tagged", tagged=[20]) +for n in range(9, 24): + iface("crs326-branch", f"ether{n}", ETH, "frei (disabled)") +iface("crs326-branch", "ether24", ETH, "ap2 (Trunk; native VLAN1 + tagged 10-99)", + mode="tagged", untagged=1, tagged=TRUNK_BRANCH) +iface("crs326-branch", "sfp-sfpplus1", SFP, "frei (disabled)") +iface("crs326-branch", "sfp-sfpplus2", SFP, "frei (disabled)") +iface("crs326-branch", "vlan99", VIRT, "Mgmt SVI (VLAN99)", mode="tagged", tagged=[99]) +if not DRY: + v99 = find("/dcim/interfaces", device="crs326-branch", name="vlan99") + br = find("/dcim/interfaces", device="crs326-branch", name="bridge") + req("PATCH", f"/dcim/interfaces/{v99['id']}/", {"parent_interface": br["id"]}) +set_mgmt("crs326-branch", "vlan99", "10.40.99.13/24", + "TP15 Bring-up-Mgmt (geplanter Renumber -> 10.40.99.12 nach Austausch beider Switches)") +req_patch = {"comments": "TP15 2026-07-23: grundkonfiguriert via REST (RouterOS). MAC 04:F4:1C:B4:15:58, " + "software-id RGZJ-UK0T, Serial HKA0AJJA52F. Ersetzt sw2 (Technik OG). Bring-up-Mgmt 10.40.99.13 " + "(+ VLAN1-Temp 10.40.41.213 waehrend Bring-up); Ziel 10.40.99.12. ap2 an ether24."} +if not DRY: + req("PATCH", f"/dcim/devices/{D['crs326-branch']['id']}/", req_patch) + +print("== crs326-core: Bring-up + geplantes Layout (tentativ, Status bleibt Planned)") +iface("crs326-core", "bridge", BR, "vlan-filtering=yes") +iface("crs326-core", "ether1", ETH, "Uplink-Trunk -> sw1 #12 (Bring-up-Pfad; native VLAN1 + tagged 10-100)", + mode="tagged", untagged=1, tagged=[10, 20, 30, 40, 50, 90, 99, 100]) +iface("crs326-core", "ether2", ETH, "Bring-up: Temp-Downlink zu branch ether1. GEPLANT (tentativ): Sonos VLAN40", + mode="tagged", untagged=1, tagged=[10, 20, 30, 40, 50, 90, 99, 100]) +iface("crs326-core", "ether3", ETH, "GEPLANT (tentativ): Sonos VLAN40 untagged") +iface("crs326-core", "ether4", ETH, "GEPLANT: ap1-Uplink-Trunk (ap1 wandert #15->#4 in Core-Swap-Session)") +iface("crs326-core", "ether5", ETH, "GEPLANT (tentativ): Sonos VLAN40 untagged") +iface("crs326-core", "vlan99", VIRT, "Mgmt SVI (VLAN99)", mode="tagged", tagged=[99]) +if not DRY: + v99c = find("/dcim/interfaces", device="crs326-core", name="vlan99") + brc = find("/dcim/interfaces", device="crs326-core", name="bridge") + req("PATCH", f"/dcim/interfaces/{v99c['id']}/", {"parent_interface": brc["id"]}) +set_mgmt("crs326-core", "vlan99", "10.40.99.11/24", "TP15 Bring-up-Mgmt-Ziel (VLAN99)") +if not DRY: + req("PATCH", f"/dcim/devices/{D['crs326-core']['id']}/", + {"comments": "TP15 2026-07-23: grundkonfiguriert via REST (Bring-up-Level, NICHT produktiv). " + "MAC 04:F4:1C:B4:17:76, software-id 4W04-URHT, Serial HKA0AREXJFM. Ersetzt sw1 = kuenftiger L3-Router. " + "Aktuell an sw1 #12. Mgmt 10.40.99.11 (VLAN99, erreichbar erst wenn sw1 #12 99T traegt) + VLAN1-Temp " + "10.40.41.211. Port-Layout ab ether3 tentativ (VLAN99 pro Port unter Vorbehalt), final in Core-Swap-Session."}) + +print("== sw1 (ALLNET): verifizierte Belegung (User-Survey 2026-07-22) -> loest UNVERIFIZIERT") +SW1 = [ + ("GE1", "Trunk (native VLAN1)"), + ("GE2", "Sonos (Access VLAN40) - Wohnzimmer"), + ("GE3", "Hybrid VLAN1 - Wohnzimmer Sideboard"), + ("GE4", "Trunk all (1U,10-100T) - ALT.-Standort ap1 (WLAN) - Wohnzimmer Sideboard"), + ("GE5", "Sonos (Access VLAN40) - Schlafzimmer"), + ("GE6", "Sonos (Access VLAN40) - Schlafzimmer"), + ("GE7", "Hybrid VLAN1 - Arbeitszimmer (Leitung evtl. tot)"), + ("GE8", "Trunk (1U,10-99T) -> crs326-branch Uplink - Arbeitszimmer"), + ("GE9", "Sonos (Access VLAN40) - Kinderzimmer"), + ("GE10", "Hybrid VLAN1 - Kinderzimmer"), + ("GE11", "cudy1 (Access VLAN10) - Technik UG"), + ("GE12", "crs326-core Bring-up/Config (aktuell Hybrid 1U, Ziel Trunk 1U,20-99T) - Technik UG"), + ("GE13", "pve (Trunk 1U,20-99T) - Technik UG"), + ("GE14", "Sonoff Dongle-M (Access VLAN30) - Technik UG"), + ("GE15", "ap1 (Trunk 1U,10-100T) - Technik UG"), + ("GE16", "Arris-Kabelmodem (Access VLAN100/WAN) - Technik UG"), +] +for name, desc in SW1: + iface("sw1", name, ETH, desc) + +print("== sw2 (ES208G): -> Decommissioning") +patch("/dcim/devices", D["sw2"], {"status": S_DECOM}, "sw2 status") +if not DRY: + req("PATCH", f"/dcim/devices/{D['sw2']['id']}/", + {"comments": "TP15 2026-07-23: wird durch crs326-branch ersetzt (Technik OG). Physischer Tausch = " + "sw2-Geraetekabel auf branch ether1-8, Uplink auf sw1 #8, dann sw2 stromlos/ausgebaut."}) + +print("\n".join(log)) +print(f"\n{'DRY-RUN - nichts geschrieben. Mit --apply ausfuehren.' if DRY else 'ANGEWENDET.'}")