TP28: ap3 (cAP ax) live als CAP, ap1 funkstill, Compliance 130/130 ueber 5 Geraete

Der dritte Access-Point laeuft: 10.40.99.13 auf vlan99, Uplink ether1 -> core ether21,
reiner CAP unter crs326-core, Kanaele 5180/ax/Ce/I und 2437/ax (ch6) -- am Geraet
nachgemessen, nicht aus dem Plan abgeschrieben. Der Kanalplan stammt aus TP25 vom
2026-08-24; mein erster Entwurf hatte ihn neu erfunden und waere bei ch1 statt ch6
gelandet. Lehre: den TP-Body lesen, nicht nur den Quick-Index.

ap1 hat bewusst KEINE Provisioning-Regel mehr: es steht bis zu seinem Umzug ans
Sideboard direkt neben ap3 und wuerde auf 5180 kollidieren. Geraet bleibt erreichbar,
die Objekte ch-*-ap1 / cfg-main-*-ap1 bleiben fuer den Umzug stehen.

Bring-up in zwei Phasen ueber ein Patchkabel ap3 ether2 -> core ether22, weil ueber
ether1 nichts ging (s.u.). Loop-Schutz: ether21 war waehrend Phase 1 abgeschaltet und
wurde im selben Befehl gegen ether22 getauscht. RouterOS 7.20.8 -> 7.24.1 plus
RouterBOOT (zwei Reboots, TP27-Kriterium erfuellt). Die anderen vier stehen auf 7.24 --
die Angleichung ist ein eigener Durchgang, der Core-Reboot ist ein Standort-Ausfall.

Nautobot: DeviceType "cAP ax", Device ap3 mit 16 Interfaces, primary_ip4, secrets_group,
ConfigContext 5g=ch36 2g=ch6 extra_ports=[]. Nebenkorrektur: ap1 stand auf "Wohnzimmer
Sideboard", steht physisch aber in Technik UG (User-Bestaetigung).

Sechs Funde, alle als Gotcha in CLAUDE.md und im Handoff:
- ether1 ist am Werksgeraet WAN: Discovery aus, Input-TCP verworfen, ICMP aber erlaubt.
  Es pingt, und das liest sich wie "erreichbar". MikroMCP hilft nicht -- Port 80 ist zu.
- pvid ist Ingress, tagged/untagged ist Egress. Ein Lease hing ewig auf "offered", weil
  der ACK getaggt rausging und ein Werksgeraet das nicht versteht.
- `cap enabled=yes` reicht nicht: jedes Master-Radio braucht configuration.manager=capsman,
  sonst bietet der CAP seine Radios gar nicht an -- state=Ok, radio-Tabelle leer.
- Ein Geraet ohne secrets_group erzeugt eine LUECKE, keine Fehlermeldung: Backup scheitert
  still, keine Compliance-Eintraege, Zahl bleibt unauffaellig bei 104/104. Eine
  gleichbleibende Compliance-Zahl nach dem Hinzufuegen eines Geraets ist ein BEFUND.
- CAPsMAN verteilt keine Login-Passwoerter, nur WLAN-PSKs. Der Geraete-Login ist eine
  lokale DB pro Geraet; das einheitliche Admin-PW ist eine Konvention von Hand.
- Firewall-Regelnummern aus `print where` sind Handles, keine Positionen. Beweis ist der
  Zaehler, nicht die Zahl.

Abnahme: 3 CAPs state=Ok, VAP-Satz vollstaendig und running, 20 IOT-Clients auf ap3 mit
0 % Ping-Verlust ueber VLAN30 (bewusst nicht ueber die Registration-Table abgenommen),
Compliance 130/130 / 0 Drifts, Core-Temp-Pfad restlos abgeraeumt.

Offen: TP25-Messrunde (braucht Rundgang), PW-Rotation TP30, MikroMCP-Neustart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 17:54:07 +02:00
parent f7d26db7a6
commit 1006f91e6a
9 changed files with 509 additions and 31 deletions

View File

