From a0ba87d5551faa371d3949be43707483072997a6 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Mon, 7 Sep 2026 13:30:24 +0200 Subject: [PATCH 1/2] TP36 A4/A5/A6: Index/Status-SSoT-Cutover nach Gitea abgeschlossen - teilprojekte.md: Quick-Index-Tabelle + "Ist-Stand (Kurz)"-Block entfernt (-493 Z.), Kopf zeigt aufs Gitea-Board (Labels status:*/thema:*); Governance/Genehmigte Ausnahmen unter eigenem Header behalten; TP36 reframed (Nautobot->Gitea, Scope = Index/Status, Nautobot nur noch Live-Geraetedaten). Alle 33 ### TPnn-Prosa-Abschnitte intakt. - gitea-tp-sync.py: an das neue SSoT-Modell angepasst - aktive TPs sind SSoT direkt in Gitea, das Tool gleicht nur noch das Archiv ab und fasst offene Issues nie an. Dry-Run 0/0. - CLAUDE.md: Pflichtlektuere #1 = Gitea-Board (Status) + Prosa-Datei (Detail); neue Sektion "SSoT & Branch-Konvention"; TP-Management-Sektion nachgezogen. /projektstatus-Skill (beide globalen Kopien ~/.claude + ~/.config/opencode) auf die Gitea-API umgestellt (Modus A Board / Modus B Markdown-Fallback) - liegt ausserhalb des Repos. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 30 ++- bin/gitea-tp-sync.py | 48 ++-- docs/teilprojekte.md | 545 +++---------------------------------------- 3 files changed, 70 insertions(+), 553 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ee8c699..49d0121 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der ## Pflichtlektüre pro Sitzung (Reihenfolge) -1. **`docs/teilprojekte.md`** (Quick-Index-Block) — kanonischer Stand, Scope, Abhängigkeiten, **offene** TPs. **1 Read reicht.** +1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)* 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`**. 4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md) @@ -34,6 +34,21 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der 6. 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. 7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift. +## SSoT & Branch-Konvention (TP36-Cutover 2026-09-07) + +- **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) · + **Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) · + **Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**, + nicht mehr in einer Repo-Datei. Sync-/Abgleich-Werkzeug fürs Archiv: `bin/gitea-tp-sync.py` (Dry-Run default, + Token aus `~/.hermes/.env`, **nie ausgeben**). +- **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`, + uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der + Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`), + kollidieren TP-Branches nicht mehr auf der Steuerungs-Doku — nur noch der eigene `### TPnn`-Prosa-Abschnitt + wird angefasst. +- **TP anlegen/schließen:** neues aktives TP = **direkt als Gitea-Issue** (Labels setzen), Prosa-Abschnitt + im Repo dazu; abschließen = Issue `closed`, Prosa wandert bei Bedarf ins Archiv. **Keine Handtabelle mehr.** + ## Was du sofort wissen musst (Inventur 2026-08-27) - **`bf-infra`** ist aktiver Master **mit Gitea-Remote**: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git` (live verifiziert 2026-08-30). ⚠️ **Die frühere Zeile „lokal, kein Gitea-Remote → verloren bei Festplattencrash" ist überholt** — sie stammt aus der Inventur vom 2026-08-27 und war damals richtig; das Remote existiert seit spätestens 2026-08-28 (`origin/main` = `b917691`). **Die Backup-Lücke ist damit keine Struktur-, sondern eine Disziplinfrage: `git push`.** Am 2026-08-30 lagen vier Commits nur lokal. 🔴 **Das Remote ist nur so viel wert wie der letzte Push** — der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist; **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Merkregel:** diese Zeile hat eine Session dazu gebracht, „kein Remote" zu behaupten, statt `git remote -v` zu tippen — vor jeder Aussage über Remotes/Backups **live prüfen**. @@ -50,12 +65,15 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der Wenn während einer Session ein neues Teilprojekt, eine Abhängigkeit oder ein Querschnittsthema sichtbar wird, das bisher in keinem TP abgebildet ist: - Vorschlagen und mit dir abstimmen (nicht eigenmächtig anlegen). -- Auf dein Okay hin anlegen: Quick-Index-Zeile + `### TP`-Abschnitt mit Status `🔜`, Scope, - Abhängigkeiten, Abgrenzung. Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus - verworfenen TPs bleiben frei). -- Ist-Stand-Block mitziehen (Datum, betroffene Zeilen). +- Auf dein Okay hin anlegen: **Gitea-Issue** `TPnn — ` mit Labels `status:🔜`(todo) + + passendem `thema:*` + Body (Anriss + Verweis auf den Prosa-Abschnitt) **und** einen + `### TP`-Prosa-Abschnitt in `docs/teilprojekte.md` (Scope, Abhängigkeiten, Abgrenzung). + Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus verworfenen TPs bleiben frei). +- **Status/Reihenfolge/„was lief zuletzt" gehen ins Gitea-Issue** (Label ändern, Kommentar), **nicht** + in eine Quick-Index-Tabelle oder einen Ist-Stand-Block — die gibt es seit dem TP36-Cutover (2026-09-07) + nicht mehr in der Datei. -**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt.** Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — und der **Ist-Stand-Block wird bei jeder Änderung mitgezogen**, nicht nur der betroffene TP-Abschnitt. +**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis) und ihr Gitea-Issue ist `closed` (Label `status:✅`/`❌`). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt** (Issue dann `reopen`). Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — nur noch der betroffene `### TPnn`-Prosa-Abschnitt wird angefasst. --- diff --git a/bin/gitea-tp-sync.py b/bin/gitea-tp-sync.py index c8dee2d..4623715 100644 --- a/bin/gitea-tp-sync.py +++ b/bin/gitea-tp-sync.py @@ -3,11 +3,15 @@ TP36 (reframed) — teilprojekte.md Index/Status → Gitea Issues (admin/bf-infra). Idempotenter Sync (get-or-create, Match ueber "TPnn" im Titel): - - Quick-Index-Tabelle in docs/teilprojekte.md -> aktive Issues (open/closed je Marker) - - Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die NICHT aktiv sind) + - Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die in Gitea + NICHT offen sind; ein reopentes TP mit Archiv-Zeiger bleibt offen). - Labels status:* (Marker) + thema:* (Gruppierung) werden vorab get-or-create. -SSoT-Modell: Gitea fuehrt Index+Status; die per-TP-Prosa bleibt in docs/teilprojekte.md. +SSoT-Modell (seit TP36-Cutover 2026-09-07): Gitea fuehrt Index+Status — aktive TPs werden +DIREKT als Gitea-Issue angelegt/gepflegt, NICHT mehr aus einer Datei-Tabelle abgeleitet +(die Quick-Index-Tabelle wurde aus teilprojekte.md entfernt). Dieses Werkzeug haelt nur noch +die ARCHIVIERTEN (closed) TPs mit teilprojekte-archiv.md idempotent abgeglichen und fasst +offene Issues nie an. Die per-TP-Prosa bleibt in docs/teilprojekte.md / teilprojekte-archiv.md. Token: ~/.hermes/.env (GITEA_ACCESS_TOKEN, GITEA_HOST) — wird NIE ausgegeben. Aufruf: @@ -100,25 +104,9 @@ def api(method, path, body=None): # --------------------------------------------------------------------------- parse -def parse_active(): - """Quick-Index-Zeilen: > | TP44 | ⏳ | Kurztitel … | Datum |""" - out = [] - row = re.compile(r"^>\s*\|\s*(TP[\d.]+)\s*\|\s*([^\s|]+)\s*\|\s*(.+?)\s*\|\s*(.*?)\s*\|\s*$") - for line in ACTIVE.read_text().splitlines(): - m = row.match(line) - if not m: - continue - tp, marker, title, date = m.groups() - num = tp[2:] - # Titel auf den Kern kuerzen (vor dem ersten " — " / " (" wenn lang) - short = re.split(r"\s+—\s+|\s*\(", title)[0].strip() - short = re.sub(r"\*\*|`", "", short)[:80] - out.append({"tp": tp, "num": num, "marker": marker.strip(), - "title": short, "date": date.strip(), "src": "active"}) - return out - - -def parse_archiv(active_nums): +def parse_archiv(exclude_nums): + """Archiv-Header -> archivierte TPs. exclude_nums = TP-Nummern, die in Gitea OFFEN sind + (reopente TPs mit Archiv-Zeiger) -> die bleiben offen, nicht als closed abgleichen.""" out = [] hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\s*—\s*(.+?)\s*▸", ) seen = set() @@ -128,7 +116,7 @@ def parse_archiv(active_nums): continue tp, title = m.groups() num = tp[2:] - if num in active_nums or num in seen: + if num in exclude_nums or num in seen: continue seen.add(num) short = re.sub(r"\*\*|`", "", title).strip()[:80] @@ -164,22 +152,26 @@ def all_issues(): def main(): print(f"Gitea {HOST} repo {OWNER_REPO} mode={'COMMIT' if COMMIT else 'DRY-RUN'}") - active = parse_active() - active_nums = {a["num"] for a in active} - archiv = parse_archiv(active_nums) - items = active + archiv - print(f"Geparst: {len(active)} aktive + {len(archiv)} archivierte = {len(items)} TP\n") st, existing_labels = api("GET", f"/repos/{OWNER_REPO}/labels?limit=100") label_id = ensure_labels(existing_labels if isinstance(existing_labels, list) else []) issues = all_issues() by_tp = {} + open_nums = set() for iss in issues: # Titel ist "TPxx — …"; volle ID inkl. Buchstaben/Bindestrich (TP14.2a, TP-R) bis zum " — " m = re.match(r"^(TP[\w.\-]*?)\s+—", iss["title"]) if m: by_tp[m.group(1)] = iss + if iss.get("state") == "open": + open_nums.add(m.group(1)[2:]) + + # Aktive TPs sind seit dem TP36-Cutover SSoT direkt in Gitea -> nur das Archiv abgleichen, + # und offene (reopente) TPs davon ausschliessen, damit sie nicht faelschlich geschlossen werden. + items = parse_archiv(open_nums) + print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: " + f"{len(open_nums)} offen, nicht angetastet)\n") created = updated = 0 for it in items: diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 6cb02a0..7988b63 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -25,479 +25,24 @@ IP-Blöcke: `.1–10` Router/Gateways · `.11–20` Switches/APs. **GW je VLAN = Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`. *(Historisch, überholt: „ap1=.1, ap2=.2 · ALLNET=.11, Omada=.12" — aus der Zeit vor dem Core-Cutover.)* -## Ist-Stand (Kurz) +## Status & Index → Gitea-Board (SSoT seit 2026-09-07, TP36-Cutover) -**Stand 2026-09-04 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben.** -**Compliance 140/140, 0 Drifts** (2026-09-04 nachmittags). Der TP25-Kanalplan ist **committed**: -Template auf DFS-Frequenz-Listen umgestellt (`ch-5g-ap1=5260,5280`, `ch-5g-ap2=5300,5320`) + die -ap1-Provisioning-Zeilen wieder aufgenommen → Transaktion `TP25-kanalplan-ap1-live` geschlossen und der -`wifi-provisioning`-Drift weg. `wifi-security` gruen (`ft=no` auf `sec-main`). -🔴 **Zusätzlich live behoben (nicht compliance-sichtbar):** ap3 trug auf `wifi1/wifi2` einen Werks-Inline -`security.ft=yes` — gemischtes FT-ESS, das die 3/3-Strich-Abbrüche vom 09-04 erklärt. Unset → uniform -`ft=no` on-air; Bring-up-Quelle `tp28-ap3-phase2-cap.rsc` um `!security.ft` ergänzt (s. Fund-E-Block). -*(Die Fassung vom 2026-08-16 stand bis zum 18. hier und war an vier Stellen überholt. Wer diesen Block -liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)* -- 🗺️ **TP37/34/36-Strang geplant + TP44-Drift bereinigt (2026-09-06, „erst planen, nichts bauen"):** - Live gegen Nautobot geprüft — **TP37 Punkt 3 (CF-Connectoren) ist faktisch durch TP44 erledigt** - (native `nautobot.cloud`-Modelle, Etappe 1 live: Account 1/ResType 6/Network 4/Service 14). Echt offen - bleibt **TP37 Punkt 1+2** (Substrat: pve/NAS/macmini-Hosts + LXC/Docker als `virtualization.VM`, Dienste - als `ipam.Service` — live **VM 0/Cluster 0/Service 0/CustomField 0**), und darauf sitzt **TP34** (Caddyfile - via GC). **TP36** (Live-Daten-CustomFields + Datei-Split) ist davon **unabhängig** → parallel. Datenmodell- - Entscheid: **native Modelle** (konsistent zu TP44), keine eigene App. **TP44 nachgezogen:** eigene - Quick-Index-Zeile + Abschnitt, Seeder `nautobot/tp36-…` → `tp44-cf-estate-seed.py` umbenannt. Runbook: - [`docs/handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md). **Kein Nautobot-Write.** -- ✅ **TP25 Multi-Pin-Konsole (2026-09-04):** die Klick-Konsole (`docs/tp6-marker/server.py`) ist auf - **N Clients** generalisiert und hat eine **Pin-UI** bekommen. Sie liest die Client-Liste aus - `geraete.txt` (schreibt `tp25-walk-messung.sh`), zeigt je Client AP/Signal/Ping-Verlust aus - `sampler-.csv`/`walk-ping-.csv` (kein eigenes SSH mehr) und pinnt N Clients band-genau - über `tp25-pin-multi.sh` direkt in der Konsole. Die alte 2-Client-Gegen-AP-Automatik ist entfernt. - Handoff: [`docs/handoff-20260904-tp25-konsole-n-clients.md`](handoff-20260904-tp25-konsole-n-clients.md). -- ✅ **TP25 Fund E GESCHLOSSEN (2026-09-02):** die vorgemerkte Abschlussmessung ist gelaufen — - `reauthenticating` am 09-02 nur noch **4 in 8,6 h** (alle iPhone `F2:62`, isolierte Einzel, 0 Ausfälle) - gegen Baseline 93 bzw. Hebel-1 **19/h**. Verlauf **218 → 40 → 4**; Vorbedingung geprüft (Core-Uptime - 1d18h, `caps joined=0` → keine Reboot-Artefakte). ⇒ **7.24.1 (TP27) war die Ursache/der Fix, nicht - `ft-over-ds`.** Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen, **kein Geräte-Write**. **Am - 2026-09-04 hat Christoph `ft=no` auf `sec-main` als Dauerzustand entschieden** (Hebel 2 doch gezogen, - Template + bf-golden-config + Nautobot-Sync, `wifi-security` gruen). 🔴 **Nachtrag 2026-09-04:** „ohne FT" - galt bis dahin **nur für Manager/ap1/ap2** — **ap3** trug einen Werks-Inline `security.ft=yes` auf - `wifi1/wifi2` (Bring-up-Skript unset `!ft-over-ds`, aber nicht `!ft`; die Abnahme zählt nur benannte - security-Objekte und sah den Inline-Wert nicht). Das ist ein **gemischtes FT-ESS** und erklärt den - Abbruch bei 3/3 Strichen: Log zeigte wiederkehrende Strong-Signal-Disconnects (−43/−45 dBm) **nur auf - ap3** (`cap-wifi2/4`, `dirtyvader-main`). Fix: `!security.ft` auf ap3 → uniform `ft=no`; Quelle - korrigiert. **Merkregel: nach jeder `sec-main`-FT-Änderung jeden CAP einzeln auf `security.ft` prüfen — - CAP-lokale Inline-Overrides sind compliance-unsichtbar.** Offen in - TP25 bleibt nur der separate Punkt **ap2 unterperformt** (2 m/−80 dBm). Handoff: - [`docs/handoff-20260902-tp25-fund-e-messung.md`](handoff-20260902-tp25-fund-e-messung.md). -- ⏸️ **TP41 CF-Access-Reparatur — ZURÜCKGESTELLT 2026-09-01 (User-Entscheid; Rest wartet auf Info):** Session 720h, - login_method-Loch zu (`bf-users` email-only), `zt-`-Gruppen, vw voll Bypass + `vw/admin` per WAF - geblockt (Mobilfunk-verifiziert), API-Token auf vollständiges `…4148` konsolidiert. **T4 ✅** - (ein Access-Service-Token `zt-svc`, Terpene-Worker per wrangler auf zt-svc umgestellt, Alt-Tokens - gelöscht). **T3 ✅** (WARP Default-Profil fallback_domains `emsenhuber.net → .36/.37`). **T2 ✅** - (WAF-Regel gelöscht, Caddy-Origin-404 für `/admin` via CF-Header; Edge=404, lokal=200). - **T5 ✅** (GitHub- **und** Google-Login laufen, User-bestätigt 2026-09-01; Google via Consent-Fix §A). - **Facebook/Meta für TP41 abgesagt** (optional, kein CF-IdP angelegt) — Geli über OTP - `angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt. - **Token-Altlast geklärt:** `b00722`=live Caddy-DNS-Token, `d47435`=stale Kopie → **✅ d47435 revoked**, - **✅ T3-WARP-Client-Verify** (User). **Geli CF-seitig fertig** (Enrollment via `zt-humans` + `geli`-Policy, - 2026-09-01 live geprüft) → verbleibt nur ein **Device-Handgriff bei Geli** (WARP-Client, Team `emsenhuber`). - **Claude-/CF-Seite damit vollständig.** Handoff: [`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md) · [Follow-up](handoff-20260901-tp41-followup.md). -- ✅ **TP14.4 als in TP41 T4 aufgegangen markiert (2026-09-02):** kein eigenständiger `claude-mcp`-Token - mehr — er wurde in TP41 T4 zu `zt-svc` konsolidiert, `mcp/cloudflare/.env` trägt jetzt - `CF_ACCESS_CLIENT_ID`/`_SECRET`. Die TP14.4-Ist-Stand-Notiz („.env leer") war seit 09-01 stale; - Quick-Index + TP-Abschnitt nachgezogen. -- ✅ **TP31 abgeschlossen markiert (2026-09-02):** Schritt 5+6 sind über den Piloten TP16.3 (09-01) - miterledigt — `bf-public` 12 conns, vier öffentliche Namen über Caddy, Alttunnel weg. Abnahme-Tool - `caddy/cf-sync.py` von einem Fehlalarm befreit (prüfte nur `http://`, gebaut ist `https://…:443`) und - **grün** (nur `ds-bf` als bewusst intern). Schritt-6-Teil „MCP auf FQDN" durch TP35 Schicht 1 - umgekehrt (LAN-IP), damit gegenstandslos. -- ✅ **TP42 durch (2026-09-02):** interner Namensraum auf **einen** Suffix `bf.emsenhuber.net` vereinheitlicht. - (A) DHCP-domain über die GC-Pipeline geflippt (Template + `push-gc-template.py` `7a4068c4` + neuer Job - `tp42-dhcp-domain` + Deploy an core) → **7/7 live, Compliance 140/140**. Nebenbefund: Remote-GC-Template - hing hinterher (`.bf.lan`), mit angeglichen. (B) `.16` (`adguard_bf1`) aus dem **mit FM geteilten** Stack - 195 getrimmt-redeployt — `.16` weg, `.17` (FM) läuft (kurzer ~40 s Blip). Nautobot-Doku-Teil (Scope 4) - an TP37 gekoppelt. -- 🔜 **TP42 Namens-Vereinheitlichung (2026-09-01, angelegt):** internes `.bf.lan` (Core-DNS, DHCP-Search- - Domain) und die `.bf.emsenhuber.net`-Aliase (Caddyfile) auf **einen** Namensraum `emsenhuber.net` - ziehen; stale `.16/.17`-Rewrites abbauen; Sollzustand in Nautobot. Handoff: - [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md). -- 🔜 **TP43 dw-Standort-DNS-Uplink (2026-09-01, angelegt):** dem dw-Standort einen eigenen Resolver/ - DNS-Uplink mit **site-spezifischen Overrides** geben (erster Fall `420.teilzeitladen.xyz` → BF-Caddy - über WG), **inkl. Verhalten bei WG-Disconnect** (Override auf BF-interne IP fällt sonst aus). FritzBox - standard kann keinen Per-Host-Override. Geli-Sofortlösung = **WARP** (nicht Teil von TP43). Cross-Projekt. -- ✅ **TP16.3 ABGESCHLOSSEN (2026-09-01):** Flip **und** Abbau durch. Öffentlich laufen nur noch - `bf-public` (12 Connectoren) und `fm` — die drei Alttunnel `bf-mm`/`bf-nas`/`bf-pve` **und** der - projektfremde `terpene-db` sind gelöscht, **7 cloudflared-Container** entfernt (inkl. - `terpene-cloudflared`, weil `420.teilzeitladen.xyz` nun über Caddy/CF läuft — vor der Löschung mit - echtem Origin-Inhalt belegt). §6 wurde auf User-Entscheid vorgezogen. **TP wandert ins Archiv.** -- 🆕 **TP16.3 §5 DURCH (2026-09-01): `n8n`/`gitea`/`hass`/`vw` laufen über `bf-public`** — der - öffentliche Verkehr geht damit erstmals über **Caddy** statt an ihm vorbei, und die - Host→Origin-Zuordnung lebt nur noch im `Caddyfile`. Je Name einzeln geflippt, dazwischen gemessen; - Antwortzeiten vorher/nachher **unverändert** (Caddy im Pfad kostet nichts Messbares). - Nachweise: `gitea` echte Gitea-Antwort · `hass` **WebSocket `101 Switching Protocols`** (= der - Companion-App-Pfad, den das Runbook als „nur vom User prüfbar" führte) · `vw` `/alive` + `/api/config` - 200, WS-Hub `401` von Vaultwarden selbst · `n8n` von Christoph im Browser bestätigt. - 🚫 **§6 (Abbau) bewusst NICHT** — die drei Alttunnel bleiben als Rückfallebene (healthy/8). - 🔴 **Christoph hat den Flip ausdrücklich vorgezogen freigegeben** („aus Gründen"); die 7-Tage-Regel - betrifft ohnehin §6, nicht den Flip. -- 🆕 **TP39 ABGESCHLOSSEN + archiviert (2026-09-01):** letzter offener Punkt — der Resolver-Sweep — - ist durch: **pve alle 13 LXCs** auf `10.40.20.1`, und die **DS923-Container via Portainer** (ep14) - gegengeprüft — kein `.41.254`-Override, alle laufenden lösen in **66–110 ms** auf (inkl. Grenzfall - `gitea`, erstellt 2026-07-10), kein eingefrorener Dead-Resolver, kein Recreate nötig. Die - Abnahme-Regel („vor jedem LXC-Reboot `systemctl --failed` + Port-Vergleich") steht als - CLAUDE.md-Gotcha, der Sweep ist als wiederkehrende Kontrolle dort verankert. **Volltext → Archiv.** - 🔴 Rest offen nur in **anderen** TPs: root-DB-PW-Rotation (→ TP30), HA-Recorder-Retention (→ TP40). -- 🆕 **TP39 fast durch (2026-09-01):** `skip-name-resolve` auf der MariaDB **aktiviert** (mysql.user - vorher geprüft — kein Hostname-Grant, gefahrlos; Wirkung: gitea/vw/hass 0 von 36 frischen Anfragen - langsam). LXC 302 war **100 % voll** → `+30G` online (95 GB/69 %). 🔴 **root-DB-Passwort dabei ins - Transkript geraten** (`.my.cnf` ohne `=`), zu rotieren (→ TP30). -- 🆕 **TP40 angelegt (2026-09-01, User-Okay):** HA-Recorder-Retention — die `homeassistant`-DB frisst - 59 GB (`states.ibd` **41 GB**), unbegrenzt gewachsen. Das `+30G` aus TP39 ist ein Puffer; die - dauerhafte Lösung (`purge_keep_days`, `exclude`, `purge repack`) ist HA-seitig und **Christophs - Policy-Entscheidung** — kein eigenmächtiges Löschen. -- 🆕 **TP39: `networking.service` in vier Containern repariert (2026-09-01).** Ursache war dieselbe - Altlast wie beim Resolver: tote `auto`-Stanzas in `/etc/network/interfaces` aus dem VLAN1-Schema - (`eth0` → `10.40.41.x`) und alten Versuchen — `ifup -a` scheiterte an Interfaces, die es nicht gibt, - während `veth20` lief. **Abnahme war ein echter Reboot:** influxdb kam danach **von selbst** hoch - (vorher gar kein Startversuch). Alle 13 LXCs stehen jetzt auf `dns=10.40.20.1`, `networking` überall - sauber. **Offen in TP39: nur noch `skip-name-resolve` und der wiederkehrende Sweep.** -- 🆕 **TP39 angelegt (2026-09-01, User-Okay):** LXC-Hygiene auf pve — die zwei Restpunkte aus dem - Wartungsfenster unten (`skip-name-resolve` auf der MariaDB, kaputtes `networking.service` in vier - Containern) plus Resolver-Sweep als wiederkehrende Kontrolle. **Ein** TP, weil beides dieselbe Ecke - betrifft und in einem Fenster erledigt wird. -- ✅ **GELÖST 2026-09-01: der 10-Sekunden-Zoll auf `gitea`/`vw`/`hass` — es war Reverse-DNS auf einen - toten Resolver.** MariaDB (LXC **302**, `10.40.20.23`) hatte `skip-name-resolve` **auskommentiert** - und machte damit bei **jeder neuen Verbindung** einen Reverse-Lookup; ihr `/etc/resolv.conf` zeigte - auf **`10.40.41.254`** — den toten VLAN1-Resolver aus dem alten Schema (`nc` bestätigt tot). - **Gemessen im Container: 10.010 ms je Lookup.** Bestehende Pool-Verbindungen waren schnell, neue - kosteten 10 s — daher wirkte es sporadisch und unerklärlich. - 🔑 **Die Blast-Radius war größer als gedacht:** an derselben DB hängen **Vaultwarden (`.127`), - Home Assistant (`.105`) und gitea (`.157`)** — belegt über die Verbindungstabelle, nicht vermutet. - *(Die erste Zuschreibung „vaultwarden nutzt SQLite, also nicht betroffen" war eine **Annahme** und - falsch — Christoph hat sie korrigiert, die Verbindungsliste hat es bewiesen.)* - **Fix (Wartungsfenster, User-Freigabe):** `pct set --nameserver 10.40.20.1` + Reboot für - **fünf** LXCs — `302 mariadb`, `102 mqtt5`, `106 influxdb2`, `107 redis`, `150 iob-exposed`. - **Nachher: Lookup 21–24 ms statt 10.010 ms; gitea 0 von 25 Proben langsam** (vorher ~10 % bei - 10,1 s), langsamste Probe 75 ms. Alle drei Dienste wieder verbunden. - ⚠️ **Strukturell offen:** `skip-name-resolve` ist weiterhin aus — der Lookup ist jetzt nur schnell, - nicht abgeschafft. Ihn zu setzen bricht alle Grants, die auf **Hostnamen** statt IPs lauten; das - gehört vorher gegen `mysql.user` geprüft (DB-Zugang fehlt: `root@localhost` ohne Passwort verweigert). -- 🔴 **Latenter Fund aus demselben Wartungsfenster: `networking.service` und - `ifupdown-wait-online.service` sind in `101`, `102`, `106`, `107` fehlgeschlagen.** Folge: - Dienste mit `After=network-online.target` starten nach einem Reboot **nicht von selbst** — - **influxdb2 kam nach dem Neustart nicht hoch**, Unit `enabled`, aber **kein einziger Startversuch - im Journal**. Von Hand gestartet lief es sofort. - 🔑 **Nicht durch diese Session verursacht:** `101 iob-master` wurde nie angefasst und zeigt dieselben - fehlgeschlagenen Units — die Container liefen nur 13+ Tage ohne Neustart, deshalb fiel es niemandem - auf. **Merkregel: diese vier Container sind einen Reboot davon entfernt, still Dienste zu verlieren.** -- 🔴 **Befund am Rand des Flips, NICHT von ihm verursacht: `gitea` hängt sporadisch exakt ~10,2 s.** - ~10 % der Anfragen, quer über Web-UI, **`/api/healthz`** und den **Git-Pfad** (`info/refs`) — also - kein Rendering-Problem. `vw` und `hass` über **dieselbe** Strecke: **0 von 30**. Damit sind CF-Edge, - AP-Connector, Firewall und Caddy entlastet; übrig bleibt **Caddy → `10.40.20.157:3000`** oder gitea - selbst. **Immer ~10,15–10,43 s** = Timeout-Signatur (klassisch: ein toter Resolver, s. den - VLAN20-Docker-DNS-Gotcha in der CLAUDE.md), kein Lastproblem. Weiter nur mit LAN-Zugriff. -- 📎 **Drei Messfallen, die diese Session gekostet haben** (alle drei sind Standort-, keine Netzfehler): - 1. **Split-Horizon macht Tests von zuhause wertlos:** der Haus-Resolver liefert für alle vier Namen - direkt `10.40.20.40`, die Anfrage sieht Cloudflare **nie**. Prüfen geht nur mit festgenagelter - CF-Edge-IP (`curl --resolve :443:188.114.96.10`) oder von außerhalb. - 2. **Access antwortet je nach Standpunkt verschieden:** `n8n` gibt aus dem Haus **200** (die - `bf4`-Policy trägt die WAN-IP), von außen **302** zum Login. Beides gesund. Für die drei - `bypass`-Namen (`hass`/`vw`/`gitea`) gibt es gar keinen Login — die Access-App heisst zu Recht - `hass, vw, gitea`, sie führt alle drei in `self_hosted_domains` (das Feld `domain` zeigt nur den - ersten). **Ein Rename wäre eine Verschlechterung gewesen.** - 3. 🔴 **„Der Host ist ausgefallen" war der eigene Standortwechsel.** Ping/SSH/Caddy waren tot, - weil die Session-Maschine auf einen **Hotspot** gewandert war (Default-GW `172.20.10.1`) — - dieselbe Klasse wie die WARP-Falle von 2026-08-13, nur ohne WARP. Es wurde daraufhin ein bereits - erfolgreicher `n8n`-Flip **grundlos zurückgerollt**. **Merkregel: bevor ein Ausfall gemeldet wird, - `route -n get default` lesen** — ein Standortwechsel sieht exakt aus wie ein Totalausfall. -- ⚠️ **Nebenbefund: Caddy führt gar kein Access-Log.** Im `Caddyfile` steht keine `log`-Direktive, im - Journal landen nur `info`/`warn`. Der Abnahmepunkt „Caddy sieht den Request" (Runbook §5.3) **kann - nicht funktionieren** — ein erfolgreicher Request hinterlässt dort nichts, und leere Ausgabe liest - sich wie „kam nie an". Dass der öffentliche Reverse Proxy keine Requests protokolliert, ist für sich - eine Lücke (gehört zu TP31/TP34). -- 🆕 **TP16.3: die sechs AP-Drifts sind ins Soll gehoben (2026-08-31) — Compliance 139/140.** - `veth` / `ip address` / `ip firewall nat` der drei cloudflared-Connectoren stehen jetzt im - **AP-Template** (Block `[7]`) und im ConfigContext `ap-common` (`ap.container`), **nicht** in einer - `gc-transaktion`: ein Dauerzustand gehört ins Soll, eine Transaktion ist ein befristeter Versuch. - **Kein Re-Baseline, kein Rückbau, keine Zeile aufs Gerät** — nur die SoT nachgezogen. - 🆕 **28. ComplianceRule `interface-veth`** (vorher gab es für `/interface veth` keine ⇒ ein - gelöschtes veth hätte **nichts** gemeldet). 🔴 **Zwei Funde dabei:** (a) der veth-Export trägt - **zwei gerätevergebene MACs** — eine nackte Regel hätte Dauer-Drift auf allen drei APs erzeugt; - gelöst mit zwei eng auf die veth-Zeile verankerten `ConfigReplace`-Regeln (`60-`/`61-veth-*`, - `nautobot/ingest-veth-rule.py`), bewusst mit der vorhandenen Maske `<>`, weil die im - Repo zugleich die **Push-Sperre** ist. (b) **Der erste Compliance-Lauf meldete `SUCCESS` und war - trotzdem unvollständig** (139 statt 140 Einträge, `crs326-core`/`interface-veth` fehlte; - Job-Log: `RENDER COMPLIANCE TASK GROUP failed: Broken pipe`). Zweiter Lauf sauber. - ⚠️ **Merkregel: nach einem Refresh nicht nur die Konformitätsquote lesen, sondern die ANZAHL der - Einträge** — sie muss `Regeln × Geräte` sein. Eine fehlende Kombination sieht aus wie „die neue - Regel greift nicht" und ist in Wahrheit ein halb gelaufener Job. - *(Die damals übrige `crs326-core`/`wifi-security`-Drift = Transaktion `TP25-fund-e-ft-over-ds` ist mit - dem Rückbau 2026-09-01 gegenstandslos; Fund E am 2026-09-02 geschlossen, Compliance 140/140.)* -- 🆕 **MCP-Bestand 2026-08-28 korrigiert:** **9 MCPs identisch** in `~/.claude.json` und `~/.config/opencode/opencode.jsonc` (vorher Asymmetrie 7 vs. 11 — `portainer-mcp`/`shelly-mcp` raus, `gitea-mcp`/`n8n-mcp` rein). **Mac mini = n8n-Host, NICHT zentraler MCP-Host** (Performance-Restriktion); zentraler MCP-Host = **DS923** (`nas-bf`, Portainer Endpoint 14). Inventur-Stand `mcp/INVENTORY.md` + Policy `docs/mcp-host-policy.md`. -- 🆕 **MCP-Migration FQDN→IP (2026-08-29, TP35 Schicht 1):** `gitea-mcp` und `n8n-mcp` in `~/.config/opencode/opencode.jsonc` von CF-FQDN auf LAN-IP umgestellt — `gitea-mcp` → `http://10.40.20.167:3000/mcp` (macmini, **ohne** Bearer, intern), `n8n-mcp` → `http://10.40.20.167:3001/mcp` (mit `Bearer {env:N8N_MCP_BEARER}`). **Befund dabei:** der Handoff 1.2 nannte für gitea-mcp die falsche IP `.157:3000` (das ist die Gitea-Web-UI, `400 Invalid CSRF`); der echte gitea-mcp läuft auf `.167:3000` (`serverInfo: gitea-mcp 0.0.10`). `cloudflare`-MCP bleibt auf dem offiziellen FQDN (User-Direktive). -- 🆕 **TP16.3 §2/§3 durchgezogen (2026-08-31, Vor-Ort):** je ein `cloudflared`-Container auf - **ap1/ap2/ap3**, `bf-public` **healthy mit 12 Connectoren** — der Tunnel hat damit zum ersten Mal - überhaupt Connectoren. 🔴 **Zwei Runbook-Fehler dabei aufgedeckt:** die Flash-Rechnung war um Faktor - 2,5 zu optimistisch (entpacktes cloudflared = **69,5 MiB**, nicht 28 MB ⇒ das offizielle Image passt - auf keinen der drei APs; Ersatz ist ein selbst gebautes `FROM scratch`-Image mit 35,3 MiB, Binary-SHA - `e93824f6…` gepinnt), und `/container/envs` heisst `list=`, nicht `name=`. - 🔴 **Offen: Compliance 128/135.** §3.4 verlangt Direkt-Writes für `ip address`/`nat`, die unter - GC-Kontrolle stehen ⇒ **sechs neue `extra`-Drifts** (zwei je AP), inhaltlich richtig, aber nicht im - Soll. Braucht eine Entscheidung (ins Template heben), **kein Re-Baseline**. Details im TP16.3-Abschnitt. -- 🆕 **TP37 angelegt (2026-08-30, User-Okay):** Nautobot soll durchgehende Netzwerkdoku werden, erster - Anwendungsfall die CF-Connectoren. 🔴 **Befund dabei, live gegen die API:** Nautobot kennt heute - **nur die 8 Netzgeräte** (4 aktiv, 4 decommissioned) — `ipam.Service`, `CustomField`, `Relationship`, - `VirtualMachine` und `Tag` sind **alle 0**, installiert ist nur `golden-config`. Kein Host, kein - Container, kein Dienst ist erfasst. **TP37 ist damit zuerst eine Datenmodell-Frage** und liefert das - Fundament, das **TP34** (Caddyfile via GC) und **TP36** (SSoT für diese Datei) ebenfalls brauchen — - Reihenfolge: **TP37 vor TP34**. -- 🆕 **CF-Public-Strang eingedampft (2026-08-30, TP16.3 grosser Schnitt):** **12 Hostnames** aus Ingress - **und** DNS entfernt — `pve`/`portainer`/`pve-mcp`/`ha-mcp`/`ds-bf`/`ccu`/`iobroker`/`z2m25`/`pbs` - plus die drei CF-Access-SSH-Ziele. Ingress: `bf-mm` 6→4, `bf-nas` 6→3, `bf-pve` 9→2. **Öffentlich - bleiben nur `vw`/`hass`/`n8n`/`gitea`** (weiterhin HTTP 200). Intern unverändert über AdGuard→Caddy - (9× HTTP 200). ✅ **Nachtrag 2026-08-30 (F1 durch): `gitea-mcp`/`n8n-mcp` sind jetzt AUCH - geschnitten** — damit **14 Hostnames** gesamt und `bf-mm` **4→2** (nur noch `n8n` + Catch-all). - Möglich wurde das, weil zuvor **12 MCP-Client-Configs** auf LAN-IP migriert wurden (nicht 10 wie - geschätzt: zwei Worktree-Kopien in `bf-network-r6{,.opencode}` kamen dazu). 🔴 **`bf-public` hat weiterhin 0 Connectoren** — er gehört - laut Entscheid auf **ap1/ap2**, dort fehlen aber `container`-Paket **und** `device-mode container=yes` - (physische Bestätigung nötig). Bis dahin stehen `bf-mm`/`bf-nas`/`bf-pve` mit **6 Containern**. - Backups: `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json`. -- 🆕 **TP16.3/TP31 Schritt 5 vorbereitet (2026-08-30):** Runbook für die Vor-Ort-Session geschrieben - ([`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)), CF-Rollback-Snapshots - unter `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` **neu angelegt** (es gab sie vorher nicht). - `docs/tp31-schritt5-runbook.md` ist damit **abgelöst** (read-only-Header): es kannte ap3 nicht, - unterstellte einen Registry-Pull (`fetch: no` auf ap1/ap2) und enthielt eine **falsche** - Firewall-Freigabe-Behauptung. **CF-Stand:** `bf-public` `inactive` / 0 Connectoren; die vier - öffentlichen Namen laufen noch über bf-mm/bf-nas/bf-pve **an Caddy vorbei** direkt aufs Backend. -- 🆕 **TP38 angelegt (2026-08-31):** OOB-Management — je ein untagged-VLAN99-Port auf beiden Switches. - Auslöser: beim Core-Neustart war `crs326-branch` nicht erreichbar, weil sein Mgmt **ausschließlich** - über den Core läuft (`vlan99` dort `tagged=ether1,ether24,bridge`, kein untagged-Member). - 🔴 **Nebenbefund mit eigenem Gewicht: `crs326-branch` hat gar keine `input`-Firewallregeln** — sein - Schutz ist heute allein die Erreichbarkeit über den gehärteten Core. Das ist die Hauptauflage des TPs. -- 🆕 **ROS-Upgrade 7.24.1 auf allen fünf (2026-08-31, User).** In Nautobot nachgezogen: **ap1** stand - als einziges Gerät noch auf `7.24` (die anderen vier waren schon richtig) ⇒ auf `7.24.1` gesetzt. - ⚠️ **Korrektur (2026-08-31, nachträglich):** die dabei gesehene `wifi-security`-Drift auf - `crs326-core` (`sec-main` `ft-over-ds=no` statt `yes`) hatte **nichts mit dem Upgrade zu tun** — sie - ist **TP25 Fund E, Hebel 1**, um 08:09 bewusst gesetzt (Core-Upgrade war erst 14:36) und als - **offene Transaktion `TP25-fund-e-ft-over-ds`** geführt (Scope `wifi-security`, Ablauf 2026-09-03). - **Die 134/135 sind Absicht**, nicht Schaden: grün machen würde den laufenden Versuch unsichtbar - machen, und `gc-transaktion.py` verweigert den Commit ohnehin, solange der Scope driftet. - 🔑 **Lehre:** vor jeder Drift-Bewertung `nautobot/gc-transaktion.py status` lesen — Drift aus einem - laufenden Versuch sieht identisch aus wie ein Versehen, und genau dafür gibt es das Werkzeug. -- 🆕 **TP16.3 §1-Blocker remote erledigt (2026-08-31):** Ingress von `bf-public` auf - `https://10.40.20.40:443` + `originServerName`; Firewall-Freigabe `vlan99 → vlan20:443` **über - Nautobot GC** (Plan `Completed`, Re-Baseline `ingest-core-firewall.py --apply`, danach - **135/135 compliant / 0 Drifts**); `ROUTER_AP3_PASS` in `~/.secrets`. **Offen bleibt nur die - Vor-Ort-Session** (§2/§3: `container.npk`, `device-mode`, Container) und danach Flip + Abbau. -- 🆕 **Globale Verhaltensregeln konsolidiert (2026-08-29, TP35 Schicht 3):** `~/.config/opencode/AGENTS.md` ist jetzt die **SSoT** für globale Verhaltensregeln (Secrets, VLAN-Schema, 5 Lessons, SoC, Übergabe-Mechanik, Mid-Action-Checkpoint, Commit-Hinweis, Backup-Guard). `~/.claude/CLAUDE.md` hält denselben Block als Claude-Code-Spiegelung mit Verweis auf die SSoT. opencode liest AGENTS.md mit Vorrang vor `~/.claude/CLAUDE.md`. - -- **Alle fünf MikroTik laufen auf RouterOS `7.24.1`** (🆕 **Angleichung am 2026-08-31 von Christoph - selbst durchgezogen, TP27 damit erledigt** — ap2 08:56, core 14:36, ap1 14:50, branch 14:52; - ap3 kam bereits so. RouterBOOT jeweils = `upgrade-firmware` = `7.24.1`, live gegengelesen). - Vorher: vier auf 7.24 (08-17), nur ap3 auf 7.24.1. Neu in der Flotte **`ap3`** - (cAP ax `cAPGi-5HaxD2HaxD`, **`10.40.99.13`**, Uplink core `ether21`). - ✅ **Folge für TP16.3:** die versionsgebundene `container.npk` ist damit **eine** Datei für alle drei - APs (alle `arm64`) — die Zwei-Dateien-Regel vom 08-30 war rein versionsbedingt und ist erledigt. - *(Hier stand bis 08-29 „Alle vier MikroTik … 7.24" — durch ap3 und das Upgrade zweifach überholt.)* -- **L3 + WAN-Edge = `crs326-core`** (CRS326-24G-2S+, Technik UG): Routing, DHCP (7 Server, - 54 Reservierungen), DNS, NAT, **NTP-Server für den Standort** und der WG-Hub (TP10, Peers `fm`/`dw`) - laufen hier. Mgmt-SVI **`10.40.99.1`** (vlan99), Gateway `.1` je VLAN. **WAN = `vlan100`** — DHCP-Client - hält die öffentliche Liwest-IP **`90.146.33.177/24`** direkt vom Arris (Bridge-Mode) → kein Double-NAT. - **DNS-Upstream = DoH zum CF-Gateway mit `verify-doh-cert=yes`** (TP23 Stufe 1 ✅ 08-18); - `servers=1.1.1.1` steht nur noch als Bootstrap für den DoH-Hostnamen. -- 🔥 **Die Firewall ist deny-by-default — in BEIDEN Ketten** (TP5 E3/E4 ✅ 08-18). `TP10-seal-lan` ist - `disabled`; Mgmt-Input kommt nur noch aus VLAN10, von HA (`8728`), von Nautobot (`22`) und über - `cf-mesh`. Ein neuer Dienst über eine VLAN-Grenze braucht ab jetzt eine explizite Regel. -- **L2-Branch = `crs326-branch`** (Technik OG): reiner L2-Switch, Mgmt **`10.40.99.2`**, - Uplink `ether1` → core `ether8`. -- **WLAN = CAPsMAN auf dem Core** (TP28 ✅ 08-17, GC-Modell nachgezogen 08-18): ap1 **`10.40.99.11`** - (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; - Kanäle, SSIDs, Security und Steering kommen vom Manager. 802.11k/v aktiv; **802.11r (FT) aus** — - `ft=no` auf allen SSIDs (sec-main seit 2026-09-04, User-Entscheid). - 🆕 **Dritter AP `ap3` (cAP ax) ✅ LIVE seit 2026-08-30** (TP28 dafür wieder geöffnet): - `10.40.99.13`, Uplink `ether1` → **core `ether21`** (Trunk), reiner CAP, **RouterOS 7.24.1** - (🆕 **seit 2026-08-31 die ganze Flotte** — die frühere Versionsdifferenz ist aufgelöst). Kanäle - **`5180/ax/Ce/I`** + **`2437/ax`** (ch6) nach dem TP25-Plan vom 2026-08-24, am Gerät nachgemessen. - 🔴 **ap1 ist funkstill** — es steht bis zu seinem Umzug ans Sideboard direkt neben ap3 und würde - 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 - `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 - der App auf dem Weg nach VLAN40 (Messung: **0 Pakete `via eth1` gegen 308 `via eth0`** in 20 min, - **0×** `Received SSDP Unicast` im ganzen Journal seit 08-18). Der Rückweg läuft jetzt **nativ geroutet** - über die längst offene Regel `TP5: MEDIA->MAIN`; nachgemessen kommen `HTTP/1.1 200 OK` von - `10.40.40.193/.195` an beiden MacBook-Beinen an. Unit liegt im Repo: **`mcast-relay/multicast-relay.service`**, - Details als Gotcha in `CLAUDE.md`. ⚠️ **Der Fall hatte zwei unabhängige Ursachen** — nach der - Relay-Reparatur suchte die **App** immer noch nicht (macOS-Local-Network-Gate stand auf „an" und - griff nicht; Fix war Schalter aus/ein + Neustart, User-bestätigt 08-25). **Beide sind erledigt.** -- **Reverse Proxy = `caddy-bf1`** (LXC 130 auf pve, **`10.40.20.40`**, TP31 ✅ Schritt 1–3 seit - 2026-08-25, **`hass` 400 behoben 2026-08-26, Split-Horizon seit 2026-08-26**): Caddy 2.11.4, - **16 Sites** (4 public, 12 intern mit `.bf.`-Suffix), Zertifikate ausschliesslich - per **DNS-01** mit einem eng geschnittenen `Zone:DNS:Edit`-Token (`CF_DNS_API_TOKEN`, **nicht** das - account-weite). Abnahme über den Host-Header von einem zweiten Host: 11× 200, `nautobot` 302. - Offen: **Schritt 5–6** (Connector für `bf-public`, CNAME-Flip, Abbau der sechs Container) — - **Runbook steht: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)**, - Vor-Ort-Session an ap1/ap2/ap3. ✅ **Die drei §1-Blocker sind am 2026-08-31 remote erledigt** — - **(a)** Ingress auf `https://10.40.20.40:443` + `originServerName` je Regel umgestellt (die alte - `:80`-Zeile hätte über CF eine Redirect-Schleife gebaut, weil Caddy dort 308 auf https antwortet); - **(b)** Freigabe `vlan99 → vlan20:443` **über Nautobot GC** deployt (`ip-firewall-filter` steht unter - Drift-Kontrolle, TP26 ⇒ kein Direkt-Write), Position vor `TP5: log-only` verifiziert, **135/135 - compliant, 0 Drifts**; **(c)** `ROUTER_AP3_PASS` in `~/.secrets` nachgetragen (REST-Auth 200). - ⇒ **Vor Ort startet die Arbeit bei §2/§3** (Pakete, `device-mode`, Container). -- **DNS/Resolver = AdGuard-Paar `agh-bf1`/`agh-bf2`** (LXC 110/111 auf pve, **`10.40.20.36`/`.37`**, - TP23 Stufe 2 + TP31 Schritt 4 ✅ seit 2026-08-26): Upstream DoH zum CF-Gateway, conditional - `[/bf.emsenhuber.net/] → 10.40.20.1`, 47 Rewrites (16 Service → Caddy `.20.40` + 10 Host-Namen - mit `.bf.`-Suffix + alte suffixlose parallel). DHCP liefert `.36`/`.37` als DNS in allen 7 VLANs - (`domain=emsenhuber.net`), Firewall forward UDP+TCP/53 auf beide. Der Core-Resolver (`10.40.X.1`) - bleibt als Bootstrap unverändert (DoH, `allow-remote-requests=yes`, `mdns-repeat-ifaces`). - 🆕 **TP23 Stufe 4+5 ✅ 2026-09-04:** `clients.persistent` = 7 VLAN-CIDR-Gruppen + `dns-bypass-exempt` - (Policy-Matrix, INFRA/MEDIA/MGMT `filtering=aus`), API-User `automation`, Sync-Tool + redigierte - Repo-SSoT unter `adguard/`. HA-Self-Service (Button → JIT-Domain-Freigabe, nativer Timer-Revert) live. - Offen nur **Stufe 6** (Bypass-Sperre, TP5). Werkzeuge/Doku: `adguard/{agh-sync,agh-exception}.py`, - `adguard/{README,ha-self-service}.md`. -- **Ausgebaut / gelöscht:** ALLNET-`sw1` und Omada-`sw2` sind **physisch weg** (2026-08-08 bzw. 2026-07-23, - in Nautobot *Decommissioned*); Omada-Controller + `omada-mcp` gelöscht (2026-07-16); FritzBox 6660 raus - seit 2026-07-01 (Liwest-Tausch auf Arris). **VLAN1 ist leer** (`/ip arp` auf `bridge` = 0 Einträge, - 08-18) — TP19 ist geschlossen; `cudy1` hängt stromlos an core `ether11`, physischer Ausbau ist hands-on. -- 🆕 **WLAN-Logging persistent seit 2026-08-30 (TP25 Fund E):** `wireless` + `caps` gehen vom Core - in den TP5-Kollektor (`10.40.20.18:514`); `/system logging` steht unter der **27. ComplianceRule** - `system-logging`. Vorher nur Ringpuffer → ein seit Wochen gemeldeter iPhone-Abbruch war rückwirkend - nicht diagnostizierbar. -- ⚖️ **TP25 Fund E — Hebel 1 ZURÜCKGEBAUT, Entscheidung faellt am 2026-09-02 (Stand 09-01 09:2x):** - Zwei Sessions haben denselben Fall bewertet und kamen zu entgegengesetzten Schluessen. **Aufgeloest - zugunsten der Kollektor-Messung:** - - Die Nacht-Session las `get_log … sinceMinutes=1440` → `total 0` und schloss auf „geheilt". Das ist - der **fluechtige Ringpuffer des Routers**: live nachgemessen **1000 Zeilen, aeltester Eintrag - 01.09. 00:38:58** — keine 9 h Tiefe. Der Core-Reboot 14:36 hatte ihn geleert, der 31.08. Vormittag - war darin **nicht sichtbar**. 🔴 Genau die Falle, wegen der am 30.08. der persistente Kollektor - eingerichtet wurde. - - Im **Kollektor** stehen fuer 08:09–14:36 (Hebel 1 **allein**, vor dem Upgrade) **124× - `reauthenticating`**, Stundenverteilung 09h=1 · 11h=2 · 12h=20 · 13h=31 · 14h=70, **alle vom - iPhone allein** (= Client-Befund nach der eigenen Merkregel „viele MACs = Infrastruktur"), dazu - Christophs **zwei erlebte Ausfaelle** um 13:16 und 14:23. Zwischen 09:00 und 14:36 gab es - **keinen Reboot**, der sie erklaeren koennte. - - ⇒ Hebel 1 hat im upgrade-freien Fenster **nichts** bewirkt; der Einbruch faellt exakt auf 7.24.1. - **Deshalb am 2026-09-01 zurueckgebaut** (`ft-over-ds=yes`, Provisionierung nachgezogen, frische - DHCP-Lease abgenommen, Transaktion **`zurueckgebaut`**, **Compliance 140/140**). Der Rueckbau **ist - das Experiment**: bleibt es ruhig, war 7.24.1 die Ursache; kehrt das Haemmern zurueck, hatte die - Nacht-Session recht und Hebel 1 wird als neue Transaktion erneut gezogen. **Bis zur Messung am - 2026-09-02 frueh committet niemand.** Handoff: - [`handoff-20260901-fund-e-hebel1-zurueckgebaut.md`](handoff-20260901-fund-e-hebel1-zurueckgebaut.md). -- 🔴 **ABER: neuer Teilbefund am 23:02 (2026-08-31) — `ap2` unterperformt, NICHT FT.** Christoph am - Schreibtisch, 5G, kein Telefonat. Log: iPhone `F2:62:4C:E1:AF:09` ping-pongt im Sekundentakt - `cap-wifi5 ↔ cap-wifi6` = **beide Radios von ap2** (5G 5220 ↔ 2,4G ch11/2462, Band-Steering-Thrash - auf demselben AP), `disconnected, not responding` um 22:37:40 und 23:01:00. Feldstärke **−75…−88 dBm - — bei laut Christoph 2 m Abstand** (ap2 auf 2,3 m, gleicher Raum). Bei 2 m wären −35…−45 normal. - Aktuell **0 Clients auf ap2**, alle 29 auf ap3. ⇒ kein Coverage-/FT-Problem, sondern **ap2s Funk - selbst** (RF/Antenne/HW) und/oder die **DTU-Störung auf ap2/ch11** (Transaktion `TP17.1-dtu-pin-ap2`, - −59 dBm auf ap2s 2,4-Radio). Als eigener 🔬-Unterpunkt geführt, In-situ zu messen. Handoff: - [`handoff-20260901-tp25-ap2-verdacht.md`](handoff-20260901-tp25-ap2-verdacht.md). -- 🔴 **TP25 Fund E — Hebel 1 ist GESCHEITERT (Stand 2026-08-31 nachmittags):** `sec-main` steht am - Core auf **`ft=yes ft-over-ds=no`** (seit 08:09), aber das Symptom blieb: **124× `reauthenticating` - nach dem Eingriff** und **zwei von Christoph erlebte Ausfälle** — 13:16 (Videocall bricht ab, - 7 min ohne WLAN) und 14:23 (5,5 min, nur durch WLAN-Toggle beendet). Stundenvergleich auf - gleicher Firmware: **07:00–08:00 = 79 · 14:00–15:00 = 70**. Erfolgskriterium war „~0" ⇒ nicht - eingetreten. Gewertet wird bewusst nur **08:09–14:36** (danach kommt das TP27-Upgrade als zweite - Variable dazu; Christoph-Entscheid: „Vormittag als Ergebnis werten"). - ⇒ Transaktion **`TP25-fund-e-ft-over-ds` bleibt OFFEN** (Ablauf **2026-09-03**), **Compliance - weiter bewusst 134/135**; Rollback oder Commit ist eine Entscheidung, kein Aufräumen. - Geräte-Backup `pre-tp25-funde-ftoverds.backup` liegt auf dem Core. - **Nächste Entscheidung:** Hebel 2 (`ft=no`) **oder** `authentication-types=wpa2-psk,wpa3-psk` — - `sec-main` ist auch die einzige SSID ohne `wpa2-psk`, und `association rejected, no cached PMK` - (157× am 31.08., 0× am 30.08.) ist eine SAE/PMKSA-Meldung. **Vorher prüfen, ob 7.24.1 auf dem - Manager das Verhalten schon verändert hat** — zwischen 14:37 und 15:12 stand nur **1** - `reauthenticating` im Log, keines vom iPhone (35 min = noch keine Aussage). - Details: [`handoff-20260831-tp27-upgrade-und-hebel1-befund.md`](handoff-20260831-tp27-upgrade-und-hebel1-befund.md). -- ⚠️ **Messfalle, die diese Session fast erwischt hat:** die 13 CAP-`joined`/`activity timeout` vom - 31.08. sahen nach CAPWAP-Instabilität aus — es waren die **Reboots des TP27-Upgrades**. Und die - **`cap-wifiN`-Karte hat sich an einem Tag zweimal verschoben** (08:09, 14:36), womit die im - Vorgänger-Handoff festgehaltene Tabelle nach sechs Stunden falsch war. Interface-Namen in - Logzeilen sind ohne zeitnahen Karten-Snapshot **nicht zuschreibbar**. -- **Kontrolle / Governance:** MikroMCP für Reads/Debug (alle 5 Geräte in der Registry), - **Config-Änderungen ausschließlich über Nautobot Golden Config** (TP21/TP22 — Deploy-Pipeline scharf, - **135/135 Compliance-Checks grün, 0 Drifts** bei **27 Rules × 5 Geräten** (Stand 2026-08-30; - **seit 2026-08-31 vorübergehend 134/135 — der eine Drift ist die offene Transaktion - `TP25-fund-e-ft-over-ds`, s. oben, kein Fehler**) — live gegen die API - geprüft am **2026-08-23**; zuletzt dazugekommen: **`wifi-access-list`** (08-22), **`queue`** - (08-23, TP6) und **`ip-firewall-mangle`** (08-23, TP6.1), s. TP22-Nachtrag im Archiv). - *(Hier stand bis zum 08-23 „100/100 bei 25 Rules" — eine Zählung, die TP6.1 am selben Tag überholt - hat. Die Merkregel gilt: der Ist-Stand-Block wird bei jeder Änderung mitgezogen, nicht nur der TP.)* - -
📜 Historische Fassung (Stand bis 2026-08-08, Topologie vor dem Core-Cutover — bewusst erhalten, nicht als Ist lesen) - -> ALLNET-Core `.11` (FW2.2.1, VLANs 10/20/30/40/50/90/99/100; GE1/GE2/GE8 Trunk; **TP1 ✅ 2026-06-15**). Omada-Switch `.12` (adopted, **TP2 ✅ 2026-06-15**; Trunk-ALL auf P1+P2, Access-Profile auf P3-P8; Profile noch PVID=1 bis Omada-PVID-Fix). -> Omada-Controller **`.175`** (DS923/macvlan, mbentley 6.2, 🟢 live). omada-mcp **`.176`** (pve-LXC, StreamableHTTP, 🟢 live, 84 read-Tools; Controller-API v2 intern via christoph@emsenhuber.com). -> 2× hAP ax² (7.23.1): ap1 `.1` (ALLNET P1) — **TP4 ✅ 2026-07-01: Router + WAN-Edge** (VLAN-Subinterfaces, DHCP, DNS, NAT, Public-Edge). ap2 `.2` (Omada P2 — Trunk). -> **Edge-Cutover ✅ 2026-07-01:** FB6660 physisch entfernt (Liwest-Kabelmodemtausch, kein Zurück), Arris-Kabelmodem im Bridge-Mode; WAN = `sw1 gi16 Access VLAN100 → gi15 Trunk → ap1 ether2 → vlan100 auf bridge` (**NICHT** ether1); ap1 hält Public-IP direkt (Liwest, kein CGNAT), Double-NAT weg. Telefonie war auf der FB und wird nicht weitergeführt. Vorarbeit: TP10-Kern durch (2026-06-27, WG-Hub live) → Bridge-Gate vorher gelöst. Runbook: `routeros/wan-cutover-runbook.md`. -> Kontrolle: MikroMCP (MikroTik) live · ALLNET = Klickanleitungen · Omada → Controller + omada-mcp 🟢. - -
- -**Nächster Schritt (Stand 2026-08-30): Vor-Ort-Session ap1/ap2** — sie ist der einzige Weg, den -Public-Strang auf `bf-public` zu heben und die drei Alt-Tunnel samt sechs Containern abzubauen -(TP16.3 Follow-up F2). ✅ **F1 ist am 2026-08-30 durch** (12 Client-Configs auf LAN-IP, danach -`gitea-mcp`/`n8n-mcp` aus Ingress+DNS geschnitten) — davor unabhängig machbar bleibt nur noch die -**Announcement-Doku (TP35 Schicht 4)**, die den anderen Projekten die neuen MCP-URLs mitteilt. - -**Vorherige Fassung (Stand 2026-08-28): drei Folge-Sessions parallel** (siehe `docs/handoff-20260828-mcp-bestand-und-performance.md`): -> - ✅ **Session A** — `/projektstatus`-Skill global für opencode (TP35 Schicht 6). Funktionsbeweis erbracht 2026-08-28. -> - 🔜 **Session B** — shelly-mcp-Container auf DS923 (`10.40.20.10`) deployen (live, ssh). Repo-Materialien committed (`mcp/shelly/{Dockerfile,BUILD.md,requirements.txt,.gitignore}`); `.env` und Tunnel-Secret lokal — kein MCP-Client (User-Direktive). -> - 🔜 **Session C** — `caddy/cf-sync.py`-Drift-Check + Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5). +> 🧭 **Ist-Stand, kritischer Pfad und Quick-Index leben ab jetzt als Gitea-Issues**, nicht mehr in +> dieser Datei: **[admin/bf-infra · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues)**. +> **TP = Issue · Status = Label `status:*`** (todo/wip/paused/idea/concept/done/dropped) · +> **Gruppierung = Label `thema:*`**. Filtern über die Label-Chips der Issue-Liste, z.B. +> [nur offene](https://gitea.emsenhuber.net/admin/bf-infra/issues?type=issue&state=open). > -> 🆕 **Dazu seit 2026-08-30: TP16.3/TP31 Schritt 5 = Vor-Ort-Session** an ap1/ap2/**ap3** -> ([Runbook](tp16.3-bf-public-ap-connector-runbook.md)). **Reihenfolge beachten:** die drei Blocker in -> §1 sind **remote** zu erledigen, *bevor* jemand in den Keller geht — sonst steht man vor einem -> Connector, der `healthy` meldet, während die Seite eine Redirect-Schleife dreht. +> **Diese Datei führt nur noch die per-TP-Prosa** — die `### TPnn`-Abschnitte unten; Abgeschlossenes im +> **[Archiv](teilprojekte-archiv.md)**. **Live-Gerätedaten** (Compliance-Zahl, RouterOS-Version …) kommen +> aus **Nautobot**, nicht von hier. Sync-Werkzeug Datei/Archiv → Gitea (idempotent, Dry-Run default): +> `bin/gitea-tp-sync.py`. > -> 🆕 **Strukturelle Folge = TP36:** SSoT für `teilprojekte.md`-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. **Merkregel im Lessons-Block.** +> *(TP36-Cutover 2026-09-07: die frühere Quick-Index-Tabelle und der chronologische „Ist-Stand (Kurz)"-Block +> sind aus dieser Datei entfernt, damit TP-Branches nicht mehr auf der Steuerungs-Doku kollidieren — Status- +> Updates gehen nach Gitea statt in diese Datei. Die alte Ist-Stand-Historie steht in der Git-Historie.)* -> 🆕 **Neu am 2026-08-24/25:** [TP31](#tp31) löst **TP16** und **TP16.2** ab und dampft cloudflared -> von **sechs Containern auf einen** ein; [TP32](#tp32) gibt die Nummern- und Adress-Systematik vor -> (und hat nebenbei die **falsche Startreihenfolge auf pve repariert** — HA startete vor seiner -> Datenbank). **TP16 ✅, TP25 ✅ und TP16.3 (Pilot) ✅** sind am 24./25. durchgezogen worden. - -**Vorherige Fassung (Stand 2026-08-23): Christoph entscheidet — der kritische Pfad ist LEER.** -> 🎯 **Das Projekt ist in seinem Kern fertig.** deny-by-default steht (TP5), Golden Config steht und -> meldet **104/104 compliant bei 0 Drifts** (live geprüft 2026-08-23), WLAN steht über CAPsMAN auf dem -> Core (TP28), VLAN1 ist leer (TP19) — und **TP6 ist beantwortet statt offengelassen**: CAKE gebaut, -> gemessen, wieder ausgebaut. **Nichts blockiert mehr etwas.** -> 🟢 **Was zuletzt wirklich etwas gebracht hat, war TP6.1** (2026-08-23): nicht Shapen, sondern -> **Klassentrennung** — NAS-Bulk `priority=8` gegen interaktiv `priority=1` drückt dieselbe Strecke -> unter derselben Sättigung von **40,8 auf 11,0 ms (−73 %)**. Die zuerst notierte DOCSIS-Erklärung aus -> TP6 ist damit **widerlegt**; der Fehler lag in der Messmethode (Last und Messung vom selben Host). -> 📋 **Offen und ohne Blockadewirkung:** **TP25** (Fund F = 5:05-Takt bei IoT, plus die Entscheidung -> „5 GHz auf 20 MHz") · **TP16** (CF-Tunnel-HA degradiert, vier tote Alt-Tunnel) · **TP16.2/16.3** · -> **TP23 Stufe 2** · **TP24** (DC-USV) · **TP13/TP14.4** (beide mit fertigem Runbook) · -> ❌ **TP17.1 VERWORFEN 2026-08-23** (Zigbee-Weg bewiesen, Ziel damit nicht erreichbar — das Gerät -> bleibt WLAN-Client; `s9` zurückgebaut) · ✅ **TP17.3 ERLEDIGT** (Ursache **`eco_mode`** bewiesen; -> `s23` + `s26` repariert; **`eco_mode` flottenweit aus**, 22/22 außer `s24`) · -> ➡️ **TP17.2 ist am 2026-08-24 in TP30 aufgegangen** (Shelly-Gerätepasswort = Erstvergabe eines -> Credentials; TP30 ist das Sammelbecken dafür) · -> 🔑 **Alle Passwort-/Token-Rotationen sind seit 2026-08-24 TP30** (Sammel-TP) — **bewusst -> gebündelt gegen Projektende**, User-Entscheid. Damit ist die ältere Einreihung „hinter TP17.1" -> gegenstandslos (TP17.1 ist verworfen). **Etappe 1 — die Service-Accounts — ist durch;** -> das `admin`-Passwort selbst ist **nicht rotiert** und bleibt es bis dahin bewusst. -> 📌 **Einstieg für die nächste Session: -> [`handoff-20260825-agh-dns-cutover.md`](handoff-20260825-agh-dns-cutover.md)** — AdGuard als -> Resolver + Split-Horizon (TP23 Stufe 2 ∥ TP31 Schritt 4). **Bewusst eigene Session:** der Schritt -> verschiebt die DNS-Auflösung des ganzen Standorts in einen Container. -> 📜 Älter: [`handoff-20260824-session.md`](handoff-20260824-session.md) -> — ⚠️ dessen Empfehlung „Start bei der Passwort-Rotation Etappe 2" ist durch den TP30-Entscheid -> **überholt**; der Rest der Übergabe gilt. Ältere Sitzungsvorschläge: -> [`handoff-20260822-naechste-schritte.md`](handoff-20260822-naechste-schritte.md). -> ✅ **`crs326-core` ist transaktionsfrei** (seit 2026-08-22, geprüft): ein gemeldeter Drift ist ab -> jetzt **echt**, nicht beabsichtigt — `python3 nautobot/gc-transaktion.py status`. - -
📜 Vorherige Fassungen dieses Absatzes (2026-07-22 → 2026-08-21, bewusst erhalten) - -> *Fassung 2026-08-21 abends (erledigt — TP25s Dead Spot ist am 08-22 repariert, TP6 am 08-23 geschlossen):* -> **Nächster Schritt: TP25 — Steering-Test in den Schlafräumen.** -> 📋 **Einstieg: [`handoff-tp25-session-20260821.md`](handoff-tp25-session-20260821.md)** — dort steht -> der verifizierte Ist-Stand, die Messfallen und der nächste Test. -> ✅ **`crs326-core` ist transaktionsfrei (2026-08-22).** `TEST-ether3-trunk-ap1-sideboard` und -> `TEST-infra-ssid-matter-pairing` sind beide zurückgerollt und geschlossen; ap1 hängt wieder auf -> `ether15`, `ether3` ist wieder untagged VLAN40. Ein gemeldeter Drift ist ab jetzt **echt**, nicht -> beabsichtigt — `python3 nautobot/gc-transaktion.py status`. -> 🆕 **TP29** (Anwesenheitserkennung fürs Energiesparen) ist als Idee angelegt, nicht auf dem Pfad. -> 📌 **Die TP6-Prämisse ist am 2026-08-20/21 gemessen** (Runbook §5): formal Fall B (+28…+47 ms unter -> Dauer-Upload), aber im Videocall **nicht wahrnehmbar** — kein einziger Problem-Klick über sechs -> Lastphasen. Laut war stattdessen der Funk: **Sticky Client + Band-Ping-Pong, zwölf Wechsel in neun -> Minuten**, je Wechsel bis zu 4 s ohne Durchsatz → -> [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md). Deshalb steht jetzt -> **TP25 vor TP6**; TP6 bleibt offen und begründet, aber nachrangig. -> -> *Vorherige Fassung (Stand 2026-08-18, erledigt):* -TP6 wird hier seit Monaten als „DAS Ziel" geführt, aber `docs/call-quality-diagnosis.md` hat den -Bufferbloat 2026-06-16 bereits gemessen (**A/A+**, „CAKE wäre marginal, nicht deployt"). Die Messung -stammt aus der FritzBox-Ära, der Edge ist seither getauscht — also **einmal gegen den heutigen Edge -nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Nacht auf den 18. abgeschlossen worden (Etappe 3 + 4: deny-by-default in `forward` **und** `input`, MGMT isoliert, `seal-lan` abgeschaltet), TP23 Stufe 1 (DoH auf dem Core) ebenfalls. Nebenher offen, blockiert nichts: **TP25** (Signal-Boden + Re-Walk), **TP28-Rest** (die fünf `wifi-*`-Features vom AP-Scope auf den Core umhängen — die einzigen verbliebenen Compliance-Drifts), **TP7.1** (SSDP-Relay für die macOS-Sonos-App), **TP16.3**, **TP24**. -> *Vorherige Fassung (Stand 2026-08-14, überholt): „TP5 — Deny-by-default am Ende von `forward`." Alles Davorliegende ist durch:* CRS326-Bring-up + L3-Umzug ap1→`crs326-core` (TP15/TP21.1 ✅ 2026-08-08), WLAN-Neuprovisionierung ohne CAPsMAN (TP3 ✅ 2026-08-08), Nautobot-Plattform + Fleet-Adoption + Deploy-Pipeline (TP21/TP22 ✅ 2026-08-13). Danach **TP6 (CAKE) = das Projektziel**. Kleine Follow-ups nebenher: DoH-Cloudflare auf core, DC-USV (**TP24**, seit 2026-08-16 eigener TP). ⚪ **Omada-PVID-Fix ist obsolet** (User-Entscheid 2026-07-22 — Omada spielt seit der TP15-Korrektur keine Rolle mehr im Netz, nie mehr aufgreifen). -> *Vorherige Fassung (Stand 2026-07-22, erledigt): „TP15 CRS326-Bring-up als neuer L3-Core, danach WLAN-Neuprovisionierung ohne CAPsMAN; TP5 folgt nach TP21/CRS326-Adoption."* - -
+## Governance & genehmigte Ausnahmen (stehende Regeln — kein Status) > 🎯 **Ziel: sauberes System „from scratch", keine Leichen/Altlasten.** Migrations-Session-Endstand = **VLAN1 + altes cudy-WLAN komplett weg** (VLAN1 nur noch Mgmt, `defconf`-Client-DHCP zurückgezogen, cudy-Uplinks aufs Ziel-Client-VLAN getaggt oder cudy abgebaut). **Unterpunkte/Aufräum-Liste bei Migration:** (a) die Interim-Härtung ap2 `wifi1/wifi2 frame-types=admit-only-vlan-tagged` (TP3.1, 2026-07-06) bleibt gültig — verschwindet automatisch mit VLAN1; (b) ~~`st-main rrm/wnm` = reines Roaming-Provisorium~~ → **kein Provisorium mehr (2026-08-13, TP3.3): Steering ist fester Bestandteil des Golden-Config-Templates + ComplianceFeature `wifi-steering`** — es war die Neuprovisionierung, die es stillschweigend verloren hatte; (c) ~~**cudy1 = verbleibender aktiver VLAN1-Leak**~~ → **ERLEDIGT (User-bestätigt + verifiziert 2026-08-14): cudy1 ist offline.** `core ether11` trägt **kein Link** (kein `R`-Flag), Nautobot-Status *Offline* (TP22 Schritt 5). Damit existiert kein aktiver VLAN1-Leak mehr; der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` ist Historie. Der formale Teardown (Gerät ausbauen, Doku/Inventar) bleibt TP19. > ⚠️ **HW-Constraint hAP ax²:** 1 GB RAM, aber nur **128 MB Flash + KEIN USB** → **Container auf dem Router NICHT praktikabel**. @@ -522,44 +67,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na > ~~**TP17.2** ↔ TP17~~ *(TP17.2 ist am 2026-08-24 in **TP30** aufgegangen und kein eigenes TP mehr)* > und **TP20.1** ↔ TP20). > -> ### 📊 Quick-Index (nur dieser Block ist für den Statusbaum nˆtig — 1 Read) -> -> | # | Status | Kurztitel | Abschluss | -> |---|---|---|---| -> | TP5.1 | ⏸️ | DNS-Visibility Airfryer | — | -> | TP8 | ⏸️ | Identity / 802.1X | — | -> | TP11 | ⏸️ | Monitoring | — | -> | TP12 | ⏸️ | IPv6 (3 Standorte) | — | -> | TP12.1 | 🔜 | IPv6 ULA + v6-Firewall | — | -> | TP13 | 🔜 | Botschafts-WLANs | — | -> | TP14.4 | ✅ | CF-Access-Token — **in TP41 T4 aufgegangen** (`claude-mcp`→`zt-svc`, `.env` geschrieben) | 2026-09-01 | -> | TP16 | ✅ | CF-Tunnel HA wieder voll | 2026-08-24 | -> | TP16.2 | 🔜 | Härtung bypass-Dienste | — | -> | TP16.3 | ✅ | cloudflared-Reduktion — Flip + Abbau durch; nur noch `bf-public`+`fm`, 7 Container weg | 2026-09-01 | -> | TP17.1 | ❌ | Shelly Gen4 → Zigbee | 2026-08-23 | -> | TP17.3 | ✅ | Shelly-Regression behoben | 2026-08-23 | -> | TP18 | 🔜 | Garage-Objektspeicher | — | -> | TP20.1 | 🔜 | Vaultwarden → Platform-PG | — | -> | TP23 | ⏳ | DNS-Policy (Stufe 1–5 ✅ 09-04) — offen nur Stufe 6 (Bypass-Sperre, in TP5) | 2026-09-04 | -> | TP24 | 🔜 | DC-USV | — | -> | TP25 | ⏳ | WLAN-Stabilisierung (Kern ✅) — **Fund E ✅ geschlossen 09-02: 7.24.1 war die Ursache, nicht `ft-over-ds` (4/8,6 h, 0 Ausfälle)**; **`ft=no` auf `sec-main` als Dauerzustand ✅ 09-04**; **Konsole auf N Clients + Pin-UI ✅ 09-04**; offen nur ap2 unterperformt (2 m/−80, In-situ) | 2026-09-04 | -> | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 | -> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — | -> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — | -> | TP31 | ✅ | Reverse Proxy Caddy — 1–4 ✅ (08-25/26), **5+6 via TP16.3 (09-01)**; Abnahme-Tool `cf-sync.py` grün | 2026-09-01 | -> | TP32 | 🟢 | Nummern-Systematik (Konzept) | — | -> | TP33 | 💡 | Sonos fern (Idee, zurückgestellt) | — | -> | TP34 | 🔜 | Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) | — | -> | TP35 | ⏳ | MCP-Schichtung + 3-Schicht-CLAUDE.md + Backup-Guard (Schicht 1+3+6 ✅) | — | -> | TP36 | 🔜 | SSoT via Nautobot für teilprojekte.md-Live-Daten + Datei-Aufteilung | — | -> | TP37 | 🔜 | Nautobot als Netzwerkdoku: Hosts/Container/Dienste + CF-Connectoren (Okay 08-30) | — | -> | TP38 | 🔜 | OOB-Mgmt: untagged-VLAN99-Port je Switch | — | -> | TP40 | 🔜 | HA-Recorder-Retention: 41 GB `states` beschneiden (Okay 09-01) | — | -> | TP41 | ⏸️ | CF-Access-Reparatur **zurückgestellt 2026-09-01** (User): **T2/T3/T4/T5 ✅** (GitHub+Google-Login live); **Facebook abgesagt**; **Geli CF-seitig fertig** (zt-humans+geli-Policy) → offen nur noch WARP-Client auf Gelis Gerät, wartet auf User-Info | — | -> | TP42 | ✅ | Namens-Vereinheitlichung: ein interner Suffix **`bf.emsenhuber.net`** — DHCP-domain via GC geflippt (140/140), `.16` abgebaut (`.17`=FM bleibt); Nautobot-Doku-Teil an TP37 | 2026-09-02 | -> | TP43 | 🔜 | dw-Standort-DNS-Uplink: eigener Resolver mit site-Overrides (`420.teilzeitladen.xyz`→BF-Caddy/WG), WG-Disconnect bedacht; Geli sofort per WARP | — | -> | TP44 | ⏳ | CF-Estate als Doku-SSoT in Nautobot — **Etappe 1 ✅ live** (`nautobot.cloud`: Account 1/ResType 6/Network 4/Service 14/Assign 3); offen Etappe 2 (CF-API→Nautobot-Sync) + IPAM-Prefixe | — | -> | — | — | **35 archivierte TPs** in `teilprojekte-archiv.md` (inkl. TP39 ✅ 2026-09-01) | — | +> 📊 **Index/Statustabelle → [Gitea-Board](https://gitea.emsenhuber.net/admin/bf-infra/issues)** (siehe Kopf dieser Datei). Hier unten folgt nur noch die per-TP-Prosa. ### TP5.1 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) ▸ vorgemerkt - **Kontext (2026-07-08/09):** Airfryer „airschreier" (Philips/Versuni, ESP, `10.40.30.180`) bekam per DHCP-Option 6 `dns 1.1.1.1` als Override, weil das OTA-Update über den ap1-DoH-Gateway-Pfad nicht durchging (DoH-Latenz beim primitiven ESP-Stack — **kein** Content-Block, via Gateway-Logs bewiesen, s. `memory/ref_esp_ota_doh_not_block`). Nebenwirkung: public **1.1.1.1 = anonymer Consumer-Resolver ≠ accountgebundener Gateway-DoH** (`px35ynlol3.cloudflare-gateway.com`) → **DNS-Sicht auf das Gerät weg**, Gerät umgeht zudem die Filterung. @@ -2874,18 +2382,17 @@ kann in Ruhe nachziehen. --- -### TP36 — SSoT via Nautobot für `teilprojekte.md`-Live-Daten + Datei-Aufteilung ▸ 🔜 ANGELEGT 2026-08-28, Scope-Ausarbeitung eigene Session -- **Auslöser:** Skill-Run 2026-08-28 (`docs/handoff-20260828-session-a-projektstatus.md`) lieferte „Nächster Schritt (2026-08-26): TP31 Schritt 5" als Output, obwohl seit dem 26. drei Folge-Sessions parallel offen waren. **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.** -- **Befund:** Drei Stellen pro Edit berücksichtigen müssen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere. Wer eine ändert, **muss** die anderen mitlesen. Die Datei hat aktuell 1940 Zeilen, der Ist-Stand-Block steht bei Z. 28, das Archiv-Cover bei Z. 1771. Strukturelles Risiko, kein Einzelfall. -- **Scope (Skizze, eigene Session):** - 1. **Nautobot Custom Fields für Live-Daten** — RouterOS-Version, Compliance-Zahl (104/104, 26 Rules × 4 Geräte), DHCP-Server-Anzahl, MGMT-IPs, DNS-Upstream-Modus. Script `nautobot/gc-snapshot.py` rendert `docs/teilprojekte.md` (oder einen schmaleren Ersatz) als **read-only Snapshot**. - 2. **Datei-Aufteilung** — `docs/teilprojekte-index.md` (1 Seite: nur aktive TPs, Titel + Status + Datum), `docs/teilprojekte-aktuell.md` (Live-Snapshot aus Nautobot), `docs/teilprojekte-archiv.md` (bleibt Volltext). `/projektstatus`-Skill liest Index + Archiv, nicht die Snapshot-Datei. - 3. **Archiv-Anzeige im Quick-Index** — die heute 34 Namen stehen heute in *einer* Zeile am Ende. Sichtbarkeit = „Geschichte zählt". Quick-Index-Block bekommt eine zweite Tabelle mit „abgeschlossen seit …" und Link aufs Archiv. - 4. **Migrationspfad** — kompatibel mit bestehendem Workflow (`/projektstatus`-Skill konsumieren, niemand außer mir editiert Live-Markdown). Testphase = beide Dateien parallel führen. -- **Abgrenzung:** TP36 ist Querschnitt, nicht Kern-BF. Eigene Session, eigener Handoff. Nicht heute. -- 🔴 **Klarstellung 2026-09-06:** **unabhängig vom TP37-Substrat** — die Live-Daten-CustomFields (Punkt 1) hängen an den **4 bestehenden `dcim.Device`**, nicht an neuen VM/Service-Objekten → TP36 kann **parallel/vorgezogen** laufen. Datenmodell: native `extras.CustomField` (Compliance-Zahl kommt aus GC selbst). ⚠️ **Namensfalle bereinigt:** das Skript `nautobot/tp36-cf-estate-seed.py` gehörte **nie** zu TP36 — es ist die TP44-CF-Estate-Befüllung und heißt seit 2026-09-06 `tp44-cf-estate-seed.py`. TP36 hat **noch kein** Skript (`gc-snapshot.py` ist zu bauen). -- **Verzahnt mit:** TP22 (Nautobot-GC als Vorbild), TP35 (geht Hand in Hand — beide sind „SSoT + Konsolidierung"), `bf-infra/CLAUDE.md`-Lessons. -- **Referenzen:** Lessons-Block-Eintrag „Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität" · Handoff Session A 2026-08-28. +### TP36 — Index/Status als SSoT nach Gitea (Steuerung aus der branchbaren Datei herauslösen) ▸ ⏳ **CUTOVER 2026-09-07** — Index/Status = Gitea-Issues, Prosa bleibt Datei +- **Auslöser:** Skill-Run 2026-08-28 (`docs/handoff-20260828-session-a-projektstatus.md`) lieferte einen überholten „Nächster Schritt"-Output; **manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.** Verschärft 2026-09-06: `docs/teilprojekte.md` wurde in **40/40** letzten Commits angefasst — jeder TP editierte Quick-Index **und** Ist-Stand-Block → **Merge-Konflikt-Magnet**, der Branch-pro-TP verhinderte (ein Parallel-Branch hatte so die TP37-Doppelvergabe verursacht). +- 🔄 **Reframe 2026-09-06 (Christoph):** von „SSoT via **Nautobot**" auf „SSoT via **Gitea**". Grund: Index+Status sind **kuratierte Steuerung** (Reihenfolge, Marker, Abhängigkeiten), kein Geräte-Live-Zustand — die gehören auf ein **Issue-Board**, nicht in Custom Fields. **Nautobot bleibt nur für Live-Gerätedaten** relevant (Compliance-Zahl, RouterOS-Version …) → als optionaler Rest unten, entkoppelt von diesem TP. +- **Modell (umgesetzt):** TP = **Gitea-Issue** in [`admin/bf-infra`](https://gitea.emsenhuber.net/admin/bf-infra/issues) · Status = Label `status:*` (todo/wip/paused/idea/concept/done/dropped) · Gruppierung = Label `thema:*`. Seeder `bin/gitea-tp-sync.py` (idempotent, Dry-Run default, `--commit` schreibt; Token aus `~/.hermes/.env`, **nie ausgeben**). Erstlauf: **68 Issues (25 open/43 closed) + 15 Labels**. +- ✅ **Erledigt (A1–A3):** Labels/Modell · 68 TPs → Issues · Board = Index (Label-Filter statt Handtabelle). +- ⏳ **Cutover 2026-09-07 (diese Session):** + - **A4** — Quick-Index-Tabelle + „Ist-Stand (Kurz)"-Block aus dieser Datei entfernt; Header zeigt aufs Gitea-Board; die vier **Genehmigten Ausnahmen** unter eigenem Governance-Header behalten. Alle `### TPnn`-Prosa bleibt. + - **A5** — `/projektstatus` liest Baum+Status aus der **Gitea-API** (Issues + `status:*`/`thema:*`), Transaktionen weiter aus `nautobot/gc-transaktion.py status`, Prosa-Detail bei Bedarf aus dem Repo. Beide Skill-Kopien (`~/.claude/skills/…` **und** `~/.config/opencode/skills/…`, `cp`, kein Symlink). + - **A6** — Projekt-`CLAUDE.md`: Pflichtlektüre = Gitea-Board (Status) + Prosa-Datei (Detail); Branch-Konvention (`tpNN-kurz` jederzeit, Merge nach `main` wenn Code/Config steht, Steuerungs-Updates nach Gitea); SSoT-Notiz. +- 🔜 **Optionaler Rest (entkoppelt, „TP36-Live"):** Live-Daten-CustomFields auf den **4 `dcim.Device`** (`ros_version`, `compliance_pct`, `dhcp_server_count`, `mgmt_ip`, `dns_upstream_mode`) + Snapshot-Skript `nautobot/gc-snapshot.py` (existiert noch nicht). Unabhängig von diesem Cutover; nur relevant, wenn die Doku Live-Zahlen tragen soll. ⚠️ **Namensfalle bereinigt:** `nautobot/tp36-cf-estate-seed.py` gehörte **nie** zu TP36 → seit 2026-09-06 `tp44-cf-estate-seed.py`. +- **Verzahnt mit:** TP35 (SSoT + Konsolidierung), TP44 (native Nautobot-Modelle), `bf-infra/CLAUDE.md`-Lessons. Plan: `~/.claude/plans/geht-das-los-auch-reflective-lark.md`. ### TP38 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch ▸ 🔜 **ANGELEGT 2026-08-31** From 56746cb24dca8ab2e2f0e3ac8b95d24b69446a9b Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Mon, 7 Sep 2026 13:29:44 +0200 Subject: [PATCH 2/2] =?UTF-8?q?TP37=20P1:=20Substrat-Seeder=20=E2=80=94=20?= =?UTF-8?q?Hosts/Container=20als=20native=20VMs=20in=20Nautobot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idempotenter Seeder: 3 ClusterTypes (Proxmox VE/Synology Docker/Docker Desktop), 3 Clusters (pve/nas-bf/macmini), 28 VirtualMachines mit primary_ip4 (24 macvlan-IPs; macmini 4 host-geteilt). Native Modelle, keine App (konsistent TP44). Live 2026-09-07, Compliance 140/140 / 0 Drift. Co-Authored-By: Claude Opus 4.8 --- nautobot/tp37-substrat-seed.py | 256 +++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 nautobot/tp37-substrat-seed.py diff --git a/nautobot/tp37-substrat-seed.py b/nautobot/tp37-substrat-seed.py new file mode 100644 index 0000000..57a9d24 --- /dev/null +++ b/nautobot/tp37-substrat-seed.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +TP37 Punkt 1 (Substrat) — Hosts/Container als native Nautobot-Objekte. + +Idempotenter Seeder (get-or-create) fuer die native virtualization-Modellwelt: + ClusterType (3x: Proxmox VE / Synology Docker / Docker Desktop) + -> Cluster (pve / nas-bf / macmini) + -> VirtualMachine je Workload (Status Active) + + VMInterface "eth0" + ipam.IPAddress (Namespace Global) + primary_ip4 + +Datenmodell-Entscheid (User 2026-09-06): native Modelle, KEINE eigene App +(konsistent zu TP44). Muster: nautobot/tp44-cf-estate-seed.py. + +Umfang Etappe 1 (User-Default 2026-09-07): + - komplettes Substrat: alle 15 pve-Gaeste (inkl. Smart-Home) + alle running + Container auf nas-bf (Portainer ep14) und macmini (ep20). + - macmini-Container teilen sich die Host-IP 10.40.20.167 (Bridge/Port-Mapping) + -> KEIN primary_ip4; Port kommt in TP37 P2 (ipam.Service). + - portainer_agent (Bridge 172.17.0.2, kein Workload) weggelassen. + - gestoppte (exited) NAS-Container weggelassen (nur running). + +Datenstand: Live-Inventur 2026-09-07 (pve-mcp pct config, Portainer ep14, +ssh macmini docker ps, ARP fuer die 2 QEMU-IPs). + +Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored), +Schluessel NAUTOBOT_SUPERUSER_API_TOKEN. + +Aufruf: + python3 nautobot/tp37-substrat-seed.py # Dry-Run (default) + python3 nautobot/tp37-substrat-seed.py --commit # legt an +""" +import json +import os +import re +import sys +import urllib.request +import urllib.parse +import urllib.error +from pathlib import Path + +NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080") +REPO = Path(__file__).resolve().parent.parent +YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml" +COMMIT = "--commit" in sys.argv + + +def load_token() -> str: + for line in YML.read_text().splitlines(): + m = re.search(r"NAUTOBOT_SUPERUSER_API_TOKEN[=:]\s*([^\s\"']+)", line) + if m: + return m.group(1) + sys.exit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML} gefunden") + + +TOKEN = load_token() +HDRS = {"Authorization": f"Token {TOKEN}", "Accept": "application/json", + "Content-Type": "application/json"} + + +def req(method: str, path: str, body=None): + url = f"{NB}{path}" + data = json.dumps(body).encode() if body is not None else None + r = urllib.request.Request(url, data=data, headers=HDRS, method=method) + try: + with urllib.request.urlopen(r, timeout=20) as resp: + return resp.status, json.loads(resp.read().decode() or "{}") + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read().decode() or "{}") + + +def is_dry(v) -> bool: + return isinstance(v, str) and v.startswith(""} + st, d = req("POST", f"/api/{endpoint}/", payload) + if st in (200, 201): + print(f" + {label}: '{match_val}' angelegt ({d.get('id')})") + return d + print(f" ! {label}: '{match_val}' FEHLER {st}: {json.dumps(d)[:300]}") + return None + + +def status_id(name: str): + return find("extras/statuses", {"name": name}, "name", name)["id"] + + +def namespace_id(name: str): + return find("ipam/namespaces", {"name": name}, "name", name)["id"] + + +# ---------------------------------------------------------------- Inventar +CLUSTER_TYPES = ["Proxmox VE", "Synology Docker", "Docker Desktop"] + +# cluster -> clustertype +CLUSTERS = [("pve", "Proxmox VE"), + ("nas-bf", "Synology Docker"), + ("macmini", "Docker Desktop")] + +# cluster -> [ (vm_name, primary_ip_with_mask_or_None, comment) ] +VMS = { + "pve": [ + ("haos", "10.40.20.105/24", "Home Assistant OS (QEMU 100)"), + ("iob-master", "10.40.20.107/24", "ioBroker Master (LXC 101)"), + ("mqtt5", "10.40.20.108/24", "MQTT-Broker Mosquitto (LXC 102)"), + ("platform-pg", "10.40.20.20/24", "PostgreSQL 16 - Nautobot-DB (LXC 105, TP20)"), + ("influxdb2", "10.40.20.8/24", "InfluxDB 2 (LXC 106)"), + ("redis", "10.40.20.111/24", "Redis (LXC 107)"), + ("mcast-relay", "10.40.10.21/24", "SSDP-Relay VLAN10<->40 (LXC 108); 2. Bein 10.40.40.21"), + ("agh-bf1", "10.40.20.36/24", "AdGuard Home Resolver (LXC 110, TP23)"), + ("agh-bf2", "10.40.20.37/24", "AdGuard Home Resolver (LXC 111, TP23)"), + ("z2m25", "10.40.20.106/24", "Zigbee2MQTT (LXC 112)"), + ("caddy-bf1", "10.40.20.40/24", "Caddy Reverse-Proxy (LXC 130, TP31)"), + ("iob-exposed", "10.40.20.19/24", "ioBroker exposed (LXC 150)"), + ("mariadb", "10.40.20.23/24", "MariaDB - vw/hass/gitea (LXC 302)"), + ("RaspberryMatic","10.40.20.232/24", "Homematic CCU (QEMU 330)"), + ("docker", "10.40.20.126/24", "Docker-Host nested (LXC 400)"), + ], + "nas-bf": [ + ("nautobot", "10.40.20.30/24", "Nautobot Web (DS923 ep14)"), + ("nautobot-worker", "10.40.20.31/24", "Nautobot Golden-Config Worker"), + ("nautobot-scheduler", "10.40.20.32/24", "Nautobot Scheduler"), + ("nautobot-redis", "10.40.20.34/24", "Nautobot Redis"), + ("gitea", "10.40.20.157/24", "Gitea (git-SSoT)"), + ("vaultwarden", "10.40.20.127/24", "Vaultwarden (DB=mariadb LXC302)"), + ("shelly-mcp", "10.40.20.180/24", "Shelly-MCP"), + ("pve-mcp", "10.40.20.173/24", "Proxmox-MCP (mcp-pve)"), + ("iob_slave", "10.40.20.33/24", "ioBroker Slave"), + ], + "macmini": [ + # Bridge/Port-Mapping auf Host 10.40.20.167 -> kein eigenes primary_ip4 + ("n8n", None, "n8n (Docker Desktop, Host 10.40.20.167:5678)"), + ("n8n-redis", None, "n8n Redis (intern, kein Host-Port)"), + ("n8n-mcp", None, "n8n-MCP (Host 10.40.20.167:3001)"), + ("gitea-mcp", None, "Gitea-MCP (Host 10.40.20.167:3000)"), + ], +} + + +def main(): + print(f"== TP37 Substrat-Seed ({'COMMIT' if COMMIT else 'DRY-RUN'}) @ {NB} ==") + + st_active = status_id("Active") + ns_global = namespace_id("Global") + print(f" (Status Active={st_active} Namespace Global={ns_global})") + + print("\n[1] ClusterTypes") + for ct in CLUSTER_TYPES: + get_or_create("virtualization/cluster-types", {"name": ct}, "name", ct, + {"name": ct}, "ClusterType") + + print("\n[2] Clusters") + ct_lookup = {ct: find("virtualization/cluster-types", {"name": ct}, "name", ct) + for ct in CLUSTER_TYPES} + cluster_ids = {} + for cname, ctname in CLUSTERS: + ctrow = ct_lookup.get(ctname) + ct_id = ctrow["id"] if ctrow else None + cl = get_or_create("virtualization/clusters", {"name": cname}, "name", cname, + {"name": cname, "cluster_type": ct_id}, "Cluster") + cluster_ids[cname] = cl["id"] if cl else None + + print("\n[3] VirtualMachines (+ eth0 + IPAddress + primary_ip4)") + n_vm = n_ip = 0 + for cname, _ in CLUSTERS: + cl_id = cluster_ids.get(cname) + print(f" -- Cluster {cname} --") + for name, ip, comment in VMS[cname]: + vm = get_or_create( + "virtualization/virtual-machines", {"name": name}, "name", name, + {"name": name, "cluster": cl_id, "status": st_active, + "comments": comment}, "VM") + if not vm: + continue + n_vm += 1 + if not ip: + print(f" (ohne primary_ip4 - Host-geteilt)") + continue + # VMInterface eth0 + vmif = None + if is_dry(vm["id"]): + print(f" + eth0 @ {name} [DRY-RUN]") + vmif = {"id": f""} + else: + vmif = find("virtualization/interfaces", + {"virtual_machine": name, "name": "eth0"}, "name", "eth0") + if vmif: + print(f" = eth0 @ {name} ({vmif['id']})") + else: + s, d = req("POST", "/api/virtualization/interfaces/", + {"virtual_machine": vm["id"], "name": "eth0", + "status": st_active}) + if s in (200, 201): + vmif = d + print(f" + eth0 @ {name} ({d.get('id')})") + else: + print(f" ! eth0 @ {name} FEHLER {s}: {json.dumps(d)[:200]}") + continue + # IPAddress + ipobj = get_or_create("ipam/ip-addresses", {"address": ip}, "address", ip, + {"address": ip, "status": st_active, + "namespace": ns_global, "dns_name": ""}, + "IP") + if not ipobj: + continue + n_ip += 1 + if is_dry(vm["id"]) or is_dry(ipobj["id"]) or is_dry(vmif["id"]): + print(f" + assign {ip} -> {name}/eth0 + primary_ip4 [DRY-RUN]") + continue + # assign IP <-> interface (idempotent) + q = urllib.parse.urlencode({"vm_interface": vmif["id"], "depth": 0}) + s, d = req("GET", f"/api/ipam/ip-address-to-interface/?{q}") + have = any((r.get("ip_address") or {}).get("id") == ipobj["id"] + for r in d.get("results", [])) + if have: + print(f" = assign {ip} -> {name}/eth0") + else: + s, d = req("POST", "/api/ipam/ip-address-to-interface/", + {"ip_address": ipobj["id"], "vm_interface": vmif["id"]}) + print(f" {'+' if s in (200,201) else '!'} assign {ip} -> {name}/eth0" + + ("" if s in (200, 201) else f" FEHLER {s}: {json.dumps(d)[:150]}")) + # primary_ip4 (idempotent) + s, vmnow = req("GET", f"/api/virtualization/virtual-machines/{vm['id']}/?depth=0") + cur = vmnow.get("primary_ip4") + cur = cur.get("id") if isinstance(cur, dict) else cur + if cur == ipobj["id"]: + print(f" = primary_ip4 {ip} @ {name}") + else: + s, d = req("PATCH", f"/api/virtualization/virtual-machines/{vm['id']}/", + {"primary_ip4": ipobj["id"]}) + print(f" {'+' if s == 200 else '!'} primary_ip4 {ip} @ {name}" + + ("" if s == 200 else f" FEHLER {s}: {json.dumps(d)[:150]}")) + + print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN - nichts geschrieben'}) " + f"| VMs={n_vm} IPs={n_ip} ==") + + +if __name__ == "__main__": + main()