TP15: CRS326-Bring-up (core+branch) + Branch-Tausch gegen sw2 durch

- Beide CRS326 grundkonfiguriert via REST (Governance-Ausnahme #2): core-zuerst
  als L2-Pfad, Zugriff auf Werks-.88.1 über temporäre ap1-.88.2+Masquerade
  (nach Bring-up entfernt). Winbox-MAC scheiterte (sync-timeout).
- branch produktiv, volle SOLL-Portbelegung (ether1 Uplink→sw1#8, 2/8 NAS-20T,
  3 Drucker/4 Dock VLAN10, 5/6 Sonos VLAN40, 7 macmini VLAN20, 24 ap2), mgmt
  .99.13; core grundkonfiguriert nicht produktiv (mgmt .99.11, an sw1#12).
- Branch-Tausch: sw2 physisch raus, Lifelines auf branch; Abnahme grün (NAS/
  macmini VLAN20, Dock VLAN10, Sonos VLAN40). Gotcha: ether3/5-Kabel vertauscht
  (Sonos-VLAN10-Leck/Dual-Homing) → gefixt.
- Nautobot modelliert + GraphQL-verifiziert (nautobot/ingest-crs326-ports.py):
  branch Active + Ports, core Planned + tentativ, sw1 verifizierte 16-Port-
  Belegung (löst UNVERIFIZIERT), sw2 Decommissioning.
- CRS326 in MikroMCP routers.yaml; Creds in ~/.secrets. Doku (teilprojekte/
  CLAUDE/ip-plan) nachgezogen; Core-Swap-Handoff-Prompt neu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 16:20:26 +02:00
parent dfec117e6b
commit b595c75eef
7 changed files with 358 additions and 3 deletions

View File

@@ -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) |

View File

@@ -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 |

View File

@@ -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.

View File

@@ -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).

View File

@@ -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.

View File

@@ -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"

View File

@@ -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.'}")