"Plan-Status: Completed" sagt nur, dass die Zeilen abgesetzt wurden. Am 2026-08-17 blieb ein `/interface/wifi/set ... !channel` wirkungslos ohne zu werfen, und das Selbstprotokoll meldete trotzdem Erfolg — weil es das Profil las, waehrend der Interface-Inline-Override gewann. Die Abnahme haengt seither daran, dass jemand daran denkt nachzusehen. Das ist kein Mechanismus. --assert prueft nach dem Deploy per RouterOS-REST am Geraet und bricht laut ab: --assert '/ip/firewall/filter|count=0' --assert '/interface/wifi?name=wifi1|channel.width=20/40mhz' --assert 'cmd:/interface/wifi/monitor?numbers=wifi1&once=|channel=5180/ax/Ce/I' Format [cmd:]<pfad>[?query]|<feld-oder-count>=<erwartet>, mehrfach angebbar. GET fuers Lesen, cmd: fuer POST-Kommandos wie monitor. Trenner ist |, nicht :, weil MAC-Adressen in Werten Doppelpunkte haben. Dokumentierte Regel dazu: bei Funkaenderungen muss die Assertion monitor lesen, NIE das Profil — print detail zeigt aufgeloeste Werte, export die rohen, nur monitor das, was auf der Luft ist. Genau diese Verwechslung war der Fehler. Mgmt-IP kommt aus Nautobot (primary_ip4) statt hartkodiert. Passwoerter aus $ENV, sonst aus ~/.secrets/credentials.env — mit eigenem KEY=VALUE-Parser, weil die Datei WireGuard-[Interface]-Bloecke enthaelt, an denen `set -a; . datei` in zsh still abbricht und alle Zeilen danach verschluckt. Fehlerausgaben redacten das Passwort. Getestet: count-GET, field-GET mit Query, monitor-POST (inkl. tx-power=18) und der Fehlerfall, der korrekt abbricht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nautobot-Tooling
ingest-baseline.py
Idempotenter Basisdaten-Ingest (Locations, Rollen, Device-Types, Devices, VLANs/Prefixes, ap1-Interfaces + IPs, WLAN-Modelle). Bildet den Ist-Zustand ab, nicht den Zielzustand.
export NAUTOBOT_TOKEN=... # aus docs/nas-vlan20/stacks/210-nautobot.yml (gitignored)
python3 nautobot/ingest-baseline.py # Dry-Run
python3 nautobot/ingest-baseline.py --apply # schreiben
API-Fallen (teuer gelernt)
tagged_vlansliest sich per REST alsnull, obwohl korrekt gespeichert. Verifikation nur über GraphQL:POST /api/graphql/mit{interfaces(device:"ap1"){name mode untagged_vlan{vid} tagged_vlans{vid}}}.- Relationen kommen ohne
?depth=1ohnedisplay→ ein voll gepflegtes Device sieht leer aus. LocationType.content_typesmussipam.vlangroup/ipam.vlan/ipam.prefix/dcim.controllerenthalten, sonst 400 beim Anlegen.parent_interfacenur für virtuelle Interfaces — WLAN-VAPs können ihren Master nicht als Parent tragen.WirelessNetworkhat kein VLAN-Feld; die VLAN-Bindung hängt anwireless/controller-managed-device-group-wireless-network-assignmentsund braucht zwingend einenController(bei uns logisch,controller_deviceleer — kein CAPsMAN).RadioProfile.channel_widthwill Integer ([20,40,80]), nicht"20mhz".- IP↔Interface läuft über
/ipam/ip-address-to-interface/.