diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index a3da90f..80d1730 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -52,7 +52,8 @@ ab jetzt bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)* 08-18) — TP19 ist geschlossen; `cudy1` hängt stromlos an core `ether11`, physischer Ausbau ist hands-on. - **Kontrolle / Governance:** MikroMCP für Reads/Debug (alle 4 Geräte in der Registry), **Config-Änderungen ausschließlich über Nautobot Golden Config** (TP21/TP22 — Deploy-Pipeline scharf, - **92/92 Compliance-Checks grün, 0 Drifts** bei **23 Rules**, Stand 2026-08-18). + **96/96 Compliance-Checks grün, 0 Drifts** bei **24 Rules × 4 Geräten**, Stand 2026-08-23 — + neu dazu: **`wifi-access-list`**, s. TP22-Nachtrag).
📜 Historische Fassung (Stand bis 2026-08-08, Topologie vor dem Core-Cutover — bewusst erhalten, nicht als Ist lesen) @@ -1214,6 +1215,26 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - **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).** +- ✅ **Nachtrag 2026-08-23 — Governance-Lücke geschlossen: ComplianceRule `wifi-access-list`.** + `/interface wifi access-list` stand unter **keiner** Regel: Einträge dort erzeugten keinen Drift + und ließen sich nicht als Transaktion führen (`gc-transaktion.py` verlangt eine ComplianceRule + als `--scope`). Seit `docs/tp25-pin-s22.sh` schreibt jeder Messrundgang dort Regeln, und **TP25 + Fund E** (`signal-range`-Boden) würde dauerhaft genau dort landen. + Angelegt per `nautobot/ingest-wifi-access-list-rule.py` (idempotent), Template um den + dokumentierten Abschnitt **[6c]** ergänzt (**nur Kommentare** — der Soll-Zustand ist *leer*, + und das ist die Aussage). + - 🔢 **Zählerkorrektur: 92 → 96, nicht 94.** Die Regeln gelten **plattformweit**, nicht + gerätespezifisch: 24 Rules × 4 Geräte. Die Annahme „nur 2 Geräte betroffen" war falsch. + - ✅ **Wirksamkeit bewiesen, nicht behauptet** (die TP22-Regel „Job-Status ist kein + Wirksamkeitsnachweis"): Test-Eintrag mit einer nicht existierenden MAC gesetzt → + **95/96 mit exakt dieser Zeile als `extra`** → Eintrag entfernt → **96/96, 0 Drifts**. + Zusätzlich geprüft: `gc-transaktion.py` akzeptiert `--scope wifi-access-list` jetzt. + - 🔎 **Nebenbefund beim Start:** ein `bridge-vlan`-Drift auf `crs326-core` stand noch in der + Anzeige (ether3 als Trunk), obwohl der Sideboard-Rollback am 08-22 gelandet war. Das Gerät + war live korrekt; die **Compliance vergleicht gegen das letzte Backup**, und das war älter + als der Rollback. Mit dem Refresh verschwunden. Merkregel bleibt: bei einem Drift zuerst + fragen, wie alt das Backup ist. + ### TP23 — DNS-Policy & Self-Service-Ausnahmen ▸ ⏳ **STUFE 1 DURCH, STUFE 2 VORBEREITET (2026-08-18)** - ✅ **Stufe 1 — DoH auf dem Core scharf** (`routeros/tp23-stufe1-doh-core.rsc`), schliesst zugleich **TP21.1 TODO 1**. `use-doh-server=https://px35ynlol3.cloudflare-gateway.com/dns-query`, diff --git a/nautobot/README.md b/nautobot/README.md index 61c261d..ba0ee1e 100644 --- a/nautobot/README.md +++ b/nautobot/README.md @@ -24,3 +24,20 @@ python3 nautobot/ingest-baseline.py --apply # schreiben einen `Controller` (bei uns logisch, `controller_device` leer — kein CAPsMAN). - `RadioProfile.channel_width` will Integer (`[20,40,80]`), nicht `"20mhz"`. - IP↔Interface läuft über `/ipam/ip-address-to-interface/`. + +## `ingest-wifi-access-list-rule.py` +Legt ComplianceFeature + ComplianceRule **`wifi-access-list`** an (Plattform RouterOS, +`match_config=/interface wifi access-list`). Idempotent, Dry-Run ist Standard. + +```bash +export NAUTOBOT_TOKEN=... +python3 nautobot/ingest-wifi-access-list-rule.py --apply +``` + +**Warum es die Rule braucht:** ohne sie erzeugen Eintraege in der access-list **keinen Drift** +und lassen sich nicht als Config-Transaktion fuehren -- `gc-transaktion.py` verlangt eine +ComplianceRule als `--scope`. `docs/tp25-pin-s22.sh` schreibt dort bei jedem Messrundgang, und +TP25 Fund E (`signal-range`-Boden) landet dauerhaft dort. + +**Der Soll-Zustand ist leer** und steht als Kommentarblock `[6c]` im Template. Waehrend eines +Messlaufs ist der Drift **erwartet** und gehoert als offene Transaktion gefuehrt. diff --git a/nautobot/ingest-wifi-access-list-rule.py b/nautobot/ingest-wifi-access-list-rule.py new file mode 100644 index 0000000..f17cdda --- /dev/null +++ b/nautobot/ingest-wifi-access-list-rule.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +ComplianceRule `wifi-access-list` anlegen (idempotent). + +Die Luecke, die das schliesst +----------------------------- +Es gibt ComplianceRules fuer wifi-configuration/-datapath/-security/-steering/ +-provisioning/-channel/-cap/-capsman -- aber **keine** fuer +`/interface wifi access-list`. Eintraege dort erzeugen deshalb KEINEN Drift und +lassen sich auch nicht als Config-Transaktion fuehren: `gc-transaktion.py` +verlangt eine ComplianceRule als `--scope`. + +Seit 2026-08-22 ist das nicht mehr theoretisch. `docs/tp25-pin-s22.sh` schreibt +bei jedem Messrundgang access-list-Regeln auf den Manager, und TP25 **Fund E** +(`signal-range`-Boden gegen klebende Clients) wuerde dauerhaft genau dort landen +-- in einem Abschnitt, den niemand prueft. + +Der Soll-Zustand ist heute **leer**: am Geraet steht keine access-list, im +Template steht keine. Beide Seiten leer = compliant. Genau das ist der Sinn -- +ab jetzt faellt jeder Eintrag dort auf, statt unsichtbar zu bleiben. + + export NAUTOBOT_TOKEN=... # aus docs/nas-vlan20/stacks/210-nautobot.yml + python3 nautobot/ingest-wifi-access-list-rule.py # Dry-Run + python3 nautobot/ingest-wifi-access-list-rule.py --apply +""" +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request + +BASE = "http://10.40.20.30:8080/api" +TOKEN = os.environ.get("NAUTOBOT_TOKEN", "") +APPLY = "--apply" in sys.argv + +FEATURE = "wifi-access-list" +BESCHREIBUNG = ("TP25 Fund E / tp25-pin-s22.sh: signal-range-Boden und Mess-Pins " + "landen hier. Soll ist leer, solange Fund E nicht entschieden ist.") +MATCH = "/interface wifi access-list" +PLATTFORM = "RouterOS" + + +def req(method, path, data=None): + url = path if path.startswith("http") else 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", "Token " + TOKEN) + r.add_header("Content-Type", "application/json") + r.add_header("Accept", "application/json") + try: + with urllib.request.urlopen(r, timeout=60) as resp: + raw = resp.read() + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + # 🔴 Fehlerkoerper ausgeben: Nautobot begruendet 400er praezise, und ohne den + # Text raet man an Feldnamen herum (TP22-Erfahrung). + sys.exit("!! %s %s -> %s\n %s" % (method, url, e.code, e.read().decode()[:600])) + + +def eins(pfad, **filter): + q = urllib.parse.urlencode(filter) + res = req("GET", "%s?%s" % (pfad, q)).get("results", []) + if len(res) > 1: + sys.exit("FEHLER: %s?%s liefert %d Treffer, erwartet 0 oder 1" % (pfad, q, len(res))) + return res[0] if res else None + + +def main(): + if not TOKEN: + sys.exit("NAUTOBOT_TOKEN fehlt.") + print("Modus:", "APPLY" if APPLY else "DRY-RUN (nichts wird geschrieben)") + + plat = eins("/dcim/platforms/", name=PLATTFORM) + if not plat: + sys.exit("Plattform %r nicht gefunden." % PLATTFORM) + print("Plattform : %s (%s)" % (plat["display"], plat["id"])) + + feat = eins("/plugins/golden-config/compliance-feature/", name=FEATURE) + if feat: + print("Feature : existiert bereits (%s)" % feat["id"]) + else: + print("Feature : anlegen -> %s" % FEATURE) + if APPLY: + feat = req("POST", "/plugins/golden-config/compliance-feature/", + {"name": FEATURE, "slug": FEATURE, "description": BESCHREIBUNG}) + print(" angelegt (%s)" % feat["id"]) + + if not feat: + print("\nDry-Run: ohne Feature kann die Rule nicht geprueft werden. Mit --apply erneut.") + return + + rule = eins("/plugins/golden-config/compliance-rule/", + feature=feat["id"], platform=plat["id"]) + if rule: + print("Rule : existiert bereits (%s), match_config=%r" + % (rule["id"], rule.get("match_config"))) + if rule.get("match_config") != MATCH: + print(" ⚠️ match_config weicht ab -- erwartet %r" % MATCH) + return + + nutzlast = { + "feature": feat["id"], + "platform": plat["id"], + "description": BESCHREIBUNG, + "config_ordered": False, # Reihenfolge der access-list-Eintraege ist hier egal + "config_type": "cli", + "match_config": MATCH, + "custom_compliance": False, + "config_remediation": False, + } + print("Rule : anlegen -> %s auf %s, match_config=%r" % (FEATURE, PLATTFORM, MATCH)) + if APPLY: + r = req("POST", "/plugins/golden-config/compliance-rule/", nutzlast) + print(" angelegt (%s)" % r["id"]) + else: + print(" Nutzlast: %s" % json.dumps(nutzlast)) + + +if __name__ == "__main__": + main() diff --git a/nautobot/mikrotik_routeros.j2 b/nautobot/mikrotik_routeros.j2 index 60bd2fa..c26a0df 100644 --- a/nautobot/mikrotik_routeros.j2 +++ b/nautobot/mikrotik_routeros.j2 @@ -212,6 +212,28 @@ /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 +# [6c] WLAN ACCESS-LIST — der Soll-Zustand ist LEER, und das ist eine Aussage. +# +# Bis 2026-08-22 stand dieser Abschnitt unter KEINER ComplianceRule. Aenderungen +# dort erzeugten keinen Drift und liessen sich nicht als Transaktion fuehren -- +# `gc-transaktion.py` verlangt eine ComplianceRule als --scope. Das war nicht +# theoretisch: `docs/tp25-pin-s22.sh` schreibt bei jedem Messrundgang +# access-list-Regeln (Kommentar TP25-PIN-S22) auf den Manager, um ein Telefon auf +# einen AP festzunageln. +# +# Seit der ComplianceRule `wifi-access-list` faellt jeder Eintrag hier auf. +# Waehrend eines Messlaufs ist der gemeldete Drift ERWARTET -- dann gehoert er als +# offene Transaktion gefuehrt: +# python3 nautobot/gc-transaktion.py begin --device crs326-core \ +# --scope wifi-access-list --change-id TEST-... --expires JJJJ-MM-TT +# +# 🔴 Hier landet TP25 **Fund E** (`signal-range`-Boden gegen klebende Clients), +# sobald er entschieden ist. Stand 2026-08-22 ist er es NICHT: der Sticky-Client +# vom 08-20 reproduzierte sich beim Rundgang am 08-22 nicht, und in den schwachen +# Raeumen gibt es keinen besseren AP, auf den ein Boden steuern koennte. Ein +# voreilig gesetzter Boden verschlechtert die Terrasse, statt zu helfen. +# Vor dem Eintragen also: Sticky-Fall erneut messen. Siehe teilprojekte.md TP25. + # Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich): # core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12 /ip dns static add address=10.40.99.1 comment="core = GW/Router" name=router.lan type=A