@@ -9,7 +9,8 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
1. **`docs/teilprojekte.md`** (Quick-Index-Block) — kanonischer Stand, Scope, Abhängigkeiten, **offene** TPs. **1 Read reicht.** 1. **`docs/teilprojekte.md`** (Quick-Index-Block) — kanonischer Stand, Scope, Abhängigkeiten, **offene** TPs. **1 Read reicht.**
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat. 2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**. 3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**.
4. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift. 4. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken.
5. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift.
## Was du sofort wissen musst (Inventur 2026-08-27) ## Was du sofort wissen musst (Inventur 2026-08-27)
@@ -59,9 +60,9 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
| ~~Omada Access-Switch (sw2)~~ | — | ❌ **abgebaut 2026-07-23** (TP15 Branch-Tausch) — durch `crs326-branch` in Technik OG ersetzt, physisch entfernt | | ~~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.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** (2026-08-16 verifiziert) · **TP15 ✅: ersetzt sw2** (L2-Switch), volle Portbelegung (ether1 Uplink-Trunk, 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-branch** (CRS326, Technik OG) | **10.40.99.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** (2026-08-16 verifiziert) · **TP15 ✅: ersetzt sw2** (L2-Switch), volle Portbelegung (ether1 Uplink-Trunk, 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.1** (VLAN99-SVI = L3-GW; same-subnet-Fallback `10.40.10.1`) · Identity `crs326-core` | RouterOS **7.24** · `admin`/`~/.secrets` (`CRS326_CORE_*`) · MikroMCP routerId `crs326-core` ✅ **live** (2026-08-16 verifiziert) · **TP15/TP21.1 ✅ 2026-08-08: PRODUKTIVER L3-Router** (ersetzt ap1) — Routing/DHCP (7 Server + 54 Leases)/DNS (**DoH zum CF-Gateway mit `verify-doh-cert=yes`**, TP23 Stufe 1 ✅ 2026-08-18; `servers=1.1.1.1` nur noch Bootstrap) · **NTP-Server für den Standort**/NAT/WG-Hub (fm+dw live); WAN `vlan100`=Liwest `90.146.33.177` · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 | | **crs326-core** (CRS326, Technik UG) | **10.40.99.1** (VLAN99-SVI = L3-GW; same-subnet-Fallback `10.40.10.1`) · Identity `crs326-core` | RouterOS **7.24** · `admin`/`~/.secrets` (`CRS326_CORE_*`) · MikroMCP routerId `crs326-core` ✅ **live** (2026-08-16 verifiziert) · **TP15/TP21.1 ✅ 2026-08-08: PRODUKTIVER L3-Router** (ersetzt ap1) — Routing/DHCP (7 Server + 54 Leases)/DNS (**DoH zum CF-Gateway mit `verify-doh-cert=yes`**, TP23 Stufe 1 ✅ 2026-08-18; `servers=1.1.1.1` nur noch Bootstrap) · **NTP-Server für den Standort**/NAT/WG-Hub (fm+dw live); WAN `vlan100`=Liwest `90.146.33.177` · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 |
| hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **TP28 ✅ 2026-08-17: reiner CAP** (Manager = `crs326-core`, `cap enabled=yes`, `discovery-interfaces=vlan99`) — Uplink `ether1` (PoE-Port, `poe-out=off`) an crs326-core `ether15`; SSIDs/Kanäle kommen vom Manager, ch36/ch1 · MAC D0:EA:11:4D:40:E0 · Config-Referenz `routeros/.secrets/ap1-prov-base.rsc` | | hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **TP28 ✅ 2026-08-17: reiner CAP** (Manager = `crs326-core`) · 🔴 **FUNKSTILL seit 2026-08-30**: keine Provisioning-Regel am Manager, weil ap3 daneben steht und denselben Kanal 5180 fährt. Gerät bleibt erreichbar. Beim Umzug ans Sideboard → `5260/Ce` (Band B, DFS) und Uplink auf `core ether3` — Uplink `ether1` (PoE-Port, `poe-out=off`) an crs326-core `ether15`; SSIDs/Kanäle kommen vom Manager, ch36/ch1 · MAC D0:EA:11:4D:40:E0 · Config-Referenz `routeros/.secrets/ap1-prov-base.rsc` |
| hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **TP28 ✅ 2026-08-17: reiner CAP** — Uplink `ether1` (PoE) an crs326-branch `ether24`; ch44/ch11 (TP25: ch149 war unter ETSI leistungsbegrenzt) · MAC D0:EA:11:4D:40:3A · Config `routeros/.secrets/ap2-prov-base.rsc` | | hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **TP28 ✅ 2026-08-17: reiner CAP** — Uplink `ether1` (PoE) an crs326-branch `ether24`; ch44/ch11 (TP25: ch149 war unter ETSI leistungsbegrenzt) · MAC D0:EA:11:4D:40:3A · Config `routeros/.secrets/ap2-prov-base.rsc` |
| **cAP ax (ap3)** | **10.40.99.13** (VLAN99, Ziel) · Identity `ap3` | ⏳ **Bring-up offen (TP28-Nachtrag, 2026-08-30)** · Modell `cAPGi-5HaxD2HaxD`, Werkszustand · Uplink `ether1` → **core `ether21`** (Trunk seit 2026-08-30 ✅; Gerät hängt und hat Strom — `link-ok`, MAC `D0:EA:11:FF:16:9D` = Label-E01) · Kanäle **`5180/Ce` + `2437`** (TP25-Plan 08-24) · 🔴 **nur 2 Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out) — kein `ether3/4/5` wie am hAP ax² · Dateien `routeros/tp28-ap3-bringup.rsc` + `-lifeline-abbau.rsc` | | **cAP ax (ap3)** | **10.40.99.13** (VLAN99) · Identity `ap3` | ✅ **LIVE seit 2026-08-30** (TP28-Nachtrag) · `cAPGi-5HaxD2HaxD`, RouterOS **7.24.1** + RouterBOOT 7.24.1 (Flotte sonst 7.24 — Differenz bewusst, s. TP27) · reiner CAP, Manager `crs326-core` · Uplink `ether1` → **core `ether21`** (Trunk) · Kanäle **`5180/ax/Ce/I`** (5 GHz) + **`2437/ax`** (ch6) nach TP25-Plan · 🔴 **nur 2 Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out, **kein** Bridge-Port) → `ap.extra_ports=[]` · `device-mode=basic` (scheduler/fetch/romon **erlaubt**, anders als ap1/ap2) · Dateien `routeros/tp28-ap3-phase{1,2}-*.rsc` + `-service-accounts.rsc` + `-werksreste.rsc` |
| **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. | | **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. |
| NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) | | NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) |
| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** | | platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** |
@@ -104,6 +105,41 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
## Kritische Gotchas ## Kritische Gotchas
- 📜 **~~ALLNET-Gotchas (1 Telnet-VTY, `save` nicht vergessen)~~ — gegenstandslos seit 2026-08-08** (Gerät ausgebaut). Nur noch historisch relevant, s. `switch/README.md`. - 📜 **~~ALLNET-Gotchas (1 Telnet-VTY, `save` nicht vergessen)~~ — gegenstandslos seit 2026-08-08** (Gerät ausgebaut). Nur noch historisch relevant, s. `switch/README.md`.
- 🔑 **CAPsMAN verteilt KEINE Login-Passwörter — nur WLAN-PSKs.** Zwei verschiedene Dinge heißen
„Passwort": die **WPA-Passphrase** (`/interface wifi security`) liegt am Manager und wird an die
CAPs verteilt ✅; der **Geräte-Login** (`/user`) ist eine **lokale Benutzer-DB auf jedem Gerät** ❌.
Beleg: ap1 trägt `admin`, `ha`, `nautobot` lokal, mit gerätelokalen `last-logged-in`-Zeitstempeln.
Das einheitliche Admin-PW ist eine **von Hand angewendete Konvention**, keine Automatik — und
logisch kann es nicht anders sein: ein Gerät *wird* erst zum CAP, nachdem sich jemand eingeloggt hat.
- 🔴 **Ein neuer CAP braucht `configuration.manager=capsman` auf JEDEM Master-Radio.**
`/interface wifi cap set enabled=yes` allein reicht nicht — ohne das Feld bietet der CAP seine
Radios dem Manager gar nicht erst an. **Symptom (2026-08-30, ap3):** `remote-cap print` zeigt
`state=Ok`, aber `/interface wifi radio print` am Manager listet für dieses Gerät **nichts**, und
weil die Provisioning-Regel dann nicht matcht, sucht man den Fehler in der `radio-mac`. Im
Werkszustand ist das Feld leer (= `local`).
- 🔌 **Werks-MikroTik am Trunk: `ether1` ist WAN, und das sieht aus wie ein Netzproblem.** Beim
cAP ax (und den hAPs) steht `ether1` per defconf in der Interface-Liste `WAN`. Folge: Discovery
aus (**kein MNDP → in `/ip neighbor` unsichtbar**), Input-TCP verworfen (22/80/8291/8728 **zu**),
**ICMP aber erlaubt** — es *pingt*, was sich wie „erreichbar" liest. Kein Registry-Eintrag und
kein MikroMCP hilft, denn Port 80 ist zu. **Der Weg rein ist die LAN-Seite** (`ether2` oder das
Werks-WLAN); dort `/interface list member remove [find where interface=ether1]` — das ist der Fix.
- 🔀 **`pvid` ist INGRESS, `tagged`/`untagged` ist EGRESS.** Ein Port mit `pvid=99`, der in der
*tagged*-Liste von VLAN99 steht, nimmt untagged an und antwortet **getaggt**. Symptom
(2026-08-30): ein DHCP-Lease hing ewig auf `offered` und ARP auf `failed` — der DISCOVER kam an,
der ACK nie. Für ein Werksgerät gehört der Port **untagged** in die VLAN-Zeile.
- 🕳️ **Ein neues Gerät ohne `secrets_group` erzeugt eine LÜCKE, keine Fehlermeldung.** Ohne
Secrets Group scheitert der Golden-Config-**Backup**-Job still; die *intended* config wird
trotzdem sauber gerendert, aber ohne Backup entstehen **keine Compliance-Einträge** — und die
Gesamtzahl bleibt unauffällig (bei uns 104/104), weil ein Gerät ohne Ergebnisse auch keine Drift
meldet. 🔴 **Merkregel: eine gleichbleibende Compliance-Zahl nach dem Hinzufügen eines Geräts ist
ein BEFUND, kein Erfolg.** Erst `130/130` bewies, dass ap3 wirklich geprüft wird. Zweite
Voraussetzung: die dedizierten Service-Accounts `nautobot`/`ha` (TP-PWD) existieren auf einem
Werksgerät nicht.
- 🔢 **Firewall-Regelnummern aus `print where` sind Handles, keine Positionen.** Nach einem
`place-before` zeigte `/ip firewall filter print terse where comment~"..."` die Nummer **60**,
während `drop` auf **59** stand — die Regel lag trotzdem davor. Beweis ist der **Zähler**: eine
Regel hinter dem `drop` kann nicht zählen. Wer nach der Zahl geht statt nach `print stats`,
„repariert" eine funktionierende Regel kaputt.
- 🔌 **`crs326-core` hat KEIN PoE-out** (`/interface ethernet poe` existiert dort gar nicht, - 🔌 **`crs326-core` hat KEIN PoE-out** (`/interface ethernet poe` existiert dort gar nicht,
CRS326-24G-2S+). Ein AP an einem Core-Port braucht eigene Stromversorgung. Der **cAP ax** bringt CRS326-24G-2S+). Ein AP an einem Core-Port braucht eigene Stromversorgung. Der **cAP ax** bringt
dafür **Netzteil UND Gigabit-PoE-Injector im Karton** mit (Produktseite, 2026-08-30) — die dafür **Netzteil UND Gigabit-PoE-Injector im Karton** mit (Produktseite, 2026-08-30) — die

View File

