diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 8db3fd4..d98a08b 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -157,6 +157,16 @@ Kontrolle: MikroMCP (MikroTik) live · ALLNET = Klickanleitungen · Omada → Co - ⏳ **Offene Coverage-Arbeit → ausgelagert nach TP3.3** (Fortsetzung in neuer Session). ### TP3.3 — WLAN Coverage-Kur ▸ 🔜 TODO (Fortsetzung, neue Session) — `docs/wifi-p750w-coverage-and-map.md` +- ✅ **BEHOBEN 2026-08-13, noch am selben Abend (User: „aus Zeitgründen zuerst 11k/v, der nächste Call steht an").** `st-main` (`rrm=yes wnm=yes neighbor-group=bf-main`) auf beiden APs wiederhergestellt und an `cfg-main-5g`/`cfg-main-2g` gehängt — **via GC-ConfigPlan**, bewusst nicht per Direkt-Write: der Push läuft so NAS→Kabel→AP und nicht über die WLAN-Verbindung, die man gerade umkonfiguriert. `/system backup` je AP davor (`ap{1,2}-pre-steering-20260813.backup`). **Gegengeprüft am Gerät** (nicht am Plan-Status): `st-main rrm=true wnm=true ng=bf-main`, beide Main-Configs verweisen darauf. + - ⚠️ **Erwartete Nebenwirkung: alle Clients haben kurz reassoziiert** (ap1 25→2, ap2 7→14 unmittelbar danach) — `steering=` auf einer `wifi configuration` zu setzen re-applied die VAPs, auch die nicht angefassten. Nach ~2 Min eingependelt (ap1 15 / ap2 13, VLAN30 = 28 Hosts auf der Core-Bridge). **Nicht während eines Calls machen.** + - ✅ **In GC verankert:** Steering steht jetzt im AP-Zweig des Templates (`st-main`/`bf-main` als Konstante — es gibt genau eine Roaming-Domain pro Standort) + neues ComplianceFeature **`wifi-steering`**. Eine künftige Neuprovisionierung kann es damit nicht mehr stillschweigend verlieren — genau das war der Fehler vom 2026-08-08. + - ⏳ **Offen: der Praxistest.** Dieselbe Route noch einmal gehen. Reißt es weiterhin ab, ist es echte Coverage → Dead-Spot-Kartierung. +- 🔴 **Der Befund (2026-08-13): Steering war auf BEIDEN APs weg.** `/interface/wifi/steering` = **0 Einträge** auf ap1 *und* ap2 (live geprüft). Vor dem Rebuild war `st-main` mit `rrm=yes wnm=yes` + `neighbor-group=bf-main` auf beiden Main-Configs gesetzt (s. TP3.2 + TP3.1-Interim). Die Neuprovisionierung am 2026-08-08 aus `routeros/.secrets/ap{1,2}-prov-base.rsc` hat es **nicht wieder angelegt**. + - **Wirkung:** kein **802.11k** (Neighbor Reports → der Client kennt die Nachbar-APs gar nicht) und kein **802.11v** (BSS Transition → der AP kann den Client nicht wegschicken). Der Client bleibt am sterbenden AP kleben, bis die Verbindung zusammenbricht, und muss dann komplett neu scannen + assoziieren = **harter Abriss statt Übergabe**. Genau das Symptom „einmal durch die Wohnung, WLAN weg" (User-Erlebnis 2026-08-13 während eines Telegram-Videocalls, musste auf 5G ausweichen). + - ⚠️ **`ft=yes` ist KEIN Gegenbeweis** (auf beiden APs gesetzt, `ft-mobility-domain` beidseitig leer = gleicher Default, FT selbst also intakt). **802.11r macht die Übergabe schnell, sobald der Client sie beschließt — es bringt ihn nicht dazu, sie zu beschließen.** Ohne 11k/v fällt die Entscheidung viel zu spät. „FT ist an" ≠ „Roaming funktioniert". + - **Reihenfolge-Konsequenz:** Diese Regression **vor** der Dead-Spot-Kartierung beheben und dieselbe Route nachlaufen — sonst kartiert man Löcher, die in Wahrheit Roaming-Verhalten sind, und zieht daraus womöglich Hardware-/Platzierungs-Entscheidungen (s. „ap1 ins Sideboard"-Idee unten). + - **Weg (Governance):** Steering gehört ins **GC-Template** (`/interface/wifi/steering` + `steering=` an der Main-Configuration) → feature-scoped Deploy. Kein Direkt-Write. Betrifft `wifi-configuration` (bereits Compliance-Feature) + ein neues Feature `wifi-steering`. +- 💡 **Idee „ap1 ersetzt den Sideboard-Switch" (User 2026-08-13):** ap1 hat 4 freie Ports (`ether2–5`; `ether3/4/5` sind bereits Bridge-Ports mit `pvid=1`, `ether2` ist seit der ether1-Umstellung **kein** Bridge-Port mehr und müsste wieder aufgenommen werden) → die Wohnzimmer-Multimediageräte könnten direkt an ap1, der Smart-Managed-Switch entfiele. **Technisch machbar** (untagged VLAN40 wie `core ether2`), **aber platzierungsgebunden:** ein Media-Sideboard ist RF-mäßig einer der schlechtesten AP-Standorte (geschlossen, Metallgehäuse der AV-Geräte, niedrig). **User-Vorgabe: Platzierung hat Vorrang vor dem eingesparten Gerät.** Entscheidung daher erst **nach** der Steering-Reparatur + Re-Walk. **Problem:** Kanten/Terrasse grenzwertig — Clients (iPhone/iPad) landen bei **−76..−89 dBm** und springen zwischen allen 4 Main-Radios (ap1-5G/2,4G ↔ ap2-5G/2,4G) → „0–1 Striche + häufiger Abriss"; **Terrasse auch mittags** schlecht. FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". @@ -571,10 +581,21 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". 1. **DoH-Cloudflare-Gateway auf core wiederherstellen** (aktuell nur plain 1.1.1.1, keine Filterung/Account-Bindung): CA-Bundle importieren → `use-doh-server=https://px35ynlol3.cloudflare-gateway.com/dns-query verify-doh-cert=yes` (Referenz-Config liegt auf **ap1**). 2. **Intended config nach Nautobot zurückschreiben** (die Direkt-Writes liefen unter der CRS326-Cutover-Governance-Ausnahme). 3. **branch `192.168.88.1`-Rest verifizieren** (Bootstrap Rev 4 entfernt es in Zeile 70; via MCP noch nicht gegengeprüft, weil Registry-Umstellung MCP-Neustart braucht). - 4. **core `ether4`** prüfen/geraderücken — laut User evtl. sein NIC (Dock/Monitor), steht aber als tagged Trunk in der Bridge-VLAN-Tabelle. + 4. ✅ **core `ether4` GEKLÄRT (2026-08-13) — es ist das Wohnzimmer-Sideboard (Smart-Managed-Switch, nur VLAN40-Multimedia), und der Port ist der falsche.** Symptom war „TV meldet, LAN funktioniert nicht". + - **Ursache = VLAN-Mismatch, kein Kabeldefekt:** `ether4` hat `pvid=40` (untagged **rein** → VLAN40 ✓), ist in VLAN40 aber **tagged**-Member (untagged sind `ether2`/`ether3`/`ether5`) → alles **raus** ist 802.1Q-getaggt und der untagged Switch/TV verwirft es. Einseitige Kommunikation. + - **Belege:** TX 3,4 GB / 16 M Pakete gegen RX 452 KB / 1.338 Pakete, 0 stabil gelernte MACs; TV-Lease `10.40.40.11` (`70:AF:24:B8:C9:86`) **bound, last-seen 1h54m**, gleichzeitig ARP-Status **`failed`** — ein Gerät, das nachweislich sendet, aber nicht antwortet. Der bound-Lease ist kein Gegenbeweis: beim Einschalten macht der TV einen INIT-REBOOT-`REQUEST` auf seine alte IP, RouterOS quittiert mit ACK und markiert bound — **das ACK muss den Client nie erreichen**. + - **Herkunft:** `ether4` war als **ap1-Uplink-Trunk vorbereitet** („ap1 wandert #15→#4"); ap1 blieb auf `ether15`, in den übrigen Trunk-Port wanderte das Sideboard-Kabel. + - ✅ **BEHOBEN 2026-08-13 — am Port, nicht am Kabel (User-Vorgabe): „Patchfeld 1–10 ist durchnummeriert und komplett gepatcht, die Nummerierung wird nicht wegen einer Config geändert."** Richtig so — die Portnummer ist physische Infrastruktur. Also `ether4` sauber auf **untagged VLAN40** gezogen statt das Kabel zu verschieben. + - **Ausgeführt via Nautobot-GC-ConfigPlan** (`plan_type=manual`, 6 Zeilen, change_control_id `TP21.1-4 …`), nicht per Direkt-Write — Governance gewahrt. Vorher `/system backup` = `core-pre-ether4-vlan40-20260813.backup` (Regel 3). `ether4` aus den Tagged-Listen von VLAN 10/20/30/40/50/90 entfernt + als untagged-Member zu VLAN40 ergänzt. **`pvid=40` stand bereits** — das war nie das Fehlende. **Verifiziert:** `vlan40 untagged=ether2,ether3,ether4,ether5`, `tagged=ether1,ether8,ether15`; vlan99 (Mgmt) und vlan100 (WAN) unangetastet. Damit ist auch der Flood-Leak aus IoT/INFRA/MAIN zum Wohnzimmer-Switch weg. + - ⏳ **End-to-End-Beweis steht aus:** der TV antwortet nicht (ARP weiter `failed`, letzter Funkkontakt 2,5 h alt) — er ist mit hoher Wahrscheinlichkeit im Standby. **Beim nächsten Einschalten gegenprüfen:** MAC muss auf `ether4`/vid40 gelernt werden und `ping 10.40.40.11` durchgehen. + - 🔴 **INCIDENT + Lehre (2026-08-13): Der erste Deploy lief gegen das FALSCHE GERÄT (ap1 statt core).** Ich hatte 4 Device-UUIDs mit einer Sammelabfrage geholt und die **Reihenfolge angenommen**; die Nautobot-REST-API sortiert aber **alphabetisch** → `ap1, ap2, crs326-branch, crs326-core`. Der ConfigPlan trug damit ap1. + - **Schaden: keiner.** RouterOS wies jedes `set` ab (`invalid value for argument interface`), weil `ether8/13/15` auf einem 5-Port-hAP nicht existieren; die Ablehnung ist **pro Kommando atomar** → kein Teilzustand. ap1 nachgeprüft: VLAN-Tabelle vollständig, 25 WLAN-Clients, Mgmt `.99.11`. **Das war Glück, kein Können** — bei einem auf beiden Geräten *gültigen* Kommando (z.B. `tagged=ether1`) hätte es ap1s VLAN-Tabelle zerlegt. + - **Regel daraus: Device-UUIDs IMMER einzeln per Name auflösen, nie aus einer Sammelabfrage positional zuordnen** — und vor dem Deploy `plan.device.display` gegen den Zielnamen assert'en (im Skript jetzt so gemacht). + - **Bestätigt zum zweiten Mal:** der Deploy-**Job** meldete `SUCCESS` und der **Plan** `Completed`, obwohl auf dem Gerät nichts ankam. **Weder Job- noch Plan-Status sind ein Wirksamkeitsnachweis — nur die Gegenprüfung am Gerät zählt.** Der Fehl-Plan wurde gelöscht (irreführender Datensatz). + - ✅ **Lücke geschlossen (2026-08-13, User: „compliance rules! die Änderung machen wir"):** `/interface bridge port` + `/interface bridge vlan` sind jetzt für **alle drei Geräteklassen** im Template und als ComplianceFeatures scharf — s. TP22. 5. **`ap1`/`ap2` Neuprovisionierung** (WLAN-Rebuild, eigene Session) — ap1 ist Werksreset; danach greifen die `.99.11`/`.99.12`-Registry-Einträge. -### TP22 — Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) ▸ ⏳ **AKTUELL IN ARBEIT** (User-Entscheid 2026-08-13, nächstes Paket; aus TP21 ausgelagert 2026-08-08) +### TP22 — Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) ▸ ✅ **DURCH (2026-08-13)** — alle 5 Schritte erledigt (Schritt 4 per Entscheid „keine Secret-Injection"); Rest = Compliance-Backlog, gehört fachlich zu TP19 (aus TP21 ausgelagert 2026-08-08) - **Ziel:** die 4 Live-Geräte (crs326-core, crs326-branch, ap1, ap2) real unter **Nautobot Golden Config** nehmen — Backup → Compliance/Drift → intended → **Push (`enable_deploy`)**. Damit greift der Governance-Entscheid „Config exklusiv über Nautobot" operativ. Macht die aktuellen `routeros/.secrets/*.rsc`-Handoffs mittelfristig überflüssig. - **Stand aus TP21:** Plattform + Datenmodell + Git-Template + intended-config für core stehen; ap1/ap2 als reine APs zurückgeschrieben (`nautobot/ingest-ap-rebuild.py`, 2026-08-08). `enable_plan`/`enable_deploy` in `nautobot_config.py` noch **bewusst aus** (pull/read-only). - ✅ **Schritt 0 DURCH (2026-08-13) — Backup-Scrubbing + Repo-Neuanlage.** Vorgezogen aus Schritt 4, weil ohne Scrubbing jedes weitere Backup Secrets geleakt hätte. @@ -604,11 +625,25 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - 🔴 **Kernbefund: `napalm-ros` ist ein READ-ONLY-Treiber.** Der Default-Dispatcher wählt NAPALM (weil `Platform.network_driver_mappings` `napalm: ros` enthält) → Backup (`get_config`) geht, **`merge_config` scheitert grundsätzlich**, mit leerer Fehlermeldung (`E1015 … ``). **Fix:** in den Golden-Config-Settings (nicht in den Nornir-Settings!) `custom_dispatcher: {"mikrotik_routeros": "nornir_nautobot.plugins.tasks.dispatcher.mikrotik_routeros.NetmikoMikrotikRouteros"}` — dieser Dispatcher pusht via `netmiko_send_config`. Danach lief der Deploy sofort. - ⚠️ **Gescheiterter Deploy = sauberer Fehlschlag:** das Gerät blieb beim NAPALM-Versuch vollständig unverändert (kein Teilzustand). Zusätzlich meldet der Deploy-**Job** `SUCCESS`, obwohl der **Plan** auf `Failed` steht — wie beim Backup gilt: **nur der Objekt-Status zählt, nicht der Job-Status.** - ✅ **Pilot-Deploy: die DNS-Statics auf dem Core.** Vorher `/system backup` (`core-pre-tp22-first-deploy-20260813.backup`). ⚠️ **`config_set` eines generierten Plans ist schreibgeschützt**, und `plan_type=missing` erzeugt **nur `add`** — das hätte Duplikate ergeben (`ap1.bf.lan` gleichzeitig auf `.99.3` und `.99.11`). Deshalb **manueller Plan mit Purge-vor-Add**. Ergebnis verifiziert: `ap1→.99.11`, `ap2→.99.12`, `branch→.99.2`, weiterhin 11 Einträge, keine Duplikate. **Compliance 51/56.** +- ✅ **Schritt 5 DURCH (2026-08-13) — Datenmodell-Nachträge, `nautobot/ingest-tp22-datamodell.py`.** Quelle war durchgehend **das Gerät**, nicht die Doku: `bridge/port` (pvid), `bridge/vlan` (tagged/untagged), `bridge/host` (gelernte MACs je Port+VID) plus ARP/DHCP-Leases zur Identifikation der Gegenstellen. + - **`crs326-core`: von 5 auf 26 Ports.** Vorher waren 3 davon als „GEPLANT (tentativ)" modelliert und `ether1` zeigte auf das tote `sw1 #12`. Belegt (MAC-hart): `ether8`→crs326-branch, `ether13`→pve `.20.100`, `ether14`→Sonoff `.30.250`, `ether15`→ap1, `ether16`→Arris/WAN, `ether5`/`ether6`→Sonos Schlafzimmer `.195`/`.193`. Die Trunk-Sets sind **exakt wie am Gerät** übernommen, inkl. Schiefstände (z.B. trägt `ether1` kein VLAN20/99) — bewusst nicht geradegezogen. + - **`crs326-branch`:** `ether1`-Ziel `sw1 #8` → `crs326-core ether8`; `ether5`/`ether6` = Sonos Arbeitszimmer (aktuell kein Link, die Schlafzimmer-Paare hängen am Core); `ether9–23`+sfp waren als „disabled" modelliert, sind live aber **enabled** (Bootstrap Rev4) → korrigiert. + - **Standort-Fehler gefunden:** `crs326-branch` **und** `ap2` standen in Nautobot auf *Technik UG*, real *Technik OG* (die Doku war richtig, das Modell falsch) → korrigiert. + - **Status-Endzustände:** sw1/sw2/cudy2 → **Decommissioned** (physisch ausgebaut; „Decommissioning" heißt *Prozess läuft*). Nautobot liefert diesen Status mit, aber nur für `circuits.circuit` → `content_types` um `dcim.device` erweitert statt einen zweiten gleichnamigen anzulegen. `cudy1` → **Offline** (core `ether11` hat kein Link; der endgültige Teardown bleibt TP19). sw1s 4 Duplikat-Ports (`gi8/gi11/gi15/gi16` neben `GE*`) entfernt. + - ✅ **Compliance-neutral verifiziert:** Generate-Intended + Compliance über alle 4 Geräte neu gelaufen → weiterhin **51/56** mit exakt denselben 5 bekannten Drifts. Grund: der core-Zweig des Templates iteriert nur Interfaces mit Präfix `vlan` und hält die Bridge-VLAN-Tabelle als Jinja-Konstante. **Umkehrschluss als Dauerregel: die `vlanXX`-Descriptions auf dem Core sind NICHT kosmetisch** — sie landen als `comment=` in der intended config. + - 🔍 **Nebenbefund, halbkonfigurierte Ports (NICHT angefasst, gehört zu TP19/TP5):** `core ether11` (cudy1) hat `pvid=10` ohne VLAN10-Membership → funktionslos; `core ether3` hat `pvid=1` bei untagged-VLAN40-Membership; `core ether2` ist gleichzeitig statisches untagged-Member von VLAN1 **und** VLAN40; `branch ether9–23` tragen `pvid=20` ohne jede Membership. Alles Bring-up-Reste, betriebsneutral (kein Link), aber sie gehören in die VLAN1-Aufräumrunde. +- ✅ **Nachtrag 2026-08-13 — Compliance-Abdeckung erweitert auf 68 Checks (63/68 grün).** Drei neue ComplianceFeatures, alle auf Anhieb grün: + - **`wifi-steering`** (`/interface wifi steering`) — sichert die TP3.3-Reparatur ab. + - **`bridge-port`** + **`bridge-vlan`** (`/interface bridge port` / `/interface bridge vlan`) — der **L2-Layer war bis dahin komplett unüberwacht**: der core-Zweig emittierte gar keine Bridge-Sektion, der AP-Zweig nur die VLAN-Tabelle ohne Ports. Jetzt für alle drei Geräteklassen im Template. + - 🔑 **Bewusst als Literal-Block, nicht aus DCIM abgeleitet.** Der As-built trägt akzeptierte Schiefstände (`core ether3` ist untagged-Member von VLAN40, hat aber `pvid=1`; `core ether2` hängt zusätzlich in VLAN1; `branch ether9–23` haben `pvid=20` ohne Membership). Würde man `pvid` aus `untagged_vlan` generieren, meldete die Compliance genau diese bekannten Altlasten als Dauerdrift. Der Literal-Block liefert das, was hier gebraucht wird: **Drift-Erkennung gegen den Ist-Zustand**. + - ⚠️ **`/interface bridge` selbst bleibt außen vor** — trägt die gerätespezifische `admin-mac`, wäre pro Gerät verschieden und ist kein Fleet-Standard. + - 🐞 **Falle dabei: Compliance vergleicht gegen das letzte BACKUP, nicht gegen das Live-Gerät.** Nach dem Steering-Deploy meldete sie erst Drift auf `wifi-configuration`/`wifi-steering`, weil das Backup vom Nachmittag stammte. **Reihenfolge nach jeder Geräteänderung: Backup → Intended → Compliance.** - 📋 **Compliance-Backlog (5 verbleibende Drifts, alle ECHTE Altlasten — bewusst NICHT eigenmächtig entfernt, weil es potenzielle Rettungspfade sind):** ap1+ap2 `/ip dns static 192.168.88.1 router.lan` (Werks-defconf); `crs326-branch` `vlan10 "TEMP Mgmt VLAN10"` + `10.40.10.2` (Bring-up-Interim, CLAUDE.md-dokumentiert); `crs326-core` `10.40.41.211` (VLAN1-Bring-up-IP). Aufräumen gehört fachlich zu **TP19** (VLAN1-Retirement) und braucht eine bewusste Entscheidung. - 🔒 **GOVERNANCE — verbindliche Deploy-Regeln (festgeschrieben 2026-08-13, VOR dem Scharfschalten von `enable_deploy`):** 1. **Config Plans werden ausschliesslich feature-/manual-scoped erzeugt, NIEMALS als „full intended".** Ein Voll-Push schriebe wörtlich `passphrase="<>"` auf die APs (= alle WLAN-Clients raus, inkl. ~26 Shellys) bzw. `private-key="<>"` auf den Core (= WG-Hub tot, FM/DW-Spokes weg). **Jede Sektion, die `<>` enthält (`/interface wifi security`, `/interface wireguard`), ist vom Push generell ausgeschlossen.** 2. **Das AP-Template wird NIE zeilenweise auf ein laufendes Gerät gepusht.** Die Provisionierung setzt zwischenzeitlich `vlan-filtering=no` → das reisst den vlan99-Mgmt-Pfad ab, die restlichen Zeilen kämen über dieselbe SSH-Session nie an (halb konfigurierter AP). Voll-Reprovisionierung ausschliesslich per Datei-Upload + `/import` (`routeros/.secrets/ap{1,2}-prov-base.rsc`). Verschärfend: `device-mode=home` blockt auf den hAPs `scheduler`/`fetch` → die übliche Auto-Revert-Absicherung steht dort **nicht** zur Verfügung; der VLAN1-untagged-Rettungspfad darf nie wegoptimiert werden. 3. **Vor jedem Push ein `/system backup` auf dem Zielgerät.** Das GC-Backup ist wegen des Scrubbings **kein** vollwertiges Restore-Artefakt. +- ✅ **Schritt 4 ENTSCHIEDEN (User 2026-08-13): Variante (a) — keine Secret-Injection, bleibt wie es ist.** Ursprünglich geplant war „WLAN-PSKs + WG-Privkey via Nornir-Secrets injizieren"; die **Governance-Regel 1** (unten) hat das überholt: Sektionen mit `<>` sind vom Push generell ausgeschlossen, die Secrets werden also nie gebraucht. **Konsequenz, bewusst akzeptiert:** Secrets bleiben dauerhaft außerhalb von Modell und Push, DR läuft weiter über `~/.secrets/` + `routeros/.secrets/*` + `routeros/ap1-wg-hub.privkey`; „Gerät vollständig aus dem Modell wiederherstellbar" gilt nur *ohne* Secrets. Die verworfene Alternative (b) hätte Regel 1 aufweichen müssen — ein Render-Fehler hätte dann WLAN bzw. WG-Hub abgeschossen. **Damit ist TP22 inhaltlich abgeschlossen.** - **Schritte:** (1) **AP-GC-Template**: `mikrotik_routeros.j2` um `role == "Wireless AP"`-Zweig erweitern (Bridge/vlan-filtering/ether1-Trunk/vlan99-Mgmt/6 SSIDs **ohne** PSK/L3) → APs komplett aus GC generierbar; (2) **Nornir/NAPALM-Adoption** aller 4 (SecretsGroup „RouterOS Nornir"; **einheitliches Admin-PW seit 2026-08-08 vereinfacht das**), Backup/Compliance-Job grün; (3) **`enable_plan`+`enable_deploy`** scharf — erst Dry-Run/Plan, dann kontrollierter Push; (4) **Secret-Handling** WG-Privkey (core) + WLAN-PSKs (bleiben aus dem Git-Template, via Nornir-Secrets injizieren); (5) Datenmodell-Nachträge: branch/core-Ports final, sw1 (außer Betrieb) + cudy1-Status. - **Nutzen:** Drift-Erkennung + reproduzierbarer Rebuild jedes Geräts aus dem Modell. - **Abh.:** TP21 ✅ (Plattform/Modell/Templates), TP20 ✅, einheitliches Admin-PW ✅. **Danach ist der kritische Pfad frei für TP5 (Deny-by-default) → TP6 (CAKE).** @@ -624,7 +659,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". --- ## Reihenfolge / Abhängigkeiten -> 🎯 **AKTUELLER KRITISCHER PFAD (Stand 2026-07-16, überschreibt die Reihenfolge unten): TP20 (Platform-Postgres) → TP21 (Nautobot) — Ziel: fertig vor CRS326-Produktivsetzung, spätestens Montag 2026-07-20.** TP18 (Garage-S3) und die Komodo-Abhängigkeit wurden am selben Tag als Blocker entfernt/entkoppelt (s. TP20), um diese Deadline zu halten — beide werden später nachgezogen, nicht mehr vorher abgewartet. **➡️ Stand-Update 2026-08-08:** TP20/TP21-Plattform steht, Core-Cutover (TP21.1) + WLAN-Rebuild (TP3) + VLAN20-Migration (TP14) sind durch. Neuer Rest-Pfad: **TP22 (Device-Adoption + Compliance-Push) → TP5 (Deny-by-default) → TP6 (CAKE, das Ziel).** Kleine Follow-ups davor: DoH-Cloudflare auf core, USV (TP15). **➡️ Stand-Update 2026-08-13 (User-Entscheid): TP22 ist jetzt aktiv das nächste Arbeitspaket** — TP17 (Shelly-Migration) ist parallel dazu vollständig abgeschlossen (alle Shellys auf VLAN30, User-bestätigt). Grund für den Nautobot-First-Ansatz: Governance-Entscheid „Config exklusiv über Nautobot" (s. oben) macht Nautobot zur Voraussetzung für praktisch jede weitere Router-/Switch-Änderung. Die Punkte 3-4 unten (Omada-PVID-Fix etc.) sind **obsolet** (User-Entscheid 2026-07-22 — Omada spielt seit TP15-Korrektur keine Rolle mehr, nie wieder aufgreifen) — ganze Liste braucht mal eine Aufräum-Session, hier nur der aktuelle Fokus vorangestellt. +> 🎯 **AKTUELLER KRITISCHER PFAD (Stand 2026-07-16, überschreibt die Reihenfolge unten): TP20 (Platform-Postgres) → TP21 (Nautobot) — Ziel: fertig vor CRS326-Produktivsetzung, spätestens Montag 2026-07-20.** TP18 (Garage-S3) und die Komodo-Abhängigkeit wurden am selben Tag als Blocker entfernt/entkoppelt (s. TP20), um diese Deadline zu halten — beide werden später nachgezogen, nicht mehr vorher abgewartet. **➡️ Stand-Update 2026-08-08:** TP20/TP21-Plattform steht, Core-Cutover (TP21.1) + WLAN-Rebuild (TP3) + VLAN20-Migration (TP14) sind durch. Neuer Rest-Pfad: **TP22 (Device-Adoption + Compliance-Push) → TP5 (Deny-by-default) → TP6 (CAKE, das Ziel).** Kleine Follow-ups davor: DoH-Cloudflare auf core, USV (TP15). **➡️ Stand-Update 2026-08-13 (Abschluss): TP22 ist DURCH** (alle 5 Schritte; Fleet adoptiert, Deploy-Pipeline scharf, Datenmodell as-built, Secret-Handling entschieden) → **kritischer Pfad jetzt: TP5 (Deny-by-default) → TP6 (CAKE)**. Kleine Follow-ups davor: DoH-Cloudflare auf core, Kabel `core ether4`→`ether2` (Sideboard/TV), DC-USV (TP15). Historisch: **TP22 war das nächste Arbeitspaket** — TP17 (Shelly-Migration) ist parallel dazu vollständig abgeschlossen (alle Shellys auf VLAN30, User-bestätigt). Grund für den Nautobot-First-Ansatz: Governance-Entscheid „Config exklusiv über Nautobot" (s. oben) macht Nautobot zur Voraussetzung für praktisch jede weitere Router-/Switch-Änderung. Die Punkte 3-4 unten (Omada-PVID-Fix etc.) sind **obsolet** (User-Entscheid 2026-07-22 — Omada spielt seit TP15-Korrektur keine Rolle mehr, nie wieder aufgreifen) — ganze Liste braucht mal eine Aufräum-Session, hier nur der aktuelle Fokus vorangestellt. > ✅ **TP16 (CF-Tunnel Round-Robin/macvlan-Host-Isolation → reziprokes 3-Tunnel-HA)** KERN UMGESETZT 2026-07-10 (#1033 strukturell weg). Offen nur Access-Strang (WARP/`vw`) = user-gated. As-Built `docs/cf-tunnel-mapping.md §10`. 1. ~~**TP1 (GE8) → TP2 (Omada Controller).**~~ ✅ erledigt 2026-06-15. 2. ~~**TP4 (Router-Cutover L3-Infra + WAN-Edge).**~~ ✅ erledigt 2026-06-15 (L3) + 2026-07-01 (Edge-Cutover, FB physisch weg). diff --git a/nautobot/ingest-tp22-datamodell.py b/nautobot/ingest-tp22-datamodell.py new file mode 100644 index 0000000..6e4aa5f --- /dev/null +++ b/nautobot/ingest-tp22-datamodell.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""BF-Netzwerk -> Nautobot: TP22 Schritt 5 -- Datenmodell-Nachtraege (2026-08-13). + +WAS DAS FIXT +------------ +Das DCIM-Modell stand noch auf dem Stand VOR dem Core-Cutover (2026-08-08): + + * `crs326-core` hatte 5 von 26 Ports modelliert, davon 3 als "GEPLANT (tentativ)" -- + das Geraet ist seit dem Cutover der produktive L3-Router mit voller Belegung. + * `crs326-branch` ether1 zeigte auf `sw1 #8`; sw1 existiert nicht mehr, der Uplink + haengt an `crs326-core ether8`. + * `crs326-branch` + `ap2` standen in "Technik UG"; beide stehen real in "Technik OG" + (TP15-Standort-Mapping: UG = core, OG = branch + ap2). + * sw1/sw2/cudy2 waren "Decommissioning" (= Prozess laeuft), sind aber physisch weg. + * sw1 trug 4 doppelt modellierte Ports (`gi8/gi11/gi15/gi16` neben `GE8/GE11/GE15/GE16`). + +DATENQUELLE = DAS GERAET, NICHT DIE DOKU +---------------------------------------- +Die Portbelegung unten ist am 2026-08-13 live erhoben worden (MikroMCP): +`/interface/bridge/port` (pvid), `/interface/bridge/vlan` (tagged/untagged), +`/interface/bridge/host` (gelernte MACs je Port+VID) und `/ip/arp` + DHCP-Leases zur +Identifikation der Gegenstellen. Wo kein MAC gelernt wurde, steht "frei" bzw. die +Unsicherheit explizit im Description-Text -- nicht geraten. + +COMPLIANCE-SICHERHEIT (wichtig) +------------------------------- +Der core-Zweig in `mikrotik_routeros.j2` iteriert NUR Interfaces mit Praefix `vlan` +(GW-IPs) und haelt die Bridge-VLAN-Tabelle als Jinja-Konstante. Ether-Ports sind damit +compliance-neutral -- dieses Skript kann die intended config nicht verschieben. +UMGEKEHRT GILT: die `vlanXX`-Descriptions auf dem Core NICHT anfassen, die landen als +`comment=` in der intended config. Dieses Skript ruehrt sie nicht an. + + export NAUTOBOT_TOKEN=... + python3 nautobot/ingest-tp22-datamodell.py # Dry-Run + python3 nautobot/ingest-tp22-datamodell.py --apply # schreiben +""" +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: + 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): + if str(obj["id"]).startswith("DRY"): + return obj + delta = {} + for k, v in payload.items(): + cur = obj.get(k) + # Relationen kommen als {"id": ...}, Choice-Felder (type/mode) als {"value": ...}. + if isinstance(cur, dict): + cur = cur.get("id", cur.get("value")) + if cur != v: + delta[k] = v + if not delta: + return obj + if DRY: + log.append(f" ~ {label}: {sorted(delta)} (dry-run)") + return obj + out = req("PATCH", f"{path}/{obj['id']}/", delta) + log.append(f" ~ {label}: {sorted(delta)}") + return out + + +def sid(name): + return find("/extras/statuses", name=name)["id"] + + +ETH, SFP, BR = "1000base-t", "10gbase-x-sfpp", "bridge" + +S_ACTIVE = sid("Active") +S_OFFLINE = sid("Offline") + +# Fuer Geraete liefert Nautobot ab Werk nur "Decommissioning" (= Prozess laeuft). +# sw1/sw2/cudy2 sind physisch ausgebaut -> Endzustand. "Decommissioned" existiert bereits +# als mitgelieferter Status, aber nur fuer `circuits.circuit` -> content_types erweitern +# statt einen zweiten, gleichnamigen Status anzulegen. ("Inventory" waere falsch, das +# hiesse "auf Lager, einsatzbereit".) +st_decom = find("/extras/statuses", name="Decommissioned") +if not st_decom: + st_decom = goc("/extras/statuses", {"name": "Decommissioned"}, + {"name": "Decommissioned", "color": "9e9e9e", + "content_types": ["dcim.device"], + "description": "Ausgebaut/ausser Betrieb (Endzustand, kein laufender Prozess)"}, + label="Status Decommissioned") +elif "dcim.device" not in st_decom["content_types"]: + patch("/extras/statuses", st_decom, + {"content_types": sorted(set(st_decom["content_types"]) | {"dcim.device"})}, + "Status Decommissioned +dcim.device") +S_DECOM_DONE = st_decom["id"] + +vg = find("/ipam/vlan-groups", name="BF") +V = {vid: find("/ipam/vlans", vid=vid, vlan_group=vg["id"]) for vid in (1, 10, 20, 30, 40, 50, 90, 99, 100)} +LOC_OG = find("/dcim/locations", name="Technik OG")["id"] + +DEVS = ("crs326-core", "crs326-branch", "ap2", "sw1", "sw2", "cudy1", "cudy2") +D = {n: find("/dcim/devices", name=n) for n in DEVS} + + +def iface(dev, name, typ, desc, mode=None, untagged=None, tagged=None, enabled=True): + obj = goc("/dcim/interfaces", {"device": dev, "name": name}, + {"device": D[dev]["id"], "name": name, "type": typ, + "status": S_ACTIVE, "description": desc, "enabled": enabled}, + label=f"{dev}:{name}") + p = {"description": desc, "type": typ, "enabled": enabled} + if mode: + p["mode"] = mode + if untagged is not None: + p["untagged_vlan"] = V[untagged]["id"] + if mode == "access": + # Nautobot verweigert mode=access, solange noch tagged_vlans haengen + # ("Clear tagged_vlans to set mode to access") -> im selben PATCH mitleeren. + p["tagged_vlans"] = [] + obj = patch("/dcim/interfaces", obj, p, f"{dev}:{name}") + if tagged is not None and not str(obj["id"]).startswith("DRY"): + # ACHTUNG: `tagged_vlans` liest sich per REST als null, auch wenn korrekt gespeichert + # (bekannte Nautobot-Falle, s. nautobot/README.md) -> dieser Vergleich schlaegt IMMER + # an und der PATCH laeuft bei jedem Durchlauf. Der Write ist idempotent, die Zeile im + # Log ist also kein Drift-Signal. Verifikation nur per GraphQL. + cur = {t["id"] for t in (obj.get("tagged_vlans") or [])} + want = {V[t]["id"] for t in tagged} + if cur != want: + if DRY: + log.append(f" ~ {dev}:{name}: tagged_vlans -> {sorted(tagged)} (dry-run)") + else: + req("PATCH", f"/dcim/interfaces/{obj['id']}/", {"tagged_vlans": sorted(want)}) + log.append(f" ~ {dev}:{name}: tagged_vlans -> {sorted(tagged)}") + return obj + + +# --- crs326-core: volle As-built-Belegung (live erhoben 2026-08-13) ------------------- +# Trunk-Sets exakt wie in der Bridge-VLAN-Tabelle des Geraets -- ether1 traegt z.B. KEIN +# VLAN20/99 (Bring-up-Rest), ether13 nur VLAN20. Bewusst nicht "geradegezogen". +T_FULL = [10, 20, 30, 40, 50, 90, 99] # ether8 (branch), ether15 (ap1) +T_EX_SW1 = [10, 30, 40, 50, 90, 100] # ether1, Ex-sw1-Uplink +FREI = "frei (pvid=1, kein Link, keine VLAN-Membership)" + +CORE = [ + ("bridge", BR, "vlan-filtering=yes; L3-Router-Bridge (SVIs vlan10-100 haengen hier)", None, None, None, True), + ("ether1", ETH, "frei -- Ex-Uplink zu sw1 GE12 (sw1 ausser Betrieb 2026-08-08). Trunk-Reste " + "+ frame-types=admit-only-vlan-tagged aus dem Bring-up, kein Link.", + "tagged", 1, T_EX_SW1, True), + ("ether2", ETH, "frei (Access VLAN40 vorbereitet, kein Link). " + "WARN zusaetzlich statisches untagged-Member von VLAN1 (Bring-up-Altlast).", + "access", 40, None, True), + ("ether3", ETH, "Wohnzimmer Sideboard (untagged VLAN40) -- kein Link. " + "WARN pvid=1 (Ex-Hybrid-VLAN1): Ingress landet in VLAN1, Egress untagged VLAN40.", + "access", 40, None, True), + # Descriptions sind auf 255 Zeichen begrenzt -> Langfassung steht in teilprojekte.md TP21.1/4. + ("ether4", ETH, "Wohnzimmer-Sideboard: Smart-Managed-Switch mit den VLAN40-Multimediageraeten " + "(u.a. TV 10.40.40.11). 2026-08-13 von Trunk auf untagged VLAN40 gezogen " + "(GC ConfigPlan) -- vorher Egress tagged = Einbahnstrasse.", + "access", 40, None, True), + ("ether5", ETH, "Sonos SYMFONISK Schlafzimmer (R), 10.40.40.195 (Access VLAN40)", "access", 40, None, True), + ("ether6", ETH, "Sonos SYMFONISK Schlafzimmer (L), 10.40.40.193 (Access VLAN40); " + "untagged-Membership nur dynamisch aus pvid=40", "access", 40, None, True), + ("ether7", ETH, FREI, None, None, None, True), + ("ether8", ETH, "Uplink-Trunk -> crs326-branch ether1 (native VLAN1 + tagged 10/20/30/40/50/90/99)", + "tagged", 1, T_FULL, True), + ("ether9", ETH, FREI, None, None, None, True), + ("ether10", ETH, FREI, None, None, None, True), + ("ether11", ETH, "cudy1 (Access VLAN10, Nachfolger sw1 GE11) -- kein Link. " + "WARN pvid=10 ohne VLAN10-Membership -> Port aktuell funktionslos.", + "access", 10, None, True), + ("ether12", ETH, FREI, None, None, None, True), + ("ether13", ETH, "pve / Proxmox 10.40.20.100 -- Trunk: native VLAN1 + tagged VLAN20", + "tagged", 1, [20], True), + ("ether14", ETH, "Sonoff Dongle-M 10.40.30.250 (Access VLAN30)", "access", 30, None, True), + ("ether15", ETH, "ap1 Uplink-Trunk (PoE-Injektor) -- native VLAN1 + tagged 10/20/30/40/50/90/99", + "tagged", 1, T_FULL, True), + ("ether16", ETH, "Arris-Kabelmodem = WAN (Access VLAN100); vlan100-SVI zieht die Liwest-Public-IP", + "access", 100, None, True), +] +CORE += [(f"ether{n}", ETH, FREI, None, None, None, True) for n in range(17, 25)] +CORE += [("sfp-sfpplus1", SFP, "frei (disabled)", None, None, None, False), + ("sfp-sfpplus2", SFP, "frei (disabled)", None, None, None, False)] + +print("== crs326-core: As-built-Portbelegung (26 Ports, live 2026-08-13)") +for name, typ, desc, mode, unt, tag, en in CORE: + iface("crs326-core", name, typ, desc, mode=mode, untagged=unt, tagged=tag, enabled=en) + +# --- crs326-branch: Korrekturen ------------------------------------------------------ +print("== crs326-branch: Uplink-Ziel + Leerport-Realitaet") +iface("crs326-branch", "ether1", ETH, + "Uplink-Trunk -> crs326-core ether8 (native VLAN1 + tagged 10/20/30/40/50/90/99, kein 100)", + mode="tagged", untagged=1, tagged=[10, 20, 30, 40, 50, 90, 99]) +for n in (5, 6): + iface("crs326-branch", f"ether{n}", ETH, + "Sonos Arbeitszimmer (10.40.40.191/.192, Access VLAN40) -- aktuell kein Link", + mode="access", untagged=40) +for n in range(9, 24): + iface("crs326-branch", f"ether{n}", ETH, + "frei (kein Link; pvid=20 aus Bootstrap Rev4, aber keine VLAN-Membership -> " + "funktionslos. NICHT disabled -- die alte 'disabled'-Notiz war stale.)") +for n in (1, 2): + iface("crs326-branch", f"sfp-sfpplus{n}", SFP, "frei (kein Link, nicht disabled)") + +# --- Standorte + Geraetestatus ------------------------------------------------------- +print("== Standorte + Status") +patch("/dcim/devices", D["crs326-branch"], {"location": LOC_OG}, "crs326-branch location") +patch("/dcim/devices", D["ap2"], {"location": LOC_OG}, "ap2 location") + +patch("/dcim/devices", D["sw1"], {"status": S_DECOM_DONE, + "comments": "ALLNET Core-Switch, ausser Betrieb 2026-08-08 -- ersetzt durch crs326-core " + "(L3-Router + Core-Switch, Technik UG). Portbelegung GE1-GE16 bleibt als " + "historischer Beleg stehen."}, "sw1") +patch("/dcim/devices", D["sw2"], {"status": S_DECOM_DONE, + "comments": "Omada ES208G, physisch abgebaut 2026-07-23 (TP15 Branch-Tausch) -- " + "ersetzt durch crs326-branch."}, "sw2") +patch("/dcim/devices", D["cudy2"], {"status": S_DECOM_DONE, + "comments": "Cudy WR3600E V1.0, ausser Betrieb, wird nicht reaktiviert (TP19)."}, "cudy2") +patch("/dcim/devices", D["cudy1"], {"status": S_OFFLINE, + "comments": "Cudy WR3600E V1.0, Uebergangs-WLAN 'elektrosmog.c' waehrend des WLAN-Umbaus. " + "Seit dem TP3-Rebuild (2026-08-08) nicht mehr noetig; core ether11 hat kein Link " + "(Stand 2026-08-13) -> Offline. Endgueltiger Teardown = TP19."}, "cudy1") + +# --- sw1: doppelt modellierte Ports entfernen ---------------------------------------- +# gi8/gi11/gi15/gi16 stammen aus einem zweiten Ingest-Lauf mit anderer Namenskonvention +# und duplizieren GE8/GE11/GE15/GE16 (die die reichere Description tragen). +print("== sw1: Duplikat-Ports (gi*) entfernen") +for gi in ("gi8", "gi11", "gi15", "gi16"): + obj = find("/dcim/interfaces", device="sw1", name=gi) + if not obj: + continue + if DRY: + log.append(f" - sw1:{gi} (dry-run)") + else: + req("DELETE", f"/dcim/interfaces/{obj['id']}/") + log.append(f" - sw1:{gi}") + +print("\n".join(log) or " (keine Aenderungen)") +print("\nDRY-RUN -- mit --apply schreiben." if DRY else "\nGeschrieben.") diff --git a/nautobot/mikrotik_routeros.j2 b/nautobot/mikrotik_routeros.j2 index 6ccac6f..2afd7b3 100644 --- a/nautobot/mikrotik_routeros.j2 +++ b/nautobot/mikrotik_routeros.j2 @@ -28,6 +28,46 @@ # Vorbereitung des Cutovers und beschreiben nicht mehr den Sollzustand. # ===================================================================================== +# [0] L2-FABRIC — Bridge-Ports + Bridge-VLAN-Tabelle. +# Literal-Block, KEINE Ableitung aus DCIM: der As-built traegt bewusste Schiefstaende +# (ether3 ist untagged-Member von VLAN40, hat aber pvid=1; ether2 haengt zusaetzlich in +# VLAN1). Wuerde man pvid aus `untagged_vlan` generieren, meldete die Compliance genau +# diese akzeptierten Altlasten als Dauerdrift. Der Block ist damit das, was Golden Config +# fuer einen handgebauten L2-Layer leisten soll: Drift-Erkennung gegen den Ist-Zustand. +# `/interface bridge` selbst ist bewusst NICHT abgedeckt (traegt die geraetespezifische +# admin-mac -> waere pro Geraet verschieden und ist kein Fleet-Standard). +/interface bridge port add bridge=bridge comment=defconf frame-types=admit-only-vlan-tagged interface=ether1 +/interface bridge port add bridge=bridge comment=defconf interface=ether2 pvid=40 +/interface bridge port add bridge=bridge comment=defconf interface=ether3 +/interface bridge port add bridge=bridge comment=defconf interface=ether4 pvid=40 +/interface bridge port add bridge=bridge comment=defconf interface=ether5 pvid=40 +/interface bridge port add bridge=bridge comment=defconf interface=ether6 pvid=40 +/interface bridge port add bridge=bridge comment=defconf interface=ether7 +/interface bridge port add bridge=bridge comment=defconf interface=ether8 +/interface bridge port add bridge=bridge comment=defconf interface=ether9 +/interface bridge port add bridge=bridge comment=defconf interface=ether10 +/interface bridge port add bridge=bridge comment=defconf interface=ether11 pvid=10 +/interface bridge port add bridge=bridge comment=defconf interface=ether12 +/interface bridge port add bridge=bridge comment=defconf interface=ether13 +/interface bridge port add bridge=bridge comment=defconf interface=ether14 pvid=30 +/interface bridge port add bridge=bridge comment=defconf interface=ether15 +/interface bridge port add bridge=bridge comment=defconf interface=ether16 pvid=100 +{% for n in range(17, 25) %} +/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }} +{% endfor %} +/interface bridge port add bridge=bridge comment=defconf interface=sfp-sfpplus1 +/interface bridge port add bridge=bridge comment=defconf interface=sfp-sfpplus2 +# ether4 = Wohnzimmer-Sideboard: 2026-08-13 von Trunk auf untagged VLAN40 gezogen (TP21.1/4). +/interface bridge vlan add bridge=bridge untagged=ether1,ether2 vlan-ids=1 +/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=10 +/interface bridge vlan add bridge=bridge tagged=ether8,ether13,ether15 vlan-ids=20 +/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 untagged=ether14 vlan-ids=30 +/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 untagged=ether2,ether3,ether4,ether5 vlan-ids=40 +/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=50 +/interface bridge vlan add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=90 +/interface bridge vlan add bridge=bridge tagged=ether8,ether15,bridge vlan-ids=99 +/interface bridge vlan add bridge=bridge tagged=ether1 untagged=ether16 vlan-ids=100 + # [1] INTERFACE-LISTS /interface list add comment=TP15 name=WAN /interface list add comment=TP15 name=LAN @@ -108,6 +148,31 @@ /ip address add address={{ obj.primary_ip4.address }} comment=TP21-mgmt-target interface=vlan99 network={{ obj.primary_ip4.host.split(".")[:3] | join(".") }}.0 /ip dns set servers=10.40.99.1 /ip route add comment=TP21-default dst-address=0.0.0.0/0 gateway=10.40.99.1 + +# L2-FABRIC — wie beim Core ein Literal-Block (Begruendung dort). +# ether9-23 tragen pvid=20 OHNE VLAN-Membership (Bootstrap-Rev4-Rest, funktionslos) — +# bewusst so abgebildet, damit eine spaetere Bereinigung als Drift sichtbar wird. +/interface bridge port add bridge=bridge comment=defconf interface=ether1 +/interface bridge port add bridge=bridge comment=defconf interface=ether2 +/interface bridge port add bridge=bridge comment=defconf interface=ether3 pvid=10 +/interface bridge port add bridge=bridge comment=defconf interface=ether4 pvid=10 +/interface bridge port add bridge=bridge comment=defconf interface=ether5 pvid=40 +/interface bridge port add bridge=bridge comment=defconf interface=ether6 pvid=40 +/interface bridge port add bridge=bridge comment=defconf interface=ether7 pvid=20 +/interface bridge port add bridge=bridge comment=defconf interface=ether8 +{% for n in range(9, 24) %} +/interface bridge port add bridge=bridge comment=defconf interface=ether{{ n }} pvid=20 +{% endfor %} +/interface bridge port add bridge=bridge comment=defconf interface=ether24 +/interface bridge port add bridge=bridge comment=defconf disabled=yes interface=sfp-sfpplus1 +/interface bridge port add bridge=bridge comment=defconf disabled=yes interface=sfp-sfpplus2 +/interface bridge vlan add bridge=bridge comment=MAIN tagged=ether1,ether24,bridge untagged=ether3,ether4 vlan-ids=10 +/interface bridge vlan add bridge=bridge comment=INFRA tagged=ether1,ether24,bridge,ether2,ether8 untagged=ether7 vlan-ids=20 +/interface bridge vlan add bridge=bridge comment=IOT tagged=ether1,ether24,bridge vlan-ids=30 +/interface bridge vlan add bridge=bridge comment=MEDIA tagged=ether1,ether24,bridge untagged=ether5,ether6 vlan-ids=40 +/interface bridge vlan add bridge=bridge comment=UNTRUSTED tagged=ether1,ether24,bridge vlan-ids=50 +/interface bridge vlan add bridge=bridge comment=GUEST tagged=ether1,ether24,bridge vlan-ids=90 +/interface bridge vlan add bridge=bridge comment=MGMT tagged=ether1,ether24,bridge vlan-ids=99 {% elif obj.role.name == "Wireless AP" %} {% set ap = obj.get_config_context()["ap"] %} {% set mgmt_host = obj.primary_ip4.host %} @@ -150,13 +215,23 @@ /interface wifi security add authentication-types={{ z.auth }} ft={{ z.ft }}{% if z.ft == "yes" %} ft-over-ds={{ z.ft_over_ds }}{% endif %} name=sec-{{ z.name }} passphrase="<>" {% endfor %} +# [3b] STEERING — 802.11k (rrm: Neighbor Reports) + 802.11v (wnm: BSS Transition). +# OHNE das klebt der Client am sterbenden AP, bis die Verbindung abreisst; 802.11r/FT +# macht die Uebergabe nur SCHNELL, es loest sie nicht aus. Genau das fehlte nach dem +# TP3-Rebuild auf beiden APs und hat Videocalls beim Durchgehen abgerissen (TP3.3). +# `neighbor-group` ist site-weit identisch -- nur dann gelten ap1/ap2 als Nachbarn. +# Konstante statt ConfigContext: es gibt genau EINE Roaming-Domain pro Standort. +{% set steering = {"name": "st-main", "neighbor_group": "bf-main"} %} +/interface wifi steering add name={{ steering.name }} neighbor-group={{ steering.neighbor_group }} rrm=yes wnm=yes + # [4] CONFIGURATION — main sitzt auf den Master-Radios und bringt den Channel mit # (je Band eine configuration); alle anderen Zonen sind Slave-VAPs ohne channel=. +# steering= haengt nur an den Master-Zonen (RouterOS exportiert es nach ssid=). {% for z in ap.zones %} {% if z.master %} {% for r in ap.radios %} {% if r.key in z.bands %} -/interface wifi configuration add channel=ch-{{ r.key }} country={{ ap.country }} datapath=dp-{{ z.name }} mode=ap name=cfg-{{ z.name }}-{{ r.key }} security=sec-{{ z.name }} ssid={{ z.ssid }} +/interface wifi configuration add channel=ch-{{ r.key }} country={{ ap.country }} datapath=dp-{{ z.name }} mode=ap name=cfg-{{ z.name }}-{{ r.key }} security=sec-{{ z.name }} ssid={{ z.ssid }} steering={{ steering.name }} {% endif %} {% endfor %} {% else %} @@ -164,6 +239,17 @@ {% endif %} {% endfor %} +# [4b] BRIDGE-PORTS — ether2 fehlt bewusst: seit der Uplink-Umstellung auf ether1 (PoE) +# ist er KEIN Bridge-Port mehr. ether1 traegt kein comment=defconf (beim Rebuild +# neu angelegt), die uebrigen schon — deshalb hier nicht ueber einen Kamm geschoren. +/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface=ether3 +/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface=ether4 +/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface=ether5 +{% for r in ap.radios %} +/interface bridge port add bridge={{ ap.bridge }} comment=defconf interface={{ r.iface }} +{% endfor %} +/interface bridge port add bridge={{ ap.bridge }} interface={{ uplink }} + # [5] BRIDGE-VLAN — Nur Master-Radios sind Bridge-Ports (Slave-VAPs forwarden darueber), # deshalb muessen wifi1/wifi2 tagged-Member je Client-VLAN sein. Die frueheren # "Ausnahmen" fallen hier aus den Daten heraus: IOT hat bands=["2g"] -> nur wifi2.