Oktett-Klasse .150-.199 (App-Services). Nautobot gibt damit .30/.31 an AGH frei. .157 ist gitea, deshalb nimmt redis .158. - Datenmigration ueber neues nautobot/tp45-ip-move.py: Nautobot verbietet das Aendern einer IP nach dem Anlegen -> anlegen/primary_ip4 umhaengen/ Altobjekt loeschen, in dieser Reihenfolge - Stack-Redeploy (PUT /api/stacks/203), Caddy-Backend, Core-Literal *24 ueber GC deployt (Plan 51a90e25) mit Wirksamkeitspruefung am Geraet - caddy/deploy.sh: push_verified() ersetzt den push||push-Fallback. `pct push` meldet Exit 0 AUCH wenn es die Quelle nicht lesen kann, der Fallback griff deshalb nie und `caddy validate` pruefte die ALTE Datei -> gruene Abnahme bei unveraendertem Zustand (Symptom: 502 bei gesundem Backend) - 39 Referenzen in 28 Dateien mitgezogen; lebende Doku nachgezogen (CLAUDE.md, ip-plan.md, cf-tunnel-mapping §0-Kopf, TP16.3/TP31-Prosa) - GC-Compliance 140/140, 0 Drifts Neue Funde (TP45-Prosa + CLAUDE.md): - Die Core-Adressliste `nautobot` wird von GC nirgends gerendert -> ein Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt. Gleiche Klasse wie die fehlenden DHCP-Reservierungen. - Regel *24 (Literal) und *28 (Liste) sind Dubletten; der Literal steht bei 0 Paketen. Zusammenlegung bleibt offen. - gc-manual-plan.py: /api/extras/job-log-entries/ liefert 404 (Nautobot 3.x) -> bei gescheitertem Deploy fehlt die Fehlermeldung. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
5.3 KiB
Python
132 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
ComplianceRule `system-logging` anlegen (idempotent). TP25 Fund E, 2026-08-30.
|
|
|
|
Die Luecke, die das schliesst
|
|
-----------------------------
|
|
`wireless`-Logs liefen bis 2026-08-30 NUR nach `action=memory`. Nur `firewall` ging in
|
|
den Kollektor (`tp5syslog` -> 10.40.20.18:514). Ein wiederkehrendes Client-Problem
|
|
hinterliess damit KEINE dauerhafte Spur: der Ringpuffer fasste an dem Tag 75 Zeilen und
|
|
war von einer Stunde ap3-Arbeit fast ueberschrieben. Christophs seit Wochen gemeldeter
|
|
iPhone-Abbruch war rueckwirkend deshalb nicht diagnostizierbar.
|
|
|
|
Am 2026-08-30 sind zwei Regeln dazugekommen (`topics=wireless` und `topics=caps`, beide
|
|
-> tp5syslog). Ohne ComplianceRule waeren sie ungeschuetzt: sie koennten still
|
|
verschwinden, und niemandem fiele es auf -- bei einer Beweissicherung, die wochenlang
|
|
laufen soll, ist das der falsche Zustand. Dieselbe blinde Stelle wie bei `/queue`
|
|
(TP6) und `/ip firewall filter` (TP26).
|
|
|
|
⚠️ RouterOS UND-verknuepft Topics innerhalb EINER Regel (`topics=info,!firewall` heisst
|
|
"info UND NICHT firewall"). `topics=wireless,caps` haette deshalb NICHTS gematcht --
|
|
es braucht zwei Regeln. Und der Topic heisst `wireless`, NICHT `wifi`, auch auf dem
|
|
neuen wifi-qcom-Stack (am Geraet nachgelesen, nicht geraten).
|
|
|
|
Warum ein Praefix reicht
|
|
------------------------
|
|
`/system logging action add ...` beginnt ebenfalls mit `/system logging`, ein einziges
|
|
Praefix fasst also Regeln UND Actions. Und weil RouterOS Defaults nicht exportiert,
|
|
ist der Block auf ap1/ap2/ap3/branch LEER -- Soll und Ist sind dort beide leer, also
|
|
compliant. Nur der Core traegt Zeilen.
|
|
|
|
python3 nautobot/ingest-logging-rule.py # Dry-Run
|
|
python3 nautobot/ingest-logging-rule.py --apply
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
BASE = "http://10.40.20.154:8080/api"
|
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
|
APPLY = "--apply" in sys.argv
|
|
|
|
FEATURE = "system-logging"
|
|
BESCHREIBUNG = ("TP25 Fund E: /system logging unter Drift-Kontrolle. Die wireless-/caps-Regeln "
|
|
"in den Kollektor sind Beweissicherung und duerfen nicht still verschwinden.")
|
|
MATCH = "/system logging"
|
|
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,
|
|
# Reihenfolge ist hier bedeutungslos: Queue-Trees haengen ueber `parent=`
|
|
# am Namen, nicht an der Position. Anders als /ip firewall filter.
|
|
"config_ordered": False,
|
|
"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()
|