@@ -0,0 +1,82 @@
# Handoff 2026-08-30 — ap3 (cAP ax) live, TP28 wieder geöffnet und durchgezogen
## Was jetzt gilt
**ap3 funkt.** MikroTik cAP ax (`cAPGi-5HaxD2HaxD`), `10.40.99.13/24` auf `vlan99`, Uplink
`ether1` → **`crs326-core ether21`**, reiner CAP unter `crs326-core`. Kanäle **`5180/ax/Ce/I`**
(5 GHz) und **`2437/ax`** (ch6) — am Gerät nachgemessen, nicht aus dem Plan abgeschrieben.
**ap1 ist funkstill.** Es steht bis zu seinem Umzug ans Sideboard direkt neben ap3 und würde auf
5180 kollidieren. Es hat deshalb **keine Provisioning-Regel** am Manager; das Gerät bleibt
erreichbar, die Objekte `ch-*-ap1`/`cfg-main-*-ap1` bleiben für den Umzug stehen.
**Compliance: 130/130, 0 Drifts über 5 Geräte.**
## Was noch offen ist
1. **TP25-Messrunde** — `bash docs/tp25-runde.sh ap3-bringup`, dann `docs/tp25-walk-messung.sh`
für den Gehweg Technik UG ↔ OG. Braucht einen Rundgang mit dem Telefon.
Leitfrage: **deckt ap3 allein ab, was ap1 abgedeckt hat, oder klafft bis zum Umzug eine Lücke?**
2. **Passwort-Rotation (TP30)** — das gemeinsame MikroTik-Admin-PW ist in dieser Session
verbrannt (ungefilterte Ausgabe des `mikromcp`-`env`-Blocks aus `~/.claude.json`). ap3 trägt es
bereits, hängt also in der Rotation mit drin. Fehlen zusätzlich in `~/.secrets/exports.zsh`:
`MCP_MIKROMCP_ROUTER_AP3_PASS`, `MCP_MIKROMCP_CRS326_CORE_PASS`, `MCP_MIKROMCP_CRS326_BRANCH_PASS`.
3. **MikroMCP-Neustart** — Registry (jetzt `bf-infra`, mit ap3) wird nur beim Start gelesen.
4. **Versions-Differenz** — ap3 läuft auf **7.24.1**, die anderen vier auf **7.24**. Bewusst:
7.20.8 (Januar, long-term) wäre ein halbes Jahr Abstand zur `/interface wifi`-Implementierung
des Managers gewesen. Die Angleichung der Flotte ist ein eigener TP27-Durchgang — der
Core-Reboot ist ein Standort-Ausfall und damit Terminentscheidung.
5. **Kabel `ap3 ether2` → `core ether22`** kann raus. Seit Phase 2 ist `ether2` kein Bridge-Port
mehr, das Kabel ist inert.
6. **Offene Frage aus TP22:** auf den APs deckt **keine ComplianceRule** `/interface wifi` ab.
Genau dort sitzt `configuration.manager=capsman` — ein Feld, dessen Fehlen das WLAN eines APs
still verhindert (s. unten). Ob dafür eine Regel sinnvoll ist, ist noch nicht entschieden.
## Die sechs Funde, die Zeit gekostet haben
1. **`ether1` ist am Werksgerät WAN.** Discovery aus (kein MNDP), Input-TCP verworfen
(22/80/8291/8728 zu) — **aber ICMP erlaubt**. Es *pingt*, und das liest sich wie „erreichbar".
MikroMCP hilft nicht: es ist ein HTTP-Client auf Port 80, und der ist zu. Gegenprobe, dass der
Core nicht schuld war: Regel 44 `MAIN->MGMT` ist `action=accept` **ohne** Port-Einschränkung.
2. **`pvid` ist Ingress, `tagged`/`untagged` ist Egress.** Ein DHCP-Lease hing ewig auf `offered`,
ARP auf `failed`: `pvid=99` nahm den DISCOVER an, aber der Port stand in der *tagged*-Liste von
VLAN99 — der ACK ging getaggt raus, ein Werksgerät versteht das nicht.
3. **`cap enabled=yes` reicht nicht.** Jedes Master-Radio braucht zusätzlich
`configuration.manager=capsman`, sonst bietet der CAP seine Radios dem Manager gar nicht an:
`remote-cap print` zeigt `state=Ok`, `/interface wifi radio print` bleibt leer — und man sucht
den Fehler in der `radio-mac`.
4. **Ein neues Gerät ohne `secrets_group` erzeugt eine Lücke, keine Fehlermeldung.** Der Backup-Job
scheitert still, die *intended* config wird trotzdem gerendert, und ohne Backup entstehen keine
Compliance-Einträge. Die Zahl blieb bei **104/104** — unauffällig. 🔴 **Eine gleichbleibende
Compliance-Zahl nach dem Hinzufügen eines Geräts ist ein Befund, kein Erfolg.** Zweite
Voraussetzung: die Service-Accounts `nautobot`/`ha` gibt es auf einem Werksgerät nicht.
5. **CAPsMAN verteilt keine Login-Passwörter.** Zentral sind die **WLAN-PSKs**; der Geräte-Login
(`/user`) ist eine lokale DB pro Gerät. Das einheitliche Admin-PW ist eine von Hand angewendete
Konvention.
6. **Firewall-Regelnummern aus `print where` sind Handles, keine Positionen.** Nach `place-before`
zeigte die TEMP-Regel **60** neben `drop` auf **59** — lag aber davor. Beweis ist der Zähler
(`print stats`), nicht die Zahl.
## Zwei Korrekturen an bestehender Doku
- **Der Kanalplan war schon da.** TP25 hatte ihn am **2026-08-24** für genau dieses Gerät gerechnet
(DFS vom User genehmigt). Ich hatte ihn zunächst neu erfunden und wäre bei ch1 statt **ch6**
gelandet. Merkregel: den TP-Body lesen, nicht nur den Quick-Index.
- **Der Port war `ether21`, nicht `ether22`.** Der Befund lag in den Daten: in der dynamischen
`pvid`-VLAN1-Zeile stand `current-untagged=…,ether21`. Gegenprobe ist
`/interface bridge host print where interface=etherN` — dort steht die MAC des Gegenübers.
- **ap1 stand in Nautobot auf „Wohnzimmer Sideboard"**, physisch aber in **Technik UG**. Der
Eintrag war der Ziel-Standort nach dem geplanten Umzug. Korrigiert.
## Dateien
| Datei | Zweck |
|---|---|
| `routeros/tp28-ap3-phase1-erreichbar.rsc` | über die LAN-Seite: `ether1` aus WAN, Mgmt auf `vlan99` |
| `routeros/tp28-ap3-phase2-cap.rsc` | über `vlan99`: Werks-WLAN weg, CAP-Modus, `vlan-filtering=yes` |
| `routeros/tp28-ap3-service-accounts.rsc` | `nautobot`/`ha` + Gruppen (sonst kein GC-Backup) |
| `routeros/tp28-ap3-werksreste.rsc` | die drei Drifts aus dem ersten Compliance-Lauf |
| `nautobot/ingest-ap3-device.py` | DeviceType, Device, 16 Interfaces, IP, SecretsGroup |
Alle vier `.rsc` tragen Platzhalter statt Secrets; die echten Dateien werden zur Laufzeit ins
Scratchpad generiert und danach gelöscht.

View File

@@ -70,12 +70,15 @@ liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur d
(an core `ether15`), ap2 **`10.40.99.12`** (an branch `ether24`), beide **reine CAPs** — (an core `ether15`), ap2 **`10.40.99.12`** (an branch `ether24`), beide **reine CAPs** —
`cap enabled=yes`, `discovery-interfaces=vlan99`. Die APs halten **keine eigene WLAN-Config** mehr; `cap enabled=yes`, `discovery-interfaces=vlan99`. Die APs halten **keine eigene WLAN-Config** mehr;
Kanäle, SSIDs, Security und Steering kommen vom Manager. 802.11r/k/v aktiv, `ft-over-ds=yes`. Kanäle, SSIDs, Security und Steering kommen vom Manager. 802.11r/k/v aktiv, `ft-over-ds=yes`.
🆕 **Dritter AP `ap3` (cAP ax) in Arbeit seit 2026-08-30** — **TP28 dafür wieder geöffnet**. 🆕 **Dritter AP `ap3` (cAP ax) ✅ LIVE seit 2026-08-30** (TP28 dafür wieder geöffnet):
Core-seitig fertig: **`ether21` ist Trunk** (10/20/30/40/50/90/99 tagged, Muster `ether15`) und im `10.40.99.13`, Uplink `ether1` → **core `ether21`** (Trunk), reiner CAP, **RouterOS 7.24.1**
GC-Template nachgezogen, Compliance weiter **104/104 / 0 Drifts**. ap3 hängt bereits am Kabel und hat Strom (`link-ok`, MAC `D0:EA:11:FF:16:9D` = Label-E01), (die anderen vier auf 7.24 — Angleichung ist ein eigener TP27-Durchgang). Kanäle
der Bring-up steht aber noch aus. Ziel-IP **`10.40.99.13`**, Kanäle **`5180/Ce` + `2437`** nach dem **`5180/ax/Ce/I`** + **`2437/ax`** (ch6) nach dem TP25-Plan vom 2026-08-24, am Gerät nachgemessen.
TP25-Kanalplan vom 2026-08-24. ⚠️ **ap1s Radios gehen vorher aus** (steht bis zum Umzug neben ap3); 🔴 **ap1 ist funkstill** — es steht bis zu seinem Umzug ans Sideboard direkt neben ap3 und würde
ap1 kehrt später auf `5260/Ce` (DFS) zurück, ap2 auf `5300/Ce`. auf 5180 kollidieren; es hat deshalb bewusst **keine Provisioning-Regel** am Manager, bleibt aber
erreichbar. Beim Umzug: ap1 → `5260/Ce` (Band B, DFS, **Frequenz-Liste statt Einzelwert**),
ap2 → `5300/Ce`, Uplink ap1 → `core ether3`.
**Compliance jetzt 130/130 / 0 Drifts über 5 Geräte** (vorher 104/104 über 4).
- **SSDP-Relay** (TP7.1, **nachgebessert 08-25**): LXC **108 `mcast-relay`** auf pve, zweibeinig - **SSDP-Relay** (TP7.1, **nachgebessert 08-25**): LXC **108 `mcast-relay`** auf pve, zweibeinig
`10.40.10.21` (VLAN10) + `10.40.40.21` (VLAN40) — dafür trägt `core ether13` beide VLANs; mDNS macht `10.40.10.21` (VLAN10) + `10.40.40.21` (VLAN40) — dafür trägt `core ether13` beide VLANs; mDNS macht
weiterhin der Core. **Läuft seit 08-25 ohne `--ssdpUnicastAddr`** — die Flagge verwarf die M-SEARCH weiterhin der Core. **Läuft seit 08-25 ohne `--ssdpUnicastAddr`** — die Flagge verwarf die M-SEARCH
@@ -242,7 +245,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
> | TP23 | ✅ | DNS-Policy (Stufe 1+2 ✅) | 2026-08-26 | > | TP23 | ✅ | DNS-Policy (Stufe 1+2 ✅) | 2026-08-26 |
> | TP24 | 🔜 | DC-USV | — | > | TP24 | 🔜 | DC-USV | — |
> | TP25 | ✅ | WLAN-Stabilisierung (Kern erledigt) | 2026-08-24 | > | TP25 | ✅ | WLAN-Stabilisierung (Kern erledigt) | 2026-08-24 |
> | TP28 | ⏳ | WLAN/CAPsMAN — **wieder geöffnet**: dritter AP `ap3` (cAP ax) | Kern ✅ 2026-08-18 | > | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 |
> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — | > | TP29 | 💡 | Anwesenheitserkennung (Idee) | — |
> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — | > | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — |
> | TP31 | ✅ | Reverse Proxy Caddy (Schritt 1–4 ✅) | — | > | TP31 | ✅ | Reverse Proxy Caddy (Schritt 1–4 ✅) | — |
@@ -1345,7 +1348,7 @@ die Redundanzfrage oben.
<a id="tp28"></a> <a id="tp28"></a>
### TP28 — WLAN über CAPsMAN auf `crs326-core` ▸ ⏳ **WIEDER GEÖFFNET 2026-08-30: dritter AP `ap3`** ### TP28 — WLAN über CAPsMAN auf `crs326-core` ▸ ✅ **ap3 LIVE (2026-08-30)** — wieder geöffnet und durchgezogen
> **Der CAPsMAN-Kern ist und bleibt ✅ KOMPLETT (2026-08-18)** — der Abschnitt darunter ist > **Der CAPsMAN-Kern ist und bleibt ✅ KOMPLETT (2026-08-18)** — der Abschnitt darunter ist
> unverändert der Archivtext inkl. Post-Mortem. Wieder geöffnet wurde das TP am 2026-08-30, > unverändert der Archivtext inkl. Post-Mortem. Wieder geöffnet wurde das TP am 2026-08-30,
@@ -1503,19 +1506,36 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört
> Bridge-Host-Tabelle stellen.** `/interface bridge host print where interface=etherN` nennt die > Bridge-Host-Tabelle stellen.** `/interface bridge host print where interface=etherN` nennt die
> MAC des Gegenübers — hier `D0:EA:11:FF:16:9D`, exakt die `E01` vom ap3-Label. > MAC des Gegenübers — hier `D0:EA:11:FF:16:9D`, exakt die `E01` vom ap3-Label.
##### Stand 2026-08-30 (diese Session) ##### Stand 2026-08-30 — ✅ DURCH
- ✅ Backups core+ap1 (`pre-ap3-20260830.backup`/`.rsc` auf den Geräten), Compliance-Ausgangsstand - ✅ **`core ether21` ist Trunk** (10/20/30/40/50/90/99 tagged, Muster `ether15`), GC-Template
**104/104, 0 Drifts**. nachgezogen; AP-Zweig rendert Bridge-Ports aus **`ap.extra_ports`** statt hartkodiert.
- ✅ **`core ether21` ist Trunk**: tagged in 10/20/30/40/50/90/99, Muster = `ether15` (ap1). - ✅ **Zugang über `ap3 ether2` → `core ether22`** (Patchkabel). Über `ether1` war ap3
`pvid=1`/`frame-types=admit-all` unverändert; das VLAN1-Bein entsteht dynamisch aus dem pvid. grundsätzlich nicht administrierbar, s. Gotchas unten. Loop-Schutz: `ether21` war während
- ✅ **GC-Template nachgezogen und gepusht** (Commit `e25d654b`): `ether21` im Core-L2-Block, und Phase 1 abgeschaltet, danach im selben Befehl getauscht (`ether22` aus, `ether21` an).
der AP-Zweig rendert die Bridge-Ports jetzt aus **`ap.extra_ports`** statt hartkodiert - ✅ **Bring-up in zwei Phasen**: Phase 1 (`ether1` aus der WAN-Liste, `10.40.99.13/24` auf
`ether3/4/5`. **Compliance danach wieder 104/104, 0 Drifts** — das beweist, dass ap1/ap2 `vlan99`, Flotten-PW) über das Kabel, Phase 2 (Werks-WLAN weg, CAP-Modus,
zeichengleich rendern. `slaves-datapath`, `vlan-filtering=yes`) über `vlan99`.
- ✅ Bring-up-Dateien geschrieben: [`routeros/tp28-ap3-bringup.rsc`](../routeros/tp28-ap3-bringup.rsc) - ✅ **RouterOS 7.20.8 → 7.24.1 + RouterBOOT 7.24.1** (zwei Reboots, TP27-Kriterium
+ [`routeros/tp28-ap3-lifeline-abbau.rsc`](../routeros/tp28-ap3-lifeline-abbau.rsc). `current-firmware = upgrade-firmware`). ⚠️ Die anderen vier stehen auf **7.24**; die
- 🔜 **Offen:** physischer Bring-up (Strom + `/import`), ap1-Radios aus, CAPsMAN-Objekte für ap3, Angleichung auf 7.24.1 ist ein eigener TP27-Durchgang (Core-Reboot = Standort-Ausfall).
Nautobot-Adoption, PW-Rotation, MikroMCP-Registry, Messrunde. - ✅ **ap1 funkstill** (Provisioning-Regeln entfernt + `remote-cap provision`), gegengeprüft
**am CAP**: nur noch `wifi1`/`wifi2` mit `running=false`, keine dynamischen VAPs.
- ✅ **ap3 funkt**: `cap-wifi5` = **`5180/ax/Ce/I`**, `cap-wifi6` = **`2437/ax`**, voller
VAP-Satz (2 Master + 7 Slaves), alle `running=true`.
- ✅ **Datapath bewiesen**: 20 IOT-Clients auf ap3, Ping über VLAN30 mit **0 % Loss**.
*(Bewusst nicht über die Registration-Table abgenommen — die zeigte im 14-h-Ausfall
25 zufriedene Clients. Bei statisch reservierten Shellys erzeugt ein Roam keine neue
DHCP-Lease, deshalb ist hier Datenverkehr das belastbare Signal.)*
- ✅ **Nautobot**: DeviceType `cAP ax`, Device `ap3` mit 16 Interfaces, `primary_ip4`,
`secrets_group`, ConfigContext (`5g=ch36 2g=ch6 extra_ports=[]`).
Nebenkorrektur: ap1 stand auf „Wohnzimmer Sideboard", steht aber in **Technik UG**.
- ✅ **Compliance 130/130, 0 Drifts über 5 Geräte.** Die drei Werksreste, die der erste
Lauf mit ap3 gemeldet hat (defconf-DNS, 11 defconf-Firewallregeln, fehlende
`/ip service`-Härtung), sind mit `routeros/tp28-ap3-werksreste.rsc` abgeräumt.
- 🔜 **Offen:** TP25-Messrunde (`bash docs/tp25-runde.sh ap3-bringup`) — braucht einen
Rundgang. Leitfrage: deckt ap3 allein ab, was ap1 abgedeckt hat?
- 🔜 **Offen:** Passwort-Rotation (TP30) — das gemeinsame Admin-PW ist in dieser Session
verbrannt; ap3 trägt es bereits mit, hängt also in der Rotation mit drin.
##### Was am cAP ax anders ist als am hAP ax² — und was daran wehtut ##### Was am cAP ax anders ist als am hAP ax² — und was daran wehtut
- 🔴 **Nur ZWEI Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out) — kein `ether3/4/5`. Der - 🔴 **Nur ZWEI Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out) — kein `ether3/4/5`. Der

View File

@@ -97,9 +97,22 @@ DEVICE_CHANNELS = {
# 5 GHz mit 40 MHz: ap1 ch36 belegt 5170-5210 (36+40), ap2 ch44 belegt 5210-5250 # 5 GHz mit 40 MHz: ap1 ch36 belegt 5170-5210 (36+40), ap2 ch44 belegt 5210-5250
# (44+48) -> benachbart, ueberlappungsfrei, beide mit /I-Indoor-Freigabe und 18 dBm. # (44+48) -> benachbart, ueberlappungsfrei, beide mit /I-Indoor-Freigabe und 18 dBm.
# ap2 war bis 2026-08-17 auf ch149 (5745, ETSI-Band C) und dort auf 9 dBm gedrosselt. # ap2 war bis 2026-08-17 auf ch149 (5745, ETSI-Band C) und dort auf 9 dBm gedrosselt.
# 2,4 GHz: ch1/ch11 mit 20 MHz -> ueberlappungsfrei, ch6 bleibt fuer die Nachbarschaft frei. # ap3 (cAP ax, seit 2026-08-30) uebernimmt in Technik UG ch36; ap1 steht bis zu seinem
# Umzug daneben und ist deshalb FUNKSTILL (keine Provisioning-Regel am Manager) --
# seine Kanaele bleiben hier stehen, weil sie beim Umzug wieder gebraucht werden.
# 2,4 GHz: mit ap3 auf ch6 ist 1/6/11 erstmals vollstaendig ueberlappungsfrei.
"ap1": {"5g": 36, "2g": 1}, "ap1": {"5g": 36, "2g": 1},
"ap2": {"5g": 44, "2g": 11}, "ap2": {"5g": 44, "2g": 11},
"ap3": {"5g": 36, "2g": 6},
}
# --- Per-Device: Abweichungen vom ap-common-Default -----------------------------------
# Nur eintragen, was wirklich abweicht. Nautobot merged Dicts TIEF, Listen ERSETZEND --
# eine leere Liste hier loescht den Default, genau das ist beim cAP ax gewollt.
DEVICE_OVERRIDES = {
# cAP ax hat nur ether1 (PoE-in) + ether2 (PoE-out); ether2 ist bewusst kein
# Bridge-Port. Es bleibt also KEIN zusaetzlicher Bridge-Port uebrig.
"ap3": {"extra_ports": []},
} }
CTX_NAME = "ap-common" CTX_NAME = "ap-common"
@@ -201,14 +214,15 @@ def main():
if not d: if not d:
print(f" !! Device {dev} nicht gefunden") print(f" !! Device {dev} nicht gefunden")
continue continue
want = {"ap": {"channels": ch}} ov = "".join(f" {k}={v}" for k, v in DEVICE_OVERRIDES.get(dev, {}).items())
want = {"ap": {"channels": ch, **DEVICE_OVERRIDES.get(dev, {})}}
if d.get("local_config_context_data") == want: if d.get("local_config_context_data") == want:
print(f" = {dev} 5g=ch{ch['5g']} 2g=ch{ch['2g']} (unveraendert)") print(f" = {dev} 5g=ch{ch['5g']} 2g=ch{ch['2g']}{ov} (unveraendert)")
elif DRY: elif DRY:
print(f" ~ {dev} -> 5g=ch{ch['5g']} 2g=ch{ch['2g']} (dry-run)") print(f" ~ {dev} -> 5g=ch{ch['5g']} 2g=ch{ch['2g']}{ov} (dry-run)")
else: else:
req("PATCH", f"/dcim/devices/{d['id']}/", {"local_config_context_data": want}) req("PATCH", f"/dcim/devices/{d['id']}/", {"local_config_context_data": want})
print(f" ~ {dev} -> 5g=ch{ch['5g']} 2g=ch{ch['2g']}") print(f" ~ {dev} -> 5g=ch{ch['5g']} 2g=ch{ch['2g']}{ov}")
if DRY: if DRY:
print("\nDRY-RUN - nichts geschrieben. Mit --apply ausfuehren.") print("\nDRY-RUN - nichts geschrieben. Mit --apply ausfuehren.")

View File

@@ -0,0 +1,226 @@
#!/usr/bin/env python3
r"""BF-Netzwerk -> Nautobot: ap3 (cAP ax) als Device anlegen. TP28-Nachtrag, 2026-08-30.
WOZU
----
ap3 ist der dritte Access Point und laeuft seit dem 2026-08-30 als reiner CAP unter
`crs326-core`. Damit Golden Config ihn rendern und pruefen kann, braucht er ein
DCIM-Modell: DeviceType, Device, Interfaces, Mgmt-IP.
MUSTER = ap1 (per GraphQL gelesen, nicht geraten -- `tagged_vlans` liest sich per REST
als null, das war schon in `ingest-ap-rebuild.py` eine Falle).
DELTAS ZU ap1/ap2 (hAP ax2), und beide sind der Grund fuer die ganze Uebung:
- cAP ax hat nur ZWEI Ethernet-Ports (ether1 PoE-in, ether2 PoE-out). Kein ether3/4/5.
Deshalb `ap.extra_ports = []` in local_config_context_data; das GC-Template rendert
die Bridge-Ports seit 2026-08-30 aus diesem Schluessel statt hartkodiert.
- ether2 ist bewusst KEIN Bridge-Port (Zielzustand wie ap2).
NEBENKORREKTUR: ap1 stand in Nautobot auf "BF -> Wohnzimmer Sideboard", steht physisch
aber in Technik UG (User-Bestaetigung 2026-08-30). Der Sideboard-Eintrag war der ZIEL-
Standort nach dem geplanten Umzug, nicht der Ist-Zustand. Wird hier mitkorrigiert; beim
tatsaechlichen Umzug wandert er zurueck auf Sideboard.
export NAUTOBOT_TOKEN=...
python3 nautobot/ingest-ap3-device.py # Dry-Run
python3 nautobot/ingest-ap3-device.py --apply # schreiben
"""
import json, os, sys, urllib.error, urllib.parse, urllib.request
BASE = "http://10.40.20.30:8080/api"
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
DRY = "--apply" not in sys.argv
DEV = "ap3"
MODEL = "cAP ax" # board-name, wie "hAP ax2" bei ap1/ap2
MANUFACTURER = "MikroTik"
LOCATION = "BF → Technik UG"
ROLE = "Wireless AP"
PLATFORM = "RouterOS"
MGMT_IP = "10.40.99.13/24"
TRUNK_VLANS = [10, 20, 30, 40, 50, 90, 99]
# Interfaces in Anlegereihenfolge: bridge zuerst (die anderen referenzieren sie).
# radio-mac steht in der description, weil die CAPsMAN-Provisionierung am Core genau
# daran haengt -- wer das Geraet tauscht, findet hier, was er am Manager nachziehen muss.
IFACES = [
("bridge", "bridge", None, None, [], None, None, "vlan-filtering=yes; VLAN1 untagged, 10-99 tagged"),
("ether1", "1000base-t", "tagged", 1, TRUNK_VLANS, None, None, "Uplink-Trunk (PoE-in) -> crs326-core ether21"),
("ether2", "1000base-t", None, None, [], None, None, "PoE-out, frei. KEIN Bridge-Port - cAP ax hat nur 2 Ports"),
("lo", "virtual", None, None, [], None, None, "loopback"),
("vlan99", "virtual", "tagged", None, [99], None, "bridge", "Mgmt SVI (VLAN99)"),
("wifi1", "ieee802.11ax", "access", 10, [], "bridge", None, "5GHz master - dirtyvader-main (radio-mac D0:EA:11:FF:16:9F)"),
("wifi2", "ieee802.11ax", "access", 10, [], "bridge", None, "2.4GHz master - dirtyvader-main (radio-mac D0:EA:11:FF:16:A0)"),
("w5-infra", "ieee802.11ax", "access", 20, [], "bridge", None, "5G slave - dirtyvader-infra"),
("w5-media", "ieee802.11ax", "access", 40, [], "bridge", None, "5G slave - dirtyvader-media"),
("w5-untrusted", "ieee802.11ax", "access", 50, [], "bridge", None, "5G slave - dirtyvader-untrusted"),
("w5-guest", "ieee802.11ax", "access", 90, [], "bridge", None, "5G slave - dirtyvader-guest"),
("w2-infra", "ieee802.11ax", "access", 20, [], "bridge", None, "2.4G slave - dirtyvader-infra"),
("w2-iot", "ieee802.11ax", "access", 30, [], "bridge", None, "2.4G slave - dirtyvader-iot (nur 2.4GHz)"),
("w2-media", "ieee802.11ax", "access", 40, [], "bridge", None, "2.4G slave - dirtyvader-media"),
("w2-untrusted", "ieee802.11ax", "access", 50, [], "bridge", None, "2.4G slave - dirtyvader-untrusted"),
("w2-guest", "ieee802.11ax", "access", 90, [], "bridge", None, "2.4G slave - dirtyvader-guest"),
]
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 -> {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 main():
if not TOKEN:
sys.exit("!! NAUTOBOT_TOKEN nicht gesetzt")
mode = "DRY-RUN" if DRY else "APPLY"
print(f"== ap3 -> Nautobot ({mode}) ==\n")
status_active = find("/extras/statuses", name="Active")
role = find("/extras/roles", name=ROLE)
plat = find("/dcim/platforms", name=PLATFORM)
loc = find("/dcim/locations", name="Technik UG")
manu = find("/dcim/manufacturers", name=MANUFACTURER)
# 🔴 OHNE SecretsGroup kein Backup. Das Geraet landet trotzdem in der GC-DynamicGroup,
# die intended config wird sauber gerendert -- nur der Backup-Job kann sich nicht
# anmelden, und ohne Backup entstehen KEINE Compliance-Eintraege. Die Gesamtzahl
# bleibt dann unauffaellig (104/104), weil ein Geraet ohne Ergebnisse auch keine
# Drift meldet. Aufgeschlagen am 2026-08-30 bei ap3.
secrets = find("/extras/secrets-groups", name="RouterOS Nornir")
for label, obj in [("Status Active", status_active), (f"Role {ROLE}", role),
(f"Platform {PLATFORM}", plat), ("Location Technik UG", loc),
(f"Manufacturer {MANUFACTURER}", manu),
("SecretsGroup 'RouterOS Nornir'", secrets)]:
if not obj:
sys.exit(f"!! {label} nicht gefunden")
vlans = {v["vid"]: v["id"] for v in req("GET", "/ipam/vlans/?limit=50")["results"]}
# --- DeviceType ---
dt = find("/dcim/device-types", model=MODEL)
if dt:
print(f" = DeviceType {MODEL!r}")
elif DRY:
print(f" + DeviceType {MODEL!r} (dry-run)")
else:
dt = req("POST", "/dcim/device-types/", {"manufacturer": manu["id"], "model": MODEL, "u_height": 0})
print(f" + DeviceType {MODEL!r}")
# --- Device ---
dev = find("/dcim/devices", name=DEV)
if dev:
print(f" = Device {DEV}")
elif DRY:
print(f" + Device {DEV} (dry-run)")
else:
dev = req("POST", "/dcim/devices/", {
"name": DEV, "device_type": dt["id"], "role": role["id"], "platform": plat["id"],
"location": loc["id"], "status": status_active["id"], "secrets_group": secrets["id"],
"comments": "cAP ax (cAPGi-5HaxD2HaxD), reiner CAP unter crs326-core. Bring-up 2026-08-30 (TP28-Nachtrag).",
})
print(f" + Device {DEV}")
if DRY and not dev:
print("\n (Interfaces/IP koennen im Dry-Run nicht geplant werden, solange das Device fehlt)")
print("\nDRY-RUN - nichts geschrieben. Mit --apply ausfuehren.")
return
# SecretsGroup auch nachziehen, wenn das Device schon existierte
if dev and not DRY:
cur = req("GET", f"/dcim/devices/{dev['id']}/")
if (cur.get("secrets_group") or {}).get("id") != secrets["id"]:
req("PATCH", f"/dcim/devices/{dev['id']}/", {"secrets_group": secrets["id"]})
print(" ~ SecretsGroup 'RouterOS Nornir' gesetzt")
# --- Interfaces ---
print(f"\n Interfaces ({len(IFACES)}):")
have = {i["name"]: i for i in req("GET", f"/dcim/interfaces/?device={DEV}&limit=100")["results"]}
ids = {n: o["id"] for n, o in have.items()}
for name, typ, mode_, untag, tagged, bridge, parent, descr in IFACES:
payload = {
"device": dev["id"], "name": name, "type": typ, "status": status_active["id"],
"enabled": True, "description": descr,
}
if mode_:
payload["mode"] = mode_
if untag is not None:
payload["untagged_vlan"] = vlans[untag]
if tagged:
payload["tagged_vlans"] = [vlans[v] for v in tagged]
if bridge:
payload["bridge"] = ids.get(bridge)
if parent:
payload["parent_interface"] = ids.get(parent)
if name in have:
print(f" = {name}")
continue
if DRY:
print(f" + {name:<13} type={typ} mode={mode_} untag={untag} tagged={tagged} (dry-run)")
continue
new = req("POST", "/dcim/interfaces/", payload)
ids[name] = new["id"]
print(f" + {name:<13} type={typ} mode={mode_} untag={untag} tagged={tagged}")
# --- Mgmt-IP + primary_ip4 ---
print("\n Mgmt-IP:")
ip = find("/ipam/ip-addresses", address=MGMT_IP.split("/")[0])
if not ip:
if DRY:
print(f" + {MGMT_IP} (dry-run)")
else:
ns = find("/ipam/namespaces", name="Global")
ip = req("POST", "/ipam/ip-addresses/", {
"address": MGMT_IP, "status": status_active["id"], "type": "host",
"namespace": ns["id"], "description": "ap3 Mgmt (VLAN99)"})
print(f" + {MGMT_IP}")
else:
print(f" = {MGMT_IP}")
if not DRY and ip and ids.get("vlan99"):
assigned = req("GET", f"/ipam/ip-address-to-interface/?ip_address={ip['id']}&interface={ids['vlan99']}")
if assigned.get("count"):
print(" = Zuordnung vlan99")
else:
req("POST", "/ipam/ip-address-to-interface/", {"ip_address": ip["id"], "interface": ids["vlan99"]})
print(" + Zuordnung vlan99")
cur = req("GET", f"/dcim/devices/{dev['id']}/")
if (cur.get("primary_ip4") or {}).get("id") != ip["id"]:
req("PATCH", f"/dcim/devices/{dev['id']}/", {"primary_ip4": ip["id"]})
print(" ~ primary_ip4 gesetzt")
else:
print(" = primary_ip4")
# --- Nebenkorrektur ap1-Standort ---
print("\n Nebenkorrektur ap1-Standort:")
ap1 = find("/dcim/devices", name="ap1")
if ap1:
cur = req("GET", f"/dcim/devices/{ap1['id']}/?depth=1")
curloc = (cur.get("location") or {}).get("name")
if curloc == "Technik UG":
print(" = ap1 steht bereits auf Technik UG")
elif DRY:
print(f" ~ ap1: {curloc!r} -> 'Technik UG' (dry-run)")
else:
req("PATCH", f"/dcim/devices/{ap1['id']}/", {"location": loc["id"]})
print(f" ~ ap1: {curloc!r} -> 'Technik UG'")
if DRY:
print("\nDRY-RUN - nichts geschrieben. Mit --apply ausfuehren.")
if __name__ == "__main__":
main()

View File

@@ -184,6 +184,12 @@
/interface wifi channel add band=5ghz-ax frequency=5220 name=ch-5g-ap2 width=20/40mhz /interface wifi channel add band=5ghz-ax frequency=5220 name=ch-5g-ap2 width=20/40mhz
/interface wifi channel add band=2ghz-ax frequency=2412 name=ch-2g-ap1 width=20mhz /interface wifi channel add band=2ghz-ax frequency=2412 name=ch-2g-ap1 width=20mhz
/interface wifi channel add band=2ghz-ax frequency=2462 name=ch-2g-ap2 width=20mhz /interface wifi channel add band=2ghz-ax frequency=2462 name=ch-2g-ap2 width=20mhz
# ap3 (cAP ax, Technik UG) seit 2026-08-30. Kanaele aus dem TP25-Plan vom 2026-08-24:
# Band A (5150-5250) ist das einzige non-DFS-Band mit vollen 23 dBm; ap3 bekommt es, weil
# seine Flaeche von keinem anderen AP mitgetragen wird. 2437 = ch6 -> mit ap1 (ch1) und
# ap2 (ch11) ist 2,4 GHz erstmals vollstaendig ueberlappungsfrei.
/interface wifi channel add band=5ghz-ax frequency=5180 name=ch-5g-ap3 width=20/40mhz
/interface wifi channel add band=2ghz-ax frequency=2437 name=ch-2g-ap3 width=20mhz
/interface wifi datapath add bridge=bridge name=dp-main vlan-id=10 /interface wifi datapath add bridge=bridge name=dp-main vlan-id=10
/interface wifi datapath add bridge=bridge name=dp-infra vlan-id=20 /interface wifi datapath add bridge=bridge name=dp-infra vlan-id=20
/interface wifi datapath add bridge=bridge name=dp-iot vlan-id=30 /interface wifi datapath add bridge=bridge name=dp-iot vlan-id=30
@@ -208,12 +214,22 @@
/interface wifi configuration add channel=ch-5g-ap2 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap2 security=sec-main ssid=dirtyvader-main steering=st-main /interface wifi configuration add channel=ch-5g-ap2 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap2 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-2g-ap1 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap1 security=sec-main ssid=dirtyvader-main steering=st-main /interface wifi configuration add channel=ch-2g-ap1 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap1 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-2g-ap2 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap2 security=sec-main ssid=dirtyvader-main steering=st-main /interface wifi configuration add channel=ch-2g-ap2 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap2 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-5g-ap3 country=Austria datapath=dp-main mode=ap name=cfg-main-5g-ap3 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add channel=ch-2g-ap3 country=Austria datapath=dp-main mode=ap name=cfg-main-2g-ap3 security=sec-main ssid=dirtyvader-main steering=st-main
/interface wifi configuration add country=Austria datapath=dp-guest mode=ap name=cfg-guest security=sec-guest ssid=dirtyvader-guest steering=st-guest /interface wifi configuration add country=Austria datapath=dp-guest mode=ap name=cfg-guest security=sec-guest ssid=dirtyvader-guest steering=st-guest
/interface wifi capsman set ca-certificate=auto certificate=auto enabled=yes interfaces=vlan99 upgrade-policy=none /interface wifi capsman set ca-certificate=auto certificate=auto enabled=yes interfaces=vlan99 upgrade-policy=none
/interface wifi provisioning add action=create-dynamic-enabled comment="ap1 5GHz" master-configuration=cfg-main-5g-ap1 radio-mac=D0:EA:11:4D:40:E5 slave-configurations=cfg-media,cfg-untrusted,cfg-guest # 🔴 ap1 hat BEWUSST keine Provisioning-Zeile (2026-08-30). ap3 hat in Technik UG seinen
/interface wifi provisioning add action=create-dynamic-enabled comment="ap1 2.4GHz" master-configuration=cfg-main-2g-ap1 radio-mac=D0:EA:11:4D:40:E6 slave-configurations=cfg-iot,cfg-media,cfg-untrusted,cfg-guest # Platz uebernommen und funkt dort auf 5180; ap1 steht bis zu seinem Umzug ans Sideboard
# direkt daneben und wuerde auf demselben Kanal kollidieren. Ohne passende Regel legt
# CAPsMAN fuer ein Radio gar nichts an -> es bleibt still, das Geraet aber erreichbar.
# Die Objekte `ch-5g-ap1`/`ch-2g-ap1` und `cfg-main-*-ap1` bleiben absichtlich stehen:
# beim Umzug bekommt ap1 laut TP25 `5260/Ce` (Band B, DFS) -- dann werden nur diese
# zwei Zeilen wieder eingesetzt und das Channel-Objekt auf eine Frequenz-LISTE gestellt
# (mit nur einem Wert waere eine Radar-Erkennung ein SPOF).
/interface wifi provisioning add action=create-dynamic-enabled comment="ap2 5GHz" master-configuration=cfg-main-5g-ap2 radio-mac=D0:EA:11:4D:40:3E slave-configurations=cfg-media,cfg-untrusted,cfg-guest /interface wifi provisioning add action=create-dynamic-enabled comment="ap2 5GHz" master-configuration=cfg-main-5g-ap2 radio-mac=D0:EA:11:4D:40:3E slave-configurations=cfg-media,cfg-untrusted,cfg-guest
/interface wifi provisioning add action=create-dynamic-enabled comment="ap2 2.4GHz" master-configuration=cfg-main-2g-ap2 radio-mac=D0:EA:11:4D:40:3F slave-configurations=cfg-iot,cfg-media,cfg-untrusted,cfg-guest /interface wifi provisioning add action=create-dynamic-enabled comment="ap2 2.4GHz" master-configuration=cfg-main-2g-ap2 radio-mac=D0:EA:11:4D:40:3F slave-configurations=cfg-iot,cfg-media,cfg-untrusted,cfg-guest
/interface wifi provisioning add action=create-dynamic-enabled comment="ap3 5GHz" master-configuration=cfg-main-5g-ap3 radio-mac=D0:EA:11:FF:16:9F slave-configurations=cfg-media,cfg-untrusted,cfg-guest
/interface wifi provisioning add action=create-dynamic-enabled comment="ap3 2.4GHz" master-configuration=cfg-main-2g-ap3 radio-mac=D0:EA:11:FF:16:A0 slave-configurations=cfg-iot,cfg-media,cfg-untrusted,cfg-guest
# [6c] WLAN ACCESS-LIST — der Soll-Zustand ist LEER, und das ist eine Aussage. # [6c] WLAN ACCESS-LIST — der Soll-Zustand ist LEER, und das ist eine Aussage.
# #

View File

@@ -64,6 +64,15 @@
# Das Objekt traegt bewusst KEINE vlan-id: die liefert der Manager pro VAP. # Das Objekt traegt bewusst KEINE vlan-id: die liefert der Manager pro VAP.
/interface wifi datapath add name=dp-caps-slaves bridge=bridge comment="TP28-fix: bridge fuer CAPsMAN-Slave-VAPs" /interface wifi datapath add name=dp-caps-slaves bridge=bridge comment="TP28-fix: bridge fuer CAPsMAN-Slave-VAPs"
/interface wifi cap set enabled=yes discovery-interfaces=vlan99 slaves-datapath=dp-caps-slaves /interface wifi cap set enabled=yes discovery-interfaces=vlan99 slaves-datapath=dp-caps-slaves
# 🔴 `cap enabled=yes` REICHT NICHT. Jedes Master-Radio muss zusaetzlich selbst auf
# `configuration.manager=capsman` stehen, sonst BIETET der CAP seine Radios dem
# Manager gar nicht an. Symptom (2026-08-30 live aufgeschlagen): `remote-cap print`
# zeigt `state=Ok`, aber `/interface wifi radio print` am Manager listet fuer dieses
# Geraet NICHTS -- und weil die Provisioning-Regel dann nicht matcht, sucht man den
# Fehler in der radio-mac statt an dieser Stelle. ap1/ap2 tragen es seit dem
# TP28-Cutover, im Werkszustand ist es leer (= local).
/interface wifi set [find name=wifi1] configuration.manager=capsman
/interface wifi set [find name=wifi2] configuration.manager=capsman
# --- (6) ZULETZT scharf schalten. Vorher waere der Rest der Datei nicht angekommen. --- # --- (6) ZULETZT scharf schalten. Vorher waere der Rest der Datei nicht angekommen. ---
/interface bridge set [find name=bridge] vlan-filtering=yes /interface bridge set [find name=bridge] vlan-filtering=yes

View File

@@ -0,0 +1,36 @@
# =====================================================================================
# ap3 — dedizierte Service-Accounts nachziehen. TP28-Nachtrag, 2026-08-30.
#
# BEFUND: nach dem Bring-up rendert Golden Config die intended config fuer ap3 sauber,
# aber der BACKUP-Job scheitert -- Nautobot kommt per SSH nicht auf das Geraet. Ursache
# ist kein Netz- und kein Firewall-Problem: ap1/ap2/core/branch tragen seit TP-PWD
# (2026-08-23) dedizierte Service-Accounts `nautobot` und `ha`, ein Werksgeraet hat nur
# `admin`. Ohne den User gibt es kein Backup, ohne Backup keine Compliance-Eintraege --
# und die Gesamtzahl bleibt unauffaellig bei 104/104, weil ein Geraet ohne Ergebnisse
# eben auch keine Drift meldet. 🔴 Merkregel: eine gleichbleibende Compliance-Zahl nach
# dem Hinzufuegen eines Geraets ist ein BEFUND, kein Erfolg.
#
# Policies 1:1 von ap1 uebernommen (live gelesen, nicht abgeschrieben):
# grp-ha = read,test,api,rest-api + alles andere explizit aus
# grp-nautobot = ssh,read,write,test,sensitive + alles andere explizit aus
# (write, weil Golden Config auch deployen koennen muss)
#
# `__HA_PW__` / `__NB_PW__` sind PLATZHALTER. Die echte Datei wird zur Laufzeit aus
# ~/.secrets/credentials.env (MIKROTIK_HA_PASS / MIKROTIK_NAUTOBOT_PASS) ins Scratchpad
# generiert und danach geloescht -- die Werte stehen nie in diesem Repo.
# =====================================================================================
:if ([:len [/user group find where name="grp-ha"]]=0) do={
/user group add name=grp-ha policy=read,test,api,rest-api,!local,!telnet,!ssh,!ftp,!reboot,!write,!policy,!winbox,!password,!web,!sniff,!sensitive,!romon
}
:if ([:len [/user group find where name="grp-nautobot"]]=0) do={
/user group add name=grp-nautobot policy=ssh,read,write,test,sensitive,!local,!telnet,!ftp,!reboot,!policy,!winbox,!password,!web,!sniff,!api,!romon,!rest-api
}
:if ([:len [/user find where name="ha"]]=0) do={
/user add name=ha group=grp-ha password="__HA_PW__" comment="TP-PWD 2026-08-23: dedizierter Service-Account"
} else={ /user set [find name="ha"] group=grp-ha password="__HA_PW__" }
:if ([:len [/user find where name="nautobot"]]=0) do={
/user add name=nautobot group=grp-nautobot password="__NB_PW__" comment="TP-PWD 2026-08-23: dedizierter Service-Account"
} else={ /user set [find name="nautobot"] group=grp-nautobot password="__NB_PW__" }
:log info ("tp28-ap3: service-accounts, users=" . [:tostr [:len [/user find]]] . " groups=" . [:tostr [:len [/user group find]]])

View File

@@ -0,0 +1,39 @@
# =====================================================================================
# ap3 — die drei Werksreste abraeumen, die die Compliance nach dem Bring-up gemeldet hat.
# TP28-Nachtrag, 2026-08-30.
#
# Befund aus dem ersten Compliance-Lauf MIT ap3 (127/130):
# ip-dns extra -> defconf-Eintrag router.lan -> 192.168.88.1 (die alte
# Werks-LAN-IP, die es seit Phase 2 gar nicht mehr gibt)
# ip-firewall-filter extra -> 11 defconf-Regeln. Ein reiner AP routet nicht, hat also
# keine forward-Kette zu schuetzen; der Mgmt-Zugang haengt
# an /ip service, nicht an der input-Kette (TP26).
# ip-service missing -> die Haertung war nie gesetzt
#
# 🔴 `[find !dynamic]` waere ein SYNTAXFEHLER -- korrekt ist `[find where !dynamic]`.
# Und Syntaxfehler schlagen beim PARSEN zu, `on-error` faengt sie NICHT. Symptom:
# kein Fehler im Log, keine Wirkung, Plan meldet trotzdem Erfolg. (TP26, 2026-08-17.)
# 🔴 Das Filtern auf `!dynamic` ist zusaetzlich noetig, weil `remove [find]` auch die
# dynamische "special dummy rule to show fasttrack counters" erfasst -- RouterOS
# verweigert deren Entfernung, das GESAMTE Kommando wirft, und es wird NICHTS entfernt.
#
# ⚠️ Kein Lockout-Risiko: `available-from=10.40.0.0/16` deckt alle BF-VLANs ab, also
# auch den Mgmt-Weg ueber vlan99 und die Nautobot-Quelle 10.40.20.30.
# =====================================================================================
# [1] /ip service haerten (Soll aus dem GC-Template, platform-weit)
/ip service set ftp disabled=yes
/ip service set ssh available-from=10.40.0.0/16,100.96.0.0/12
/ip service set telnet disabled=yes
/ip service set www available-from=10.40.0.0/16,100.96.0.0/12
/ip service set winbox available-from=10.40.0.0/16,100.96.0.0/12
/ip service set api available-from=10.40.0.0/16,100.96.0.0/12
/ip service set api-ssl disabled=yes
# [2] defconf-Firewall abraeumen (TP26-Muster, identisch zu ap1/ap2)
:do { /ip firewall filter remove [find where !dynamic] } on-error={ :log info "tp28-ap3: firewall-remove warf" }
# [3] defconf-DNS-Eintrag auf die tote Werks-IP
:do { /ip dns static remove [find where address="192.168.88.1"] } on-error={ :log info "tp28-ap3: dns-remove warf" }
:log info ("tp28-ap3-werksreste: fw-statisch=" . [:tostr [:len [/ip firewall filter find where !dynamic]]] . " dns-static=" . [:tostr [:len [/ip dns static find]]])