TP45 Schicht A: AGH-DNS-Freigaben .30/.31 im GC-Soll + am Core nachgezogen
- nautobot/tp45-agh-firewall.py (neu): pflegt die vier `TP5-E2: LAN->AGH dns*`- Regeln im `local_config_context_data.firewall_filter` (add/remove, Dry-Run default, Unified Diff). Bewusst NICHT `ingest-core-firewall.py` -- das ist ein RE-BASELINE-Werkzeug und wuerde die Aenderung mit sich selbst absegnen. - gc-configplan.py: Jobs `tp45-agh-fw` (add + move vor die Log-Regel; `caddy` dahinter, damit der AGH-Block zusammenbleibt) und `tp45-agh-dhcp`. Fix in `create_plans`: unter derselben `change_control_id` liegen nach einem zweiten Lauf zwei Plaene. Die Auswahl folgte der REST-Reihenfolge und traf den ALTEN -> der config_set-Guard brach ab, obwohl der neue Plan korrekt erzeugt war. Jetzt gewinnt der juengste; die alten bleiben Historie. Fund: `find where dst-address=<bare-IP>` matcht in RouterOS nie (nur `~` als Regex). Die Zeile stand als "Verifikation" in docs/transaktionen/TP23-agh-firewall.commands.rsc:7 und hat dort seit dem 2026-08-25 immer `0` gemeldet, ohne je etwas zu pruefen. Gleiche Klasse wie `nc -z` (09-01): eine Pruefung, die 0 meldet, sieht wie "fehlt" aus, nicht wie "kaputt". Abnahme: Reihenfolge am Geraet (vier neue Regeln vor dem Drop), Compliance 140/140 / 0 Drifts mit `RouterOS - ip-firewall-filter` (ordered, frisch bewertet, `actual` traegt die neuen Zeilen), `dig` vom VLAN10-Client gegen .30/.31/.36/.37 alle beantwortet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
BASE = "http://10.40.20.154:8080/api"
|
||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||
@@ -160,8 +161,22 @@ def create_plans(device_names, config_set, change_control_id, dry):
|
||||
# Sammelabfrage zuordnen (Lehre aus TP21.1: REST sortiert alphabetisch).
|
||||
q = urllib.parse.urlencode({"change_control_id": change_control_id, "depth": 1})
|
||||
found = req("GET", f"/plugins/golden-config/config-plan/?{q}").get("results", [])
|
||||
|
||||
# Mehrere Plaene unter derselben ccid sind normal, sobald ein Job mit geaendertem
|
||||
# Inhalt erneut laeuft -- die ccid benennt die Aenderung, nicht den Versuch.
|
||||
# 🔴 Wer dann die REST-Reihenfolge entscheiden laesst, nimmt nondeterministisch
|
||||
# den ALTEN Plan und laeuft in den config_set-Guard unten (am 2026-09-21 genau so
|
||||
# passiert: Abbruch trotz korrekt erzeugtem neuen Plan). Der Job hat eben einen
|
||||
# neuen Plan angelegt, also gilt der juengste -- die alten bleiben Historie.
|
||||
def created(p):
|
||||
raw = str(p.get("created") or "")
|
||||
try:
|
||||
return datetime.fromisoformat(raw.replace("Z", "+00:00")).replace(tzinfo=None)
|
||||
except ValueError:
|
||||
return datetime.min
|
||||
|
||||
by_name = {}
|
||||
for p in found:
|
||||
for p in sorted(found, key=created):
|
||||
name = (p.get("device") or {}).get("display")
|
||||
by_name[name] = p["id"]
|
||||
|
||||
@@ -418,11 +433,126 @@ def job_tp38_oob():
|
||||
]
|
||||
|
||||
|
||||
def job_tp45_agh_fw():
|
||||
"""TP45 Schicht A (2026-09-21): DNS-Freigaben fuer die neuen Resolver-Adressen .30/.31.
|
||||
|
||||
Der Resolver zieht von .36/.37 auf .30/.31 (TP32-Oktett-Klasse DNS `.3x`). Damit die
|
||||
Umstellung ohne Ausfall geht, laufen beide Adressen kurz parallel — also braucht der
|
||||
Core **beide** Freigaben. Diese hier ist ADDITIV: die vier .36/.37-Regeln bleiben
|
||||
stehen und fallen erst beim Rueckbau (eigener Schritt, wenn die Zaehler stillstehen).
|
||||
|
||||
Die vier Regeln sind ein Literal im Soll (`local_config_context_data.firewall_filter`,
|
||||
gepflegt mit `nautobot/tp45-agh-firewall.py`) — der Job zieht nur das Geraet nach.
|
||||
|
||||
🔴 `place-before` wird ueber Netmiko IGNORIERT (Lehre aus TP38, 2026-09-15): die Regel
|
||||
landet am Ketten-Ende, also HINTER dem finalen `drop` und waere wirkungslos, waehrend
|
||||
der Plan trotzdem „Completed" meldet. Deshalb `add` (appends) und danach `move` vor
|
||||
die Log-Regel — in der Reihenfolge, in der die Regeln stehen sollen (jedes `move`
|
||||
schiebt seine Regel unmittelbar vor das Ziel, also hinter die zuvor verschobenen).
|
||||
|
||||
🔴 **Zweite Falle, am 2026-09-21 beim ersten Lauf aufgeschlagen:** nicht der `move`
|
||||
war falsch, sondern der **Selektor** — `find where dst-address=<bare-IP>` matcht nie
|
||||
(Details und Tragweite in der Docstring von `sel()` unten). Der Plan meldete
|
||||
trotzdem „Completed", die Regeln lagen hinter dem `drop`. Deshalb ist die Abnahme
|
||||
hier **nicht** der Plan-Status, sondern die Position am Geraet.
|
||||
|
||||
KEIN LOCKOUT-RISIKO: die Regeln erweitern nur `accept` in der forward-Kette; der
|
||||
Mgmt-Pfad (Nautobot-SSH -> vlan99) ist von der Aenderung nicht beruehrt. Der Deploy
|
||||
laeuft von 10.40.20.154, das auf Regel 22 (established) matcht.
|
||||
"""
|
||||
ips = ["10.40.20.30", "10.40.20.31"]
|
||||
log = 'TP5: log-only, Vorstufe zum deny-by-default'
|
||||
|
||||
def sel(cmt, ip):
|
||||
"""Selektor auf genau eine Regel.
|
||||
|
||||
🔴 **`dst-address=<bare-IP>` matcht in `find where` NIE** — live am Core
|
||||
gemessen (2026-09-21): `find where dst-address=10.40.20.30` → **0 Treffer**,
|
||||
auch mit `/32`; `dst-address~"10.40.20.30"` (Regex) → 2. Deshalb steht hier
|
||||
`~`, nicht `=`.
|
||||
|
||||
⚠️ Die Tragweite ist größer als dieser Job: **eine Prüfung, die 0 meldet,
|
||||
sieht wie „nicht vorhanden" aus und nicht wie „kaputt".** Genau daran ist
|
||||
der erste Lauf dieses Jobs gescheitert — der `add` lief (der Guard sah
|
||||
„nicht vorhanden"), der `move` lief ins Leere, und der Plan meldete
|
||||
trotzdem „Completed". Dieselbe Zeile stand als *Verifikation* in
|
||||
`docs/transaktionen/TP23-agh-firewall.commands.rsc:7` und hat dort seit
|
||||
dem 2026-08-25 immer `0` gemeldet, ohne je etwas zu prüfen.
|
||||
Gleiche Klasse wie `nc -z` (2026-09-01) und das `sed`-Maskierungsmuster:
|
||||
**erst den Selektor verdächtigen, dann das Gerät.**
|
||||
"""
|
||||
return f'comment="{cmt}" dst-address~"{ip}"'
|
||||
|
||||
def rule(ip, proto, cmt):
|
||||
return (f'/ip firewall filter add action=accept chain=forward comment="{cmt}" '
|
||||
f'dst-address={ip} dst-port=53 protocol={proto}')
|
||||
|
||||
lines = []
|
||||
for ip in ips:
|
||||
# Guard + move getrennt: nur der Guard ist idempotent (ein zweiter Lauf
|
||||
# verdoppelt die Regeln nicht), der move läuft immer — er ist auch der
|
||||
# Reparaturweg, wenn eine Regel schon existiert, aber falsch einsortiert ist.
|
||||
for proto, cmt in (("udp", "TP5-E2: LAN->AGH dns"), ("tcp", "TP5-E2: LAN->AGH dns-tcp")):
|
||||
lines.append(f':if ([:len [/ip firewall filter find where {sel(cmt, ip)}]] = 0) '
|
||||
f'do={{ {rule(ip, proto, cmt)} }}')
|
||||
for ip in ips:
|
||||
for cmt in ("TP5-E2: LAN->AGH dns", "TP5-E2: LAN->AGH dns-tcp"):
|
||||
lines.append(f'/ip firewall filter move [find where {sel(cmt, ip)}] '
|
||||
f'destination=[find where comment="{log}"]')
|
||||
# Der Anker ist bewusst die Log-Regel (Baseline, stabil) und nicht `caddy`: haengt
|
||||
# sie unmittelbar vor der Log-Regel, landen die vier Regeln DAHINTER -- der AGH-Block
|
||||
# bleibt so zusammen, wie das Soll ihn fuehrt. Waehlte man `caddy` als Ziel und die
|
||||
# Regel fehlte, laegen die vier hinter dem `drop` = funktional kaputt; so bleibt im
|
||||
# Fehlerfall nur eine Reihenfolge-Abweichung (Compliance-Drift, kein Ausfall).
|
||||
lines.append(f'/ip firewall filter move [find where comment="TP31: AP-cloudflared -> caddy-bf1"] '
|
||||
f'destination=[find where comment="{log}"]')
|
||||
# ⚠️ Diese Zeile meldet NUR Anwesenheit (je IP 2 Regeln erwartet), NICHT die
|
||||
# Position -- sie kann also gruen aussehen, waehrend die Regeln hinter dem `drop`
|
||||
# liegen. Genau daran ist der erste Lauf gescheitert. **Die Abnahme ist der
|
||||
# Geraete-Read nach dem Deploy** (`print terse proplist=comment,packets`).
|
||||
lines.append(
|
||||
':log info ("tp45-agh-fw: .30=" . '
|
||||
'[:tostr [:len [/ip firewall filter find where dst-address~"10.40.20.30"]]] . " .31=" . '
|
||||
'[:tostr [:len [/ip firewall filter find where dst-address~"10.40.20.31"]]] . " log=" . '
|
||||
f'[:tostr [:len [/ip firewall filter find where comment="{log}"]]])')
|
||||
|
||||
return [(["crs326-core"], "\n".join(lines), "TP45-agh-firewall")]
|
||||
|
||||
|
||||
def job_tp45_agh_dhcp():
|
||||
"""TP45 Schicht A (2026-09-21): DHCP-DNS-Server der 7 Netze .36,.37 -> .30,.31.
|
||||
|
||||
Der Core verteilt in ALLEN sieben Netzen `dns-server=10.40.20.36,10.40.20.37`
|
||||
(live geprueft 2026-09-21). Das Template (Z.137) ist auf .30,.31 gehoben; dieser Job
|
||||
zieht das laufende Geraet nach.
|
||||
|
||||
MODIFY, kein add: das Netz existiert -> `set [find]`, nicht die `add`-Zeile der
|
||||
intended config (die wuerfe `already have such address`). `[find]` ohne Filter
|
||||
trifft alle 7 Netze. Gleiche Form wie `job_tp42_dhcp_domain`.
|
||||
|
||||
⚠️ **Reihenfolge zwingend:** erst `tp45-agh-fw` deployen. Sonst liefert DHCP Adressen
|
||||
aus, die die Firewall noch nicht durchlaesst — unter deny-by-default faellt DNS aus
|
||||
allen VLANs aus.
|
||||
|
||||
KEIN LOCKOUT-RISIKO: eine DHCP-Option beruehrt weder Routing noch Mgmt-Zugang;
|
||||
Clients ziehen sie erst beim naechsten Lease-Renew (lease-time 1d, GUEST 12h).
|
||||
"""
|
||||
core_set = "\n".join([
|
||||
':do { /ip dhcp-server network set [find] dns-server=10.40.20.30,10.40.20.31 } '
|
||||
'on-error={ :log info "tp45-agh-dhcp: set geworfen" }',
|
||||
':log info ("tp45-agh-dhcp: auf-neu=" . [:tostr [:len [/ip dhcp-server network '
|
||||
'find where dns-server~"10.40.20.30"]]] . "/7")',
|
||||
])
|
||||
return [(["crs326-core"], core_set, "TP45-agh-dhcp")]
|
||||
|
||||
|
||||
JOBS = {
|
||||
"drift-cleanup": job_drift_cleanup,
|
||||
"mgmt-acl": job_mgmt_acl,
|
||||
"tp42-dhcp-domain": job_tp42_dhcp_domain,
|
||||
"tp38-oob": job_tp38_oob,
|
||||
"tp45-agh-fw": job_tp45_agh_fw,
|
||||
"tp45-agh-dhcp": job_tp45_agh_dhcp,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user