Compare commits
21 Commits
bca8f6e27e
...
claude/luc
| Author | SHA1 | Date | |
|---|---|---|---|
| 0278b3b8ef | |||
| 614025eafb | |||
| 677dade184 | |||
| b20edd6b62 | |||
| a38ed0fe1d | |||
| 5a29c482bd | |||
| 4eba3673b7 | |||
| 2b856d3d42 | |||
| f1d565bc88 | |||
| 43c592e5d4 | |||
| 33b18a08db | |||
| 799b693ffa | |||
| 4aeced30b5 | |||
| 89d94dbd45 | |||
| 6f60e03679 | |||
| 2f4866ca9a | |||
| 8e41268b4b | |||
| b915d02a82 | |||
| efb03979a0 | |||
| 268fd859ab | |||
| 7a20a1db4f |
@@ -57,7 +57,7 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
|||||||
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
|
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
|
||||||
- **`inventory/` ist tot** (Pipeline seit 2025-06 stale). Live-Daten aus MikroMCP/CF/Portainer.
|
- **`inventory/` ist tot** (Pipeline seit 2025-06 stale). Live-Daten aus MikroMCP/CF/Portainer.
|
||||||
- **`mcp/INVENTORY.md`** ist BF-Netz-SSoT; `~/.claude.json`/`opencode.jsonc` sind globalere Werkzeugkästen.
|
- **`mcp/INVENTORY.md`** ist BF-Netz-SSoT; `~/.claude.json`/`opencode.jsonc` sind globalere Werkzeugkästen.
|
||||||
- **terpene.teilzeitladen.xyz**: Worker läuft auf Mac mini (`10.40.20.167:8890`), **kein Caddy-Block, kein CF-Tunnel** — Backfill via DB-Migration blockiert (Handoff 2026-08-27).
|
- **Terpenfinder = `420.teilzeitladen.xyz`** (CF-Worker, Repo `admin/blaueblueten-n8n`, hinter CF Access), **kein Caddy, kein Mac mini im Pfad**. `terpene.*` **existiert nicht und soll es nicht** (live geprüft 2026-09-10). Einziger terpene-benannter CF-Gegenstand: der **interne** Tunnel `terpene-db` (`f2d2fc8a`, ohne öffentlichen Hostname, Hyperdrive→VPC→Postgres, s. `docs/cf-tunnel-mapping.md` §17). ⚠️ **Grenze:** blaueblüten ist ein eigenes Projekt — nicht anfassen. *(Die frühere „terpene.teilzeitladen.xyz:8890 via Tunnel"-Zeile war veraltet — bf-mm ist gelöscht.)*
|
||||||
- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. Inventar-Eintrag spiegelt das. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention.
|
- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. Inventar-Eintrag spiegelt das. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention.
|
||||||
- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Performance-Überlegung: MCP-Clients würden auf dem macmini (User-Workstation) laufen und die Box zusätzlich belasten; deshalb liegt der MCP-Host auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft aktuell noch auf dem macmini (historisch, eigene Container-Definition dort) — gehört **bei Gelegenheit** auf die DS923 umgezogen (separater Punkt, **nicht** Teil dieser Session). Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`.
|
- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Performance-Überlegung: MCP-Clients würden auf dem macmini (User-Workstation) laufen und die Box zusätzlich belasten; deshalb liegt der MCP-Host auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft aktuell noch auf dem macmini (historisch, eigene Container-Definition dort) — gehört **bei Gelegenheit** auf die DS923 umgezogen (separater Punkt, **nicht** Teil dieser Session). Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`.
|
||||||
|
|
||||||
|
|||||||
18
__init__.py
Normal file
18
__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Macht die bf-infra-Repo-Wurzel zu einem regulären Python-Paket.
|
||||||
|
|
||||||
|
🔴 LOAD-BEARING — NICHT löschen (sonst laden die Nautobot-Jobs nicht mehr).
|
||||||
|
|
||||||
|
Nautobot klont dieses Repo nach GIT_ROOT/bf_infra/ und lädt Jobs über
|
||||||
|
`import_modules_privately()` → `pkgutil.walk_packages([GIT_ROOT])`. walk_packages
|
||||||
|
entdeckt ausschließlich *reguläre* Pakete (mit __init__.py), niemals
|
||||||
|
Namespace-Packages (Verzeichnisse ohne __init__.py). Ohne diese Datei ist
|
||||||
|
`bf_infra` für den Loader unsichtbar und `jobs/` wird nie importiert — Symptom:
|
||||||
|
Git-Sync SUCCESS, aber Log "No jobs were registered on loading the
|
||||||
|
`bf_infra.jobs` submodule" und kein Job-DB-Eintrag.
|
||||||
|
|
||||||
|
Der module_path-Filter des Loaders (["bf_infra","jobs"]) importiert nur
|
||||||
|
`bf_infra` + `bf_infra.jobs.*`; die übrigen Verzeichnisse (nautobot/, routeros/,
|
||||||
|
bin/, …) bleiben unangetastet und laufen weiter als Standalone-Skripte.
|
||||||
|
|
||||||
|
Verifiziert 2026-09-10 (Nautobot 3.1.7, Python 3.14) — TP44 Teil B.
|
||||||
|
"""
|
||||||
@@ -8,9 +8,33 @@
|
|||||||
> und As-Built-Log; die *strukturierte* Estate liest man ab jetzt in Nautobot. Etappe 2 (CF-API→Nautobot-Sync)
|
> und As-Built-Log; die *strukturierte* Estate liest man ab jetzt in Nautobot. Etappe 2 (CF-API→Nautobot-Sync)
|
||||||
> ist offen.
|
> ist offen.
|
||||||
|
|
||||||
> **Aktueller Stand: → §10 (2026-07-10, TP16 UMGESETZT ✅).** §9 = Analyse/Zielbild, §10 = As-Built.
|
> **Aktueller Stand: → §0 (unten).** §1–16 sind Historie/As-Built-Log früherer Sessions und beschreiben
|
||||||
> Abschnitte 1–8 sind Historie (Track A, 2026-06-30); die „3 Connectors gut/Zielbild"-Aussagen dort sind
|
> Topologien, die **nicht mehr live** sind (v.a. das 3-Tunnel-HA `bf-mm`/`bf-nas`/`bf-pve`). §9 = Analyse,
|
||||||
> durch §9/§10 **überholt** (Round-Robin = macvlan-Host-Isolation → 1/N-Ausfälle). Track A aus `docs/handoff-cf-tunnel-1033.md`.
|
> §10–15 = As-Built des inzwischen abgelösten 3-Tunnel-Designs. Track A aus `docs/handoff-cf-tunnel-1033.md`.
|
||||||
|
|
||||||
|
## §0 — Live-Ist-Stand (2026-09-08, maßgeblich)
|
||||||
|
|
||||||
|
> Der einzige aktuelle Topologie-Stand. **§1–16 nicht als aktuelle Konfiguration lesen** — insbesondere das
|
||||||
|
> 3-Tunnel-HA `bf-mm`/`bf-nas`/`bf-pve` (§10–15) existiert seit dem TP16.3-Cutover (bf-public/Caddy,
|
||||||
|
> 2026-08-27) **nicht mehr**. Strukturierte Estate = Nautobot (TP44, deckt sich mit diesem Stand);
|
||||||
|
> operativer Master = Cloudflare. Read-only gemessen gegen die CF-API (`cfd_tunnel`, `warp_connector`,
|
||||||
|
> `teamnet/routes`) am 2026-09-08.
|
||||||
|
|
||||||
|
**cloudflared-Tunnel (live) — es gibt genau drei:**
|
||||||
|
|
||||||
|
| Tunnel | ID | Conns | Zweck |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **bf-public** | `48d3b0b6-7233-4781-a76a-1de4a5e957ce` | 2, healthy | **einziger öffentlicher BF-Tunnel** (TP31/TP16.3), erstellt 2026-08-27 |
|
||||||
|
| **fm.emsenhuber.net** | `b697d124-53bf-4a33-8fcf-384cced4db1a` | 1, healthy | Standort FM (`ds-fm`, `gateway-fm`), separat |
|
||||||
|
| **terpene-db** | `f2d2fc8a-54ae-41df-b09a-97312cdb9fe2` | 1, healthy | DB-Pfad (Hyperdrive→VPC-Service), Rebuild 2026-09-01 — s. §17 |
|
||||||
|
|
||||||
|
**bf-public-Ingress** → alle vier auf **Caddy `10.40.20.40:443`** (Caddy macht das Fan-out), catch-all 404:
|
||||||
|
`vw` · `hass` · `n8n` · `gitea` (`.emsenhuber.net`).
|
||||||
|
|
||||||
|
**WARP / Private Access — nativer Mesh, KEIN cloudflared:**
|
||||||
|
- warp_connector **bf-core-mesh** `d1da7c8a-2736-4cbb-8af1-32db5393fb9c`, healthy — nativer
|
||||||
|
WireGuard-Mesh-Node auf dem Core (TP16.1). **Live und produktiv** (nicht mehr „gated/Konzept" wie in §14).
|
||||||
|
- Routen: `10.40.0.0/16` → **bf-core-mesh** · `10.101.0.0/24` → **fm**. Ein vnet `vnetemsenhuber` (default).
|
||||||
|
|
||||||
## 1. Alle Tunnel im Account (`393c…66c36`)
|
## 1. Alle Tunnel im Account (`393c…66c36`)
|
||||||
| Tunnel | ID | Status | Conns | Verwendung |
|
| Tunnel | ID | Status | Conns | Verwendung |
|
||||||
@@ -234,6 +258,9 @@ Der macmini ist vollwertiger dritter Knoten (nicht „vergessen").
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🗄️ <b>ARCHIV (§10–16)</b> — überholtes 3-Tunnel-HA <code>bf-mm</code>/<code>bf-nas</code>/<code>bf-pve</code>. Seit dem TP16.3-Cutover (2026-08-27) nicht mehr live — siehe §0. Aufklappen nur für die Historie.</summary>
|
||||||
|
|
||||||
## 10. AS-BUILT 2026-07-10 (TP16 umgesetzt) — reziprokes 3-Tunnel-HA ✅
|
## 10. AS-BUILT 2026-07-10 (TP16 umgesetzt) — reziprokes 3-Tunnel-HA ✅
|
||||||
> Gebaut & verifiziert in eigener Session. Round-Robin-#1033 **strukturell weg** (Loop-Gegentest 0×530 über alle 19 Live-FQDNs, HA per Host-Ausfall-Sim bewiesen). Schritte 1–4+HA durch; **Schritt 5 (Access/WARP + `vw`) offen** (user-gated).
|
> Gebaut & verifiziert in eigener Session. Round-Robin-#1033 **strukturell weg** (Loop-Gegentest 0×530 über alle 19 Live-FQDNs, HA per Host-Ausfall-Sim bewiesen). Schritte 1–4+HA durch; **Schritt 5 (Access/WARP + `vw`) offen** (user-gated).
|
||||||
|
|
||||||
@@ -337,6 +364,10 @@ Letzte in §9.2/§8 als „bleibt `.41`" geführte Origin-Klasse (`pve .41.100`,
|
|||||||
|
|
||||||
## 14. Richtungsentscheid 2026-07-16 — Private Access via native Cloudflare Mesh (WARP Connector) auf dem CRS326 Core-Switch (TP16.1)
|
## 14. Richtungsentscheid 2026-07-16 — Private Access via native Cloudflare Mesh (WARP Connector) auf dem CRS326 Core-Switch (TP16.1)
|
||||||
|
|
||||||
|
> ✅ **Nachtrag 2026-09-08 (live):** umgesetzt. warp_connector **bf-core-mesh** (`d1da7c8a-…`) ist healthy
|
||||||
|
> und trägt die Route `10.40.0.0/16` — der „Scharfschalten = eigene Session, gated"-Status unten ist
|
||||||
|
> überholt. Siehe §0.
|
||||||
|
|
||||||
> ✅ **Update 2026-07-22 (User-Entscheid):** Ziel-Device retargeted von **ap1** auf den **CRS326 Core-Switch** — konsistent mit TP15-Endstand (kein RB5009, Router bleibt ap1/ap2) und TP21s Kabel-Fabric-Modell. Reine Ziel-Doku-Änderung, Scharfschalten bleibt gated bis TP21 den CRS326 abdeckt. Der Rest dieser Sektion ist entsprechend auf den neuen Zielhost umgeschrieben; CRS326 ist aktuell noch Werkszustand, Management-IP/Interface TBD.
|
> ✅ **Update 2026-07-22 (User-Entscheid):** Ziel-Device retargeted von **ap1** auf den **CRS326 Core-Switch** — konsistent mit TP15-Endstand (kein RB5009, Router bleibt ap1/ap2) und TP21s Kabel-Fabric-Modell. Reine Ziel-Doku-Änderung, Scharfschalten bleibt gated bis TP21 den CRS326 abdeckt. Der Rest dieser Sektion ist entsprechend auf den neuen Zielhost umgeschrieben; CRS326 ist aktuell noch Werkszustand, Management-IP/Interface TBD.
|
||||||
|
|
||||||
**Trennung der Concerns (wichtig):**
|
**Trennung der Concerns (wichtig):**
|
||||||
@@ -452,6 +483,8 @@ Netzwerk-Namespace, sieht ohnehin alles), und die cloudflared-Connectoren laufen
|
|||||||
eigene Maschinen, auf denen die Blindheit prinzipiell nicht auftreten kann. Der Shim bleibt als Option
|
eigene Maschinen, auf denen die Blindheit prinzipiell nicht auftreten kann. Der Shim bleibt als Option
|
||||||
für den Fall, dass später doch ein Dienst neben macvlan-Nachbarn einziehen soll.
|
für den Fall, dass später doch ein Dienst neben macvlan-Nachbarn einziehen soll.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 17. Terpenfinder-DB-Pfad neu aufgebaut 2026-09-02 (Fremdprojekt-Handout blaueblüten)
|
## 17. Terpenfinder-DB-Pfad neu aufgebaut 2026-09-02 (Fremdprojekt-Handout blaueblüten)
|
||||||
|
|
||||||
**Auslöser:** Handout `blaueblueten-n8n/docs/handout-bf-infra-db-pfad-20260902.md` — der Cloudflare-Worker
|
**Auslöser:** Handout `blaueblueten-n8n/docs/handout-bf-infra-db-pfad-20260902.md` — der Cloudflare-Worker
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ Token aus `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored). Aufruf: Dry-Ru
|
|||||||
- VPC-Service-API liegt unter `connectivity/directory/services` (Update = **PUT**, voller Body), nicht `/vpc/`.
|
- VPC-Service-API liegt unter `connectivity/directory/services` (Update = **PUT**, voller Body), nicht `/vpc/`.
|
||||||
- Der Repo-`CF_API_TOKEN` darf `cfd_tunnel`, aber **nicht** Workers-VPC (`connectivity/directory`) — dort
|
- Der Repo-`CF_API_TOKEN` darf `cfd_tunnel`, aber **nicht** Workers-VPC (`connectivity/directory`) — dort
|
||||||
über den `cloudflare`-MCP-Credential gehen.
|
über den `cloudflare`-MCP-Credential gehen.
|
||||||
|
> ⚠️ **ÜBERHOLT (2026-09-11):** falsch. Live getestet liest der `CF_API_TOKEN` aus
|
||||||
|
> `~/.secrets/credentials.env` Hyperdrive/VPC/Workers (alle 200) — der schmale ist `CF_DNS_API_TOKEN`.
|
||||||
|
> Der Teil-B-Sync deckt damit 14/14 ab. Details: `handoff-20260910-tp44-teilb-sync-live.md`.
|
||||||
|
|
||||||
## Offen (Etappe 2, eigene Session)
|
## Offen (Etappe 2, eigene Session)
|
||||||
|
|
||||||
|
|||||||
57
docs/handoff-20260910-tp44-teilb-sync-live.md
Normal file
57
docs/handoff-20260910-tp44-teilb-sync-live.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Handoff 2026-09-10 — TP44 Teil B: CF-Estate-Sync als Nautobot-Job LIVE
|
||||||
|
|
||||||
|
> **Ergebnis:** Der Cloudflare-Estate→Nautobot-Sync läuft als **täglicher Nautobot-Job**
|
||||||
|
> (`CFEstateSync`, grouping `TP44 / Cloudflare`). Dry-Run + Commit-Run sauber: **4 CloudNetworks /
|
||||||
|
> 14 CloudServices, alle CF-Endpoints lesbar, 0 Drift.** CF bleibt operativer Master, Nautobot ist
|
||||||
|
> read-only Dokumentations-SSoT. **TP44 ist damit vollständig durch.**
|
||||||
|
|
||||||
|
## Governance (User, 2026-09-09)
|
||||||
|
Periodische externe→Nautobot-Syncs leben als **Nautobot-Jobs** in `bf-infra/jobs/` und werden **in
|
||||||
|
Nautobot geplant** — kein per-Task-Container, kein Cron. Künftige Syncs = weitere Jobs in `jobs/`.
|
||||||
|
|
||||||
|
## Was live ist
|
||||||
|
- **Runtime:** `jobs/cf_estate_sync.py` (`CFEstateSync`, ORM). Pull der CF-API (urllib+Bearer),
|
||||||
|
Upsert per `extra_config`-Match (`tunnel_id`/`script`/`id`/`service_id`), Verwaisung = `stale`-Flag
|
||||||
|
statt Löschen. 403-tolerant (fehlender Scope kippt den Sync nicht; Verwaisung wird dann übersprungen).
|
||||||
|
- **Registrierung:** `jobs/__init__.py` ruft `register_jobs(CFEstateSync)`; **Repo-Wurzel-`__init__.py`**
|
||||||
|
macht `bf_infra` als reguläres Paket sichtbar.
|
||||||
|
- **Token:** Nautobot-Secret `cf-api-token` (Provider `environment-variable`) → `CF_API_TOKEN` am
|
||||||
|
`nautobot-worker`, verdrahtet im Anchor-`environment` von `docs/nas-vlan20/stacks/210-nautobot.yml`
|
||||||
|
(gitignored). Wert = der breite `CF_API_TOKEN` aus `~/.secrets/credentials.env` (liest 14/14).
|
||||||
|
- **Schedule:** `ScheduledJob "CF Estate Sync – taeglich"`, `interval=daily`, `start_time=2026-09-11T03:00:00Z`
|
||||||
|
(= 05:00 Europe/Vienna, vor den Golden-Config-Läufen), `enabled=True`, `dryrun=False`.
|
||||||
|
- **GitRepository:** `bf-infra` (`provided_contents=['extras.job']`) — war schon verdrahtet.
|
||||||
|
|
||||||
|
## Drei load-bearing Funde (sonst lädt/plant der Job nicht)
|
||||||
|
1. **Repo-Wurzel `__init__.py` ist Pflicht.** Nautobot 3.1.7 lädt Git-Repo-Jobs über
|
||||||
|
`import_modules_privately()` → `pkgutil.walk_packages([GIT_ROOT])`. walk_packages entdeckt **nur**
|
||||||
|
reguläre Pakete (mit `__init__.py`), **keine** Namespace-Packages. Ohne die Datei ist der geklonte
|
||||||
|
`GIT_ROOT/bf_infra/` unsichtbar → `jobs/` wird nie importiert. Symptom: Git-Sync **SUCCESS**, aber
|
||||||
|
Log „No jobs were registered on loading the `bf_infra.jobs` submodule" und **kein** Job-DB-Eintrag.
|
||||||
|
bf-infra ist das **erste** Git-Repo mit `extras.job` — daher nie zuvor aufgetreten. Der `module_path`-
|
||||||
|
Filter des Loaders importiert nur `bf_infra` + `bf_infra.jobs.*`; `nautobot/`, `routeros/` etc. bleiben
|
||||||
|
unberührt.
|
||||||
|
2. **`register_jobs()` gehört ins Paket-`__init__`**, nicht nur ins Submodul (Doku bestätigt).
|
||||||
|
3. **`Meta.has_sensitive_variables = False`** nötig — Default `True` ⇒ Nautobot verweigert das Scheduling
|
||||||
|
(„Job may have sensitive input variables"). Der Job nimmt nur `dryrun` (Boolean); das Token kommt aus
|
||||||
|
dem Secret, nicht aus einer Job-Variable.
|
||||||
|
|
||||||
|
## Zwei Daten-/Code-Funde beim Dry-Run
|
||||||
|
- **VPC-Bug:** VPC-Directory-Services tragen ihren Identifier in **`service_id`**, nicht `id`. Der Job las
|
||||||
|
`id` (=None) → per `.get(mk)==None` matchte er ein Fremdobjekt (App Launcher) und ließ den echten
|
||||||
|
`terpene-db` auf stale laufen. Fix: VPC nutzt `service_id`; `_upsert` überspringt `mv=None` generell.
|
||||||
|
- **Seed-Artefakt:** der geseedete CloudService `terpene-db` hatte `extra_config.service_id: null`
|
||||||
|
(der echte `019fa3d5-…` stand beim Seed nur in den Hyperdrive-Einträgen als `vpc_service_id`).
|
||||||
|
**Einmalig reconciled** (PATCH `service_id=019fa3d5-…`) → seither matcht der Sync sauber, 0 Drift.
|
||||||
|
|
||||||
|
## Drift-Korrektur (nicht stillschweigend)
|
||||||
|
Die frühere Notiz „Repo-`CF_API_TOKEN` darf kein Workers-VPC → über den `cloudflare`-MCP-Credential"
|
||||||
|
(TP44-Prosa + `handoff-20260902-…`) war **falsch**. Live getestet liest der `CF_API_TOKEN` aus
|
||||||
|
`~/.secrets/credentials.env` Hyperdrive/VPC/Workers (alle 200). Der schmale Token ist `CF_DNS_API_TOKEN`
|
||||||
|
(403). Der `cloudflare`-MCP ist der **remote OAuth-MCP** (kein API-Token) und für einen serverseitigen
|
||||||
|
Job ohnehin nicht nutzbar.
|
||||||
|
|
||||||
|
## Nächster CF-Sync = neuer Job im selben Muster
|
||||||
|
Modul in `jobs/` anlegen, Klasse importieren + `register_jobs(...)` im `jobs/__init__.py` ergänzen,
|
||||||
|
`Meta.has_sensitive_variables=False` setzen, Git-Sync, enablen, Dry-Run, dann Schedule. Die Wurzel-
|
||||||
|
`__init__.py` gilt schon.
|
||||||
65
docs/handoff-20260912-tp25-ap2-messung-blockiert.md
Normal file
65
docs/handoff-20260912-tp25-ap2-messung-blockiert.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Handoff 2026-09-12 — TP25 „ap2 unterperformt": Messung blockiert (kein Manager-Zugriff)
|
||||||
|
|
||||||
|
**Kurz:** Die ap2-In-situ-Frage (2 m/−80 dBm) konnte **nicht** gemessen werden. Grund ist **kein**
|
||||||
|
WLAN-/Gerätedefekt, sondern ein **fehlender Management-Pfad zum Manager `crs326-core`** von den
|
||||||
|
verfügbaren Vantage-Points. Auf Christophs Entscheidung **vertagt**, kein Geräte-Write, SPOF nicht
|
||||||
|
angefasst.
|
||||||
|
|
||||||
|
## Bestandsaufnahme (erledigt)
|
||||||
|
|
||||||
|
- Gitea #17 (TP25) = nur Zeiger, `status:wip`, Prosa im Repo. SSoT bestätigt.
|
||||||
|
- Prosa `docs/teilprojekte.md` ab Zeile 1081 + Archiv-Langfassung gelesen.
|
||||||
|
- Handoffs `20260902-fund-e` (Fund E geschlossen, 7.24.1 war die Ursache) und
|
||||||
|
`20260903-kanalplan` (Kanalplan live, In-situ-Messung als einziger Rest offen) gelesen.
|
||||||
|
- **Einziger offener TP25-Punkt bleibt: ap2 unterperformt.**
|
||||||
|
|
||||||
|
## Warum die Messung blockiert ist (live verifiziert, nicht geraten)
|
||||||
|
|
||||||
|
Registration-Table/Scan liegen unter CAPsMAN **nur am Manager** (`crs326-core`). Dieser ist von
|
||||||
|
beiden Messpunkten nicht erreichbar:
|
||||||
|
|
||||||
|
**Vantage 1 — diese Session (Mac am dw-Standort):**
|
||||||
|
- Default-GW `fritz.box` 10.95.10.1 → BF nur über **WG-Tunnel**; **Cloudflare WARP aktiv** (lief).
|
||||||
|
- **ICMP zu ganz BF ok** (ap2, NAS, Nautobot, core — alle ~32 ms, 0 % Loss).
|
||||||
|
- **Direktes TCP nach BF tot**: VLAN20-Hosts (NAS:22, Nautobot:8080, pve:8006) → `connect`
|
||||||
|
kommt nie zustande (`connect=0.000`, `http=000`). MikroTik-Ports schienen bei `nc -z` „offen",
|
||||||
|
aber jeder echte Handshake (SSH-kex, REST, HTTPS) wird **resettet** → das waren
|
||||||
|
**WARP-lokale nc-Fehlmeldungen** (CLAUDE.md-Gotcha: `nc -z` falsch-positiv/-negativ).
|
||||||
|
- ⇒ Von hier existiert **kein** TCP-Management-Pfad in BF. Der Pfad trägt gerade nur ICMP.
|
||||||
|
Passt zum WARP-/Tunnel-Muster (WARP = QUIC-Proxy, kein ICMP; hier umgekehrt: nur ICMP kommt durch).
|
||||||
|
|
||||||
|
**Vantage 2 — mikromcp (DS923, im BF-LAN, kein WARP):**
|
||||||
|
- `check_router_health` core → **Connect Timeout :80** (~10,5 s).
|
||||||
|
- ap1/ap2 → **`other side closed` / `ECONNRESET`** (TCP an, Handshake zu).
|
||||||
|
- `run_command` core → `Timed out waiting for handshake`; ap2 → `Connection lost before handshake`.
|
||||||
|
- ⇒ Auch der on-LAN-Pfad erreicht die Flotte nicht. Mögliche Ursachen (von hier **nicht** trennbar,
|
||||||
|
daher nicht entschieden): VLAN20-Firewall auf :80 (mgmt-input nur VLAN10/HA:8728/Nautobot:22/cf-mesh),
|
||||||
|
mikromcp-Host-Netzproblem auf der DS923, oder real.
|
||||||
|
|
||||||
|
**Wichtig:** Der Core ist **nicht** nachweislich defekt — ICMP + Routing sind gesund. Der Defekt liegt
|
||||||
|
im Zugangspfad, nicht (belegbar) am Gerät.
|
||||||
|
|
||||||
|
## Nebenbefund: Doku-Drift ap2-Kanal (nicht verifizierbar mangels Manager-Zugriff)
|
||||||
|
|
||||||
|
- CLAUDE.md + `teilprojekte.md`-Langfassung: ap2 = **ch44 / 5220** (2,4 GHz ch11).
|
||||||
|
- Handoff 09-03: ap2 = **5300/5320** (ch60/64, DFS Band B) + ch11.
|
||||||
|
- Was **live** läuft, ist offen. Beim nächsten Manager-Zugriff via
|
||||||
|
`/interface wifi radio print` (Frequenz je Radio) klären und die Doku angleichen.
|
||||||
|
|
||||||
|
## Nächste Session — was zu tun ist
|
||||||
|
|
||||||
|
1. **Sauberen Pfad herstellen** (Christophs Wahl, offen): WARP trennen + retest, ODER aus einer
|
||||||
|
**BF-LAN-Session** (macmini) messen, ODER zuerst **mikromcp/DS923-Erreichbarkeit** zur Flotte klären.
|
||||||
|
2. Sobald Manager erreichbar, der Messplan aus Handoff 09-03 §„Aufstellorts-Messplan":
|
||||||
|
- `/interface wifi radio print terse proplist=cap,radio-mac,interface` (Karten-Snapshot; wandert bei Rejoin).
|
||||||
|
- `/interface wifi radio print` → aktuelle Frequenz/TX je Radio (ap2 ch prüfen, Doku-Drift oben).
|
||||||
|
- `/interface wifi registration-table print terse where ssid=dirtyvader-main` → Signal/Rate der ap2-Clients.
|
||||||
|
- `/interface wifi scan cap-wifiN duration=…` **am Manager** → 5-GHz-Umfeld an ap2 (Wiederholung des 08-24-Scans).
|
||||||
|
- Ziel: −80 dBm@2 m einordnen (Platzierung/Antenne/TX vs. Kanal-Umfeld), mit Zahlen.
|
||||||
|
3. **Gotchas** (CLAUDE.md/09-03): scan/registration-table nur am Manager · `list_wifi_interfaces`
|
||||||
|
leakt PSKs (nicht benutzen) · `cap-wifiN`-Karte wandert · vor Client-Diagnose `uptime`+`grep " caps,"`.
|
||||||
|
|
||||||
|
## Grenze eingehalten
|
||||||
|
|
||||||
|
Kein Geräte-Write. Core (SPOF) nicht angefasst. WARP nicht getrennt (Christophs Client). Doku-Drift
|
||||||
|
dokumentiert statt stillschweigend „korrigiert".
|
||||||
@@ -63,6 +63,11 @@ teuerste Eigenschaft der Datei zerstört, um ihr billigstes Problem zu lösen. B
|
|||||||
| [TP28](#tp28) | WLAN über CAPsMAN auf `crs326-core` | ↩️ **wieder geöffnet 2026-08-30** (ap3) — Volltext in [teilprojekte.md](teilprojekte.md#tp28) |
|
| [TP28](#tp28) | WLAN über CAPsMAN auf `crs326-core` | ↩️ **wieder geöffnet 2026-08-30** (ap3) — Volltext in [teilprojekte.md](teilprojekte.md#tp28) |
|
||||||
| [TP-R](#tp-r) | Router-Capabilities-Sweep | FERTIG (docs/research/routeros-capabilities.md) |
|
| [TP-R](#tp-r) | Router-Capabilities-Sweep | FERTIG (docs/research/routeros-capabilities.md) |
|
||||||
| [TP39](#tp39) | LXC-Hygiene pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup | ✅ ABGESCHLOSSEN (2026-09-01) — pve+NAS-Sweep grün |
|
| [TP39](#tp39) | LXC-Hygiene pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup | ✅ ABGESCHLOSSEN (2026-09-01) — pve+NAS-Sweep grün |
|
||||||
|
| [TP34](#tp34) | Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) | ❌ VERWORFEN (2026-09-08) — Caddyfile bleibt manuell |
|
||||||
|
| [TP44](#tp44) | Cloudflare-Estate als Dokumentations-SSoT in Nautobot | ✅ DURCH (2026-09-10) |
|
||||||
|
| [TP41](#tp41) | CF-Access-Reparatur + Zero-Trust-Konsolidierung | ✅ ABGESCHLOSSEN (2026-09-10) — Gelis WARP via Android eingerichtet, letzter Blocker weg |
|
||||||
|
| [TP43](#tp43) | dw-Standort-DNS-Uplink (site-spezifische Overrides) | ♻️ REOPENED (2026-09-11) — Volltext zurück in teilprojekte.md; interne BF-Namen über WG am dw offen |
|
||||||
|
| [TP42](#tp42) | Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net | ✅ DURCH (2026-09-02) — `.bf.lan` raus, `.16` abgebaut, Compliance 140/140 |
|
||||||
|
|
||||||
Dazu am Ende: **[TP25 (Langfassung)](#tp25-lang)** — die Messchronik und die Funde A–E im Volltext.
|
Dazu am Ende: **[TP25 (Langfassung)](#tp25-lang)** — die Messchronik und die Funde A–E im Volltext.
|
||||||
TP25 selbst ist **nicht** abgeschlossen und steht weiter in `teilprojekte.md`; nur seine 394 Zeilen
|
TP25 selbst ist **nicht** abgeschlossen und steht weiter in `teilprojekte.md`; nur seine 394 Zeilen
|
||||||
@@ -1777,3 +1782,198 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
|
|||||||
- **Nicht** TP31/TP34 (Caddy) und nicht TP11 (Monitoring) — obwohl gilt: ein Monitoring, das
|
- **Nicht** TP31/TP34 (Caddy) und nicht TP11 (Monitoring) — obwohl gilt: ein Monitoring, das
|
||||||
DB-Verbindungszeiten kennt, hätte diesen Fall Monate früher gezeigt.
|
DB-Verbindungszeiten kennt, hätte diesen Fall Monate früher gezeigt.
|
||||||
- **Nicht** TP40: der HA-Recorder-Wildwuchs (Punkt 3) ist HA-seitig und ein eigenes TP.
|
- **Nicht** TP40: der HA-Recorder-Wildwuchs (Punkt 3) ist HA-seitig und ein eigenes TP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ ❌ **VERWORFEN (2026-09-08)**
|
||||||
|
> 🔴 **Nachtrag 2026-09-08 (verworfen):** Der Ansatz „Caddyfile via Golden Config rendern" ist
|
||||||
|
> zurückgezogen (Commits `d63dc7b`, `bca8f6e`; Gitea-Issue #24 → `status:dropped`, closed). Der
|
||||||
|
> `420.teilzeitladen.xyz`-Block wurde wieder aus dem GC-gerenderten Caddyfile entfernt; **der
|
||||||
|
> Caddyfile bleibt manuell** in `caddy/Caddyfile`. Gründe: kein zweites SSoT-Modell für Proxy-Routen
|
||||||
|
> nötig, TP37 deckt die Dienst-Doku als `ipam.Service` bereits ab, und der einzige akute Anwendungsfall
|
||||||
|
> ließ sich mit einem manuellen Block lösen. Text unten unverändert als Konzept-Historie.
|
||||||
|
|
||||||
|
> **Konzept + Architektur-Skizze:** [`docs/handoff-20260827-inventur-und-architektur.md`](handoff-20260827-inventur-und-architektur.md) — bitte dort weiterlesen (Architektur-Abschnitt).
|
||||||
|
- **Scope:** Reverse-Proxy-Hosts werden im **Nautobot-Datenmodell** gepflegt (Site, Host, Backend-Spec). **Golden Config** rendert den Caddyfile aus den Daten, Diff-Check gegen die Live-Version auf `caddy-bf1`, Compliance-Checks + Transaktionen identisch zum bestehenden Workflow für Router-Configs.
|
||||||
|
- **Auslöser (Inventur 2026-08-27):** der Caddyfile (`bf-infra/caddy/Caddyfile`) ist heute **manuell gepflegt** — kein Drift-Check, keine Transaktionen, andere Projekte können keine Sites anlegen ohne Christoph. Lösung: **Nautobot/GC**, kein Caddy-API-Sidecar (kein zweites SSoT).
|
||||||
|
- **Erster Anwendungsfall:** `terpene.teilzeitladen.xyz → http://10.40.20.167:8890` (Mac-mini-Backend, dort PostgREST + sync + web via lokalem Caddy `:8890`). Vor TP34: manueller Quick-Win-Caddy-Block.
|
||||||
|
- **Abh.:** TP22 (GC-Pipeline) ✅ · TP23 (DNS/Caddy-Voraussetzungen) ✅ Stufe 1 · 🔴 **NEU 2026-09-06: TP37 Punkt 1+2** — Caddy rendert *Config aus Daten*, die Backend-Dienste müssen als `ipam.Service` in Nautobot existieren, sonst gibt es nichts zu rendern. **TP37 vor TP34.**
|
||||||
|
- **Datenmodell (entschieden 2026-09-06):** native Modelle — Backend = `ipam.Service`, Proxy-Route = Relationship/CustomField Frontend↔Backend, GC-Job rendert den Caddyfile. Keine eigene App (konsistent zu TP44).
|
||||||
|
- **Status:** 🔜 Stub; wartet auf TP37 P1+2. Quick-Win `terpene.teilzeitladen.xyz → 10.40.20.167:8890` als manueller Caddy-Block ist **Schritt 0** des Runbooks. Plan: [`handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<a id="tp44"></a>
|
||||||
|
### TP44 — Cloudflare-Estate als Dokumentations-SSoT in Nautobot ▸ ✅ **DURCH (2026-09-10): Etappe 1 + Etappe 2 (Teil A IPAM-Prefixe, Teil B Live-Sync als täglicher Nautobot-Job)**
|
||||||
|
> **Entscheid (User, 2026-09-02):** die volle CF-Estate (Tunnel + WARP-Connector + VPC-Service +
|
||||||
|
> Hyperdrive + Workers + alle Access-Apps) als strukturierte, abfragbare SSoT in Nautobot abbilden —
|
||||||
|
> **etappenweise: manuell jetzt, Sync später.** Nautobot kann CF nicht steuern → „SSoT" heißt
|
||||||
|
> **Dokumentations-SSoT; CF bleibt operativer Master.** Detail-Handoff:
|
||||||
|
> [`handoff-20260902-cf-estate-nautobot-ssot.md`](handoff-20260902-cf-estate-nautobot-ssot.md).
|
||||||
|
|
||||||
|
- **Warum eigenes TP, nicht TP36:** TP36 ist eng auf `teilprojekte.md`-Live-Daten geschnitten; gleiche
|
||||||
|
Idee (Nautobot als Doku-SSoT), andere Dimension. TP44 = nächste freie Nummer nach TP43.
|
||||||
|
- **Modell (native `nautobot.cloud`, Nautobot 3.1.7 — keine eigene App):** `Manufacturer "Cloudflare"` →
|
||||||
|
`CloudAccount` (`393c051f…`) → `CloudResourceType ×6` → `CloudNetwork ×4` (`bf-public`/`fm`/`terpene-db`/
|
||||||
|
`bf-core-mesh`) + `CloudService ×14`; Service↔Tunnel über das Through-Modell
|
||||||
|
`cloud-service-network-assignments` (3 Rows). IDs/Ports/Hostnames/Origins je Objekt in `extra_config`.
|
||||||
|
- **Werkzeug:** `nautobot/tp44-cf-estate-seed.py` — idempotenter Seeder (Re-Run = 26/26 „existiert"),
|
||||||
|
Datenstand inline (Live-CF-Inventur 2026-09-02). Token aus `docs/nas-vlan20/stacks/210-nautobot.yml`.
|
||||||
|
- ✅ **Verifikation 2026-09-06 (live gegen die API):** CloudAccount 1 · ResourceType 6 · CloudNetwork 4 ·
|
||||||
|
CloudService 14 — unverändert seit 09-02.
|
||||||
|
- 🔴 **Gotchas:** `CloudService.cloud_networks` ist `read_only` (nur Through-Rows schreiben/lesen) ·
|
||||||
|
VPC-Service-API unter `connectivity/directory/services` (PUT, voller Body) · **VPC-Objekte tragen ihren
|
||||||
|
Identifier in `service_id`, NICHT `id`** (kostete im Sync einen Fehl-Match) · ⚠️ **DRIFT-KORREKTUR
|
||||||
|
2026-09-10:** die frühere Zeile „Repo-`CF_API_TOKEN` darf kein Workers-VPC → über den `cloudflare`-MCP"
|
||||||
|
war **falsch**. Live getestet liest der `CF_API_TOKEN` aus `~/.secrets/credentials.env` Hyperdrive **und**
|
||||||
|
VPC **und** Workers (alle 200). Der schmale Token ist `CF_DNS_API_TOKEN` (403). Der `cloudflare`-MCP ist
|
||||||
|
der **remote OAuth-MCP** (kein API-Token) und für einen serverseitigen Job ohnehin nicht nutzbar.
|
||||||
|
- ✅ **Etappe 2 Teil A (2026-09-08): IPAM-Prefixe live.** `10.40.0.0/16` (container, Route `bf-core-mesh`) +
|
||||||
|
`10.101.0.0/24` (network, Route `fm`) angelegt via `nautobot/tp44-ipam-prefixes.py`; der Seeder-Re-Run
|
||||||
|
hat die zwei `cloud-network-prefix-assignments` eingehängt (count=2, Idempotenz-Gegenprobe grün).
|
||||||
|
- ✅ **Etappe 2 Teil B (2026-09-10): Live-Sync als Nautobot-Job — DURCH.** Governance (User 2026-09-09):
|
||||||
|
periodische externe→Nautobot-Syncs leben als **Nautobot-Jobs** in `bf-infra/jobs/`, in Nautobot geplant —
|
||||||
|
**kein per-Task-Container/-Cron.** Runtime `jobs/cf_estate_sync.py` (`CFEstateSync`, ORM, read-only Pull,
|
||||||
|
403-tolerant, Verwaisung = `extra_config.stale` statt Löschen). Token = Nautobot-Secret `cf-api-token`
|
||||||
|
(Provider env → `CF_API_TOKEN` am Worker, in `210-nautobot.yml` verdrahtet). **Schedule: täglich 03:00Z**
|
||||||
|
(`ScheduledJob "CF Estate Sync – taeglich"`). Verifiziert: Dry-Run + Commit-Run sauber, **4 Networks /
|
||||||
|
14 Services, alle Endpoints lesbar, 0 Drift**. Der REST-Einmalproof `nautobot/tp44-cf-sync.py` bleibt als
|
||||||
|
workstation-seitiges Diagnose-Werkzeug liegen (nicht die kanonische Runtime).
|
||||||
|
- 🔴 **Drei load-bearing Funde (Detail-Handoff `handoff-20260910-tp44-teilb-sync-live.md`):**
|
||||||
|
(1) **Wurzel-`__init__.py` im Repo ist Pflicht** — Nautobot lädt Git-Repo-Jobs via
|
||||||
|
`pkgutil.walk_packages([GIT_ROOT])`, das Namespace-Packages (ohne `__init__.py`) NICHT entdeckt; ohne die
|
||||||
|
Datei bleibt `bf_infra.jobs` unsichtbar (Symptom: Sync SUCCESS + „No jobs were registered"). bf-infra ist
|
||||||
|
das erste Git-Repo mit `extras.job`. (2) `register_jobs()` gehört ins **Paket-`__init__`**, nicht nur ins
|
||||||
|
Submodul. (3) `Meta.has_sensitive_variables=False` nötig, sonst verweigert Nautobot das Scheduling.
|
||||||
|
- **Beziehung zu TP37:** TP44 hat **TP37 Punkt 3** (CF-Connectoren) vorweggenommen — dieselbe native-Modell-
|
||||||
|
Linie, die TP37 P1+2 (Substrat/Dienste) fortsetzt.
|
||||||
|
|
||||||
|
### TP41 — CF-Access-Reparatur + Zero-Trust-Konsolidierung ▸ ✅ **ABGESCHLOSSEN (2026-09-10)** — Auth-Kern seit 2026-09-01 komplett; **Gelis WARP-Client am 2026-09-10 via Android erfolgreich eingerichtet** (letzter Blocker weg, User-Bestätigung). Optionale Restpunkte (T6 Rate-Limit, geli-CGNAT-Policy-Cleanup) bewusst nicht gezogen; Keystore-Rotation läuft unter TP30 #7.
|
||||||
|
|
||||||
|
**Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google
|
||||||
|
„funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet),
|
||||||
|
App-Session 24h, diverse Ordnungs-/Sicherheitslecks. Vollständiger Verlauf + offene Schritte:
|
||||||
|
**[`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md)**. Branch `tp41-cf-access-repair`.
|
||||||
|
|
||||||
|
**✅ Live erledigt:** Session 720h (Wildcard/vw/Warp-Login) · login_method-Loch zu (reusable Policy
|
||||||
|
`bf-users` = nur `email:christoph`+`email:geli`) · `8e44f5d1` von 4 Apps auf **nur Terpenfinder**
|
||||||
|
entkoppelt · Access-**Gruppen** `zt-humans`/`zt-home`/`zt-automation`, Policies zeigen darauf · **vw voll
|
||||||
|
Bypass** (Lebensader offen) + **`vw/admin` per WAF-Regel öffentlich geblockt** (CF-Edge + Mobilfunk
|
||||||
|
verifiziert) · API-Token auf das vollständige `…4148` konsolidiert (User hat Service-Tokens- + Zero-Trust-
|
||||||
|
Write ergänzt), altes `…2174` revoken.
|
||||||
|
|
||||||
|
**✅ T4 DURCH (2026-09-01, Variante B):** ein Access-Service-Token **`zt-svc`** (`65f1821a`, rotiert) über
|
||||||
|
Gruppe `zt-automation`; Terpene-Worker per `wrangler secret put TERPENE_SVC_CLIENT_ID` auf zt-svc
|
||||||
|
umgestellt (Worker-Pin `index.js:44`); `terpene-verify`+`-v2` **gelöscht**; `test_whoami` grün.
|
||||||
|
**Token-Altlast geklärt:** `b00722` = LIVE Caddy-DNS-Token (LXC130 caddy.env, SSoT), `d47435` = stale
|
||||||
|
Kopie (kein Consumer) → **User revoket `d47435`**; `credentials.env`-Kopie auf b00722 realignt.
|
||||||
|
|
||||||
|
**✅ T3 APPLIED (2026-09-01):** Default-Profil (`0197d1b3`; das custom „Cloudflare Mesh" matcht nur den
|
||||||
|
`warp_connector`) `fallback_domains += emsenhuber.net → [10.40.20.36, 10.40.20.37]`; Tunnel-Route
|
||||||
|
`10.40.0.0/16` (`d1da7c8a`) da, kein `10/8`-Exclude → routbar. ⏳ Client-Verify: WARP neu verbinden.
|
||||||
|
**✅ T2 DURCH (2026-09-01):** WAF-Regel `f26744eb` gelöscht; Caddy `vw`-Block `respond @cf_admin 404` bei
|
||||||
|
Header `CF-Connecting-IP` (LXC130). Verify: Edge `/admin`=404, lokal/WARP `/admin`=200. ✅
|
||||||
|
|
||||||
|
**✅ T5 DURCH (2026-09-01, User-bestätigt):** GitHub-Login läuft; Google-Login läuft nach dem
|
||||||
|
Consent-Screen-Fix (§A des Follow-up-Handoffs: User Type *Internal*→*External*/veröffentlicht).
|
||||||
|
Callback für beide `https://emsenhuber.cloudflareaccess.com/cdn-cgi/access/callback`.
|
||||||
|
**❌ Facebook/Meta für TP41 abgesagt (2026-09-01, User-Entscheid):** optional, kein Bedarf — Geli ist
|
||||||
|
über OTP `angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt. **Keine Facebook-App,
|
||||||
|
kein CF-Facebook-IdP angelegt → nichts aufzuräumen.** Instagram hat ohnehin keinen CF-IdP.
|
||||||
|
**✅ App-Launcher (D1):** je eine Bookmark-Kachel vw/hass/terpenfinder, Wildcard-Kachel raus, Launcher
|
||||||
|
gestylt (Follow-up-Handoff §D, Policies unverändert).
|
||||||
|
|
||||||
|
**✅ Geli in WARP — CF-seitig nichts zu tun (2026-09-01 live geprüft):** Device-Enrollment läuft über
|
||||||
|
die App **„Warp Login App"** (`6c32fe23…`, type `warp`) → Policy `bf-users` → Gruppe **`zt-humans`**, die
|
||||||
|
Gelis beide Mails **schon enthält** → sie **darf bereits enrollen** (deshalb war „Device enrollment
|
||||||
|
permissions" nicht als fehlendes Setting zu finden). Terpenfinder-`geli`-Policy lässt sie per E-Mail durch.
|
||||||
|
✅ **Client am 2026-09-10 via Android erfolgreich eingerichtet** (WARP installiert, Team `emsenhuber`,
|
||||||
|
Login OTP `angelikahy@outlook.de` — User-Bestätigung); damit ist der einzige verbliebene Handgriff erledigt.
|
||||||
|
🔑 **Permission-Frage beantwortet:** CF-seitig **keine** nötig. Für generelle WARP/Enrollment/User-Verwaltung
|
||||||
|
per Account-Token: *Access: Apps and Policies → Edit* + *Access: Organizations, IdPs, and Groups → Edit*
|
||||||
|
(User erteilt 2026-09-01). **Seat-Verwaltung (`/access/seats`) bleibt per MCP gesperrt** — `10405 Method
|
||||||
|
not allowed for this authentication scheme` (Endpoint will User-Token, nicht Account-Token; kein Scope-Fix,
|
||||||
|
für Enroll irrelevant).
|
||||||
|
|
||||||
|
**🔻 Optionale Restpunkte (bewusst nicht gezogen, kein Blocker):** **T6** optional Rate-Limit ·
|
||||||
|
🔴 `TERPENE_ANDROID_KEYSTORE_PW`-Rotation (→ **TP30 Posten #7**, Transkript-Leak) · **Security-Cleanup:**
|
||||||
|
die `geli`-Terpenfinder-Policy hat **OR-Semantik** und enthält den breiten CGNAT-Bereich
|
||||||
|
`94.154.138.0/24` als IP-Include → jeder aus dieser geteilten Range käme **ohne Login** rein. Über WARP ist
|
||||||
|
der Egress ohnehin Cloudflare (nicht diese IP) → der IP-Include wäre dann überflüssig und entfernbar.
|
||||||
|
**d47435-Revoke ✅ · T3-WARP-Client-Verify ✅** (User, 2026-09-01).
|
||||||
|
|
||||||
|
**Abgrenzung:** hass/gitea bleiben (diese Runde) öffentlich; Terpenfinder-Human-Policies nicht angefasst;
|
||||||
|
**Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).
|
||||||
|
- Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.
|
||||||
|
|
||||||
|
### TP43 — dw-Standort-DNS-Uplink (site-spezifische Overrides) ▸ ♻️ **REOPENED 2026-09-11 → Volltext zurück in [`teilprojekte.md`](teilprojekte.md) (Abschnitt TP43)**
|
||||||
|
|
||||||
|
Kurz zwischen 2026-09-11 verworfen und am selben Tag reopened: die 420-Prämisse war gegenstandslos
|
||||||
|
(public CF-Worker, via WARP/TP41 gelöst), aber die **interne** BF-Namensauflösung über WG am dw blieb
|
||||||
|
dabei offen (`portainer.bf.emsenhuber.net` am dw nicht erreichbar). Korrigierter Scope + verworfene
|
||||||
|
420-Historie stehen jetzt aktiv in `teilprojekte.md`. Gitea-Issue #32 = `status:todo` · `thema:firewall-dns`.
|
||||||
|
|
||||||
|
### TP42 — Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net ▸ ✅ **DURCH (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`, `.bf.lan` raus, `.16` (adguard_bf1) abgebaut, Compliance 140/140. Offen nur der an **TP37** gekoppelte Nautobot-Doku-Teil.
|
||||||
|
|
||||||
|
**Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver
|
||||||
|
`10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein
|
||||||
|
zweiter, gedrifteter interner Namensraum: der **Core** trägt in `/ip dns static` 11 Einträge auf
|
||||||
|
`*.bf.lan`, der **Caddyfile** doppelt jeden internen Namen als `*.bf.emsenhuber.net`, und die alten
|
||||||
|
AdGuard `.16`/`.17` antworten noch mit `nginx*.bf.emsenhuber.net` bzw. totem VLAN1 `10.40.41.123`.
|
||||||
|
Christoph: „aus emsenhuber.net wurde schon wieder bf.lan — das müssen wir ein für alle mal aus allen
|
||||||
|
Geräten rausprügeln und in Nautobot festhalten."
|
||||||
|
|
||||||
|
**Ziel:** genau **ein** interner Namensraum `emsenhuber.net` (Split-Horizon `.36/.37 → .40`). `.bf.lan`
|
||||||
|
und `.bf.emsenhuber.net` werden entfernt; Sollzustand in Nautobot dokumentiert.
|
||||||
|
|
||||||
|
**Scope (Reihenfolge = eigene Session):**
|
||||||
|
1. **Bestandsaufnahme** aller Fundstellen: Core `/ip dns static` (`*.bf.lan`), DHCP-Option
|
||||||
|
`domain`/Search-Domain je Netz, Caddyfile-Aliase (`*.bf.emsenhuber.net`), AdGuard-Rewrites `.36/.37`,
|
||||||
|
stale `.16/.17`, Geräte-`identity`.
|
||||||
|
2. **Entscheidung Zielschema** (nur `emsenhuber.net`, ggf. `bf.emsenhuber.net` als *einzige* interne
|
||||||
|
Zone — Christoph-Entscheid) und Migrationsreihenfolge ohne DNS-Loch.
|
||||||
|
3. **Umsetzen:** Core-DNS + DHCP + Caddyfile angleichen; stale `.16/.17` klären/abbauen.
|
||||||
|
4. **Nautobot:** interne Namen/Records als Sollzustand erfassen (Bezug TP37 Datenmodell).
|
||||||
|
|
||||||
|
**Abgrenzung:** kein CF-Public-DNS-Umbau (Zone `emsenhuber.net` öffentlich bleibt); kein Resolver-Swap
|
||||||
|
(`.36/.37` bleiben); `.1`-als-DNS-Reste auf Clients sind separat/tolerierbar (User: „bis jetzt kein
|
||||||
|
Problem"). **Abh.:** TP37 (Nautobot-Datenmodell) für Punkt 4; sonst nichts Offenes.
|
||||||
|
|
||||||
|
#### ✅ Bestandsaufnahme LIVE (2026-09-02) — Drift hat 80 % schon erledigt
|
||||||
|
Gegen die Geräte gemessen (MikroMCP + dig), nicht aus dem Handoff:
|
||||||
|
|
||||||
|
| Fundstelle | Handoff 09-01 | Live 09-02 |
|
||||||
|
|---|---|---|
|
||||||
|
| Core `/ip dns static` | 11× `*.bf.lan` | ✅ **0× `.bf.lan`** — 10× `*.bf.emsenhuber.net` (deckt sich mit GC-Template Z.284–293) |
|
||||||
|
| DHCP `domain` (7 Netze) | „prüfen" | `emsenhuber.net` (GC-Template Z.133 = gleich ⇒ **keine Drift**), DNS `.36/.37` |
|
||||||
|
| `.36/.37` (Soll) | aktiv | `vw.emsenhuber.net→.40` ✅ · `macmini.bf.emsenhuber.net→.20.167` ✅ · **`macmini.emsenhuber.net→leer`** ❌ |
|
||||||
|
| Caddyfile | `*.bf.emsenhuber.net`-Aliase | unverändert, **konsistent** mit Core-Static |
|
||||||
|
| `.16`/`.17` | tot | laufen (:53), liefern totes VLAN1; **kein Core-Objekt** (Firewall/NAT/DNS-Upstream/Addr-List) hängt an `.16` |
|
||||||
|
|
||||||
|
**Kernkonflikt:** DHCP verteilt `emsenhuber.net` (bare), Records leben unter `bf.emsenhuber.net` ⇒ bare `macmini` löst nicht.
|
||||||
|
|
||||||
|
#### ✅ Entscheidungen (User, 2026-09-02)
|
||||||
|
1. **Zielschema = `bf.emsenhuber.net`** (der wenig-Arbeit-Weg): DHCP-domain → `bf.emsenhuber.net`, dann
|
||||||
|
löst `macmini` bare korrekt; Core-Static + Caddyfile bleiben (tragen schon `.bf.`). Öffentliche Zone
|
||||||
|
`emsenhuber.net` bleibt getrennt.
|
||||||
|
2. **`.16` (adguard_bf1) abbauen**, **`.17` (adguard_fm2 = Standort FM) NICHT anfassen.**
|
||||||
|
|
||||||
|
#### ✅ Umsetzung DURCH (2026-09-02)
|
||||||
|
- **(A) DHCP-domain `emsenhuber.net → bf.emsenhuber.net` über GC ✅** — kein Direkt-Write:
|
||||||
|
`mikrotik_routeros.j2` Z.133 gehoben → `push-gc-template.py` (Commit `7a4068c4`, **Nebenbefund:** das
|
||||||
|
Remote-Template hing dem lokalen hinterher — es trug noch `*.bf.lan`+`router.lan`, obwohl Gerät+lokal
|
||||||
|
längst `.bf.emsenhuber.net` fuhren; der Push hat das GC-Repo an den Live-Stand angeglichen) →
|
||||||
|
GitRepo-Sync (SUCCESS) → manueller ConfigPlan **`tp42-dhcp-domain`** (`set [find] domain=…`, weil
|
||||||
|
`add` das Netz doppelt anlegen würde) → Deploy an **crs326-core** → Live **7/7 = bf.emsenhuber.net**,
|
||||||
|
Compliance **140/140, keine Drifts**. Der Job steht dauerhaft in `nautobot/gc-configplan.py`.
|
||||||
|
- **(B) `.16`-Teardown ✅** — `adguard_bf1` lag im geteilten Portainer-Stack **195 `adguard`** (Docker-LXC
|
||||||
|
`.126`, **ep2** — *nicht* ep14) **zusammen mit `adguard_fm2` (`.17`, Standort FM!)**. Deshalb **nicht**
|
||||||
|
Stack löschen, sondern compose auf nur `.17` getrimmt + Stack redeployt (Prune): `.16`-Container weg,
|
||||||
|
`.16:53` tot; **`.17` läuft** (⚠️ ~40 s mit-neugestartet durch den Redeploy — kurzer FM-DNS-Blip,
|
||||||
|
danach sauber). Rollback-compose gesichert. 🧹 Daten-Dirs `/opt/docker/adguard_bf1/*` bleiben als
|
||||||
|
harmlose Leichen auf dem LXC.
|
||||||
|
- **(C) Nautobot-Soll** (Scope-Punkt 4) bleibt an **TP37** gekoppelt (Datenmodell), separat — kein Blocker.
|
||||||
|
|
||||||
|
**Status:** ✅ **Namensraum vereinheitlicht (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`,
|
||||||
|
`.bf.lan` raus, `.16` abgebaut, Compliance 140/140. Offen nur der TP37-gekoppelte Nautobot-Doku-Teil.
|
||||||
|
Handoff: [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
|
||||||
|
|||||||
@@ -2354,17 +2354,6 @@ kann in Ruhe nachziehen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ 🔜 STUB
|
|
||||||
> **Konzept + Architektur-Skizze:** [`docs/handoff-20260827-inventur-und-architektur.md`](handoff-20260827-inventur-und-architektur.md) — bitte dort weiterlesen (Architektur-Abschnitt).
|
|
||||||
- **Scope:** Reverse-Proxy-Hosts werden im **Nautobot-Datenmodell** gepflegt (Site, Host, Backend-Spec). **Golden Config** rendert den Caddyfile aus den Daten, Diff-Check gegen die Live-Version auf `caddy-bf1`, Compliance-Checks + Transaktionen identisch zum bestehenden Workflow für Router-Configs.
|
|
||||||
- **Auslöser (Inventur 2026-08-27):** der Caddyfile (`bf-infra/caddy/Caddyfile`) ist heute **manuell gepflegt** — kein Drift-Check, keine Transaktionen, andere Projekte können keine Sites anlegen ohne Christoph. Lösung: **Nautobot/GC**, kein Caddy-API-Sidecar (kein zweites SSoT).
|
|
||||||
- **Erster Anwendungsfall:** `terpene.teilzeitladen.xyz → http://10.40.20.167:8890` (Mac-mini-Backend, dort PostgREST + sync + web via lokalem Caddy `:8890`). Vor TP34: manueller Quick-Win-Caddy-Block.
|
|
||||||
- **Abh.:** TP22 (GC-Pipeline) ✅ · TP23 (DNS/Caddy-Voraussetzungen) ✅ Stufe 1 · 🔴 **NEU 2026-09-06: TP37 Punkt 1+2** — Caddy rendert *Config aus Daten*, die Backend-Dienste müssen als `ipam.Service` in Nautobot existieren, sonst gibt es nichts zu rendern. **TP37 vor TP34.**
|
|
||||||
- **Datenmodell (entschieden 2026-09-06):** native Modelle — Backend = `ipam.Service`, Proxy-Route = Relationship/CustomField Frontend↔Backend, GC-Job rendert den Caddyfile. Keine eigene App (konsistent zu TP44).
|
|
||||||
- **Status:** 🔜 Stub; wartet auf TP37 P1+2. Quick-Win `terpene.teilzeitladen.xyz → 10.40.20.167:8890` als manueller Caddy-Block ist **Schritt 0** des Runbooks. Plan: [`handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TP35 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard ▸ ⏳ **ANGELEGT 2026-08-28, Schicht 1 (FQDN→IP) + Schicht 3 (globale Regeln) + Schicht 6 (Skill) ✅**
|
### TP35 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard ▸ ⏳ **ANGELEGT 2026-08-28, Schicht 1 (FQDN→IP) + Schicht 3 (globale Regeln) + Schicht 6 (Skill) ✅**
|
||||||
- **Auslöser:** Bestandsaufnahme Session 2026-08-28 (Handoff `docs/handoff-20260828-pre-opencode-migration.md`). Erkenntnis: MCP-Configs sind heute über `~/.config/opencode/opencode.jsonc` UND `~/.claude.json` verstreut, ohne klares Schema.
|
- **Auslöser:** Bestandsaufnahme Session 2026-08-28 (Handoff `docs/handoff-20260828-pre-opencode-migration.md`). Erkenntnis: MCP-Configs sind heute über `~/.config/opencode/opencode.jsonc` UND `~/.claude.json` verstreut, ohne klares Schema.
|
||||||
- **Befund (geprüft):** 11/12 Backups im Verzeichnis `~/dev/.pre-opencode-migration-backups/20260828-150335/` gzip-valid; `uwe-job-bewerbung.tar.gz` korrupt (Source unverändert da). Repos und globale Configs md5-/HEAD-identisch zum Vor-Session-Stand. Synology-Drive-Sync aktiv (Anwender, manuell).
|
- **Befund (geprüft):** 11/12 Backups im Verzeichnis `~/dev/.pre-opencode-migration-backups/20260828-150335/` gzip-valid; `uwe-job-bewerbung.tar.gz` korrupt (Source unverändert da). Repos und globale Configs md5-/HEAD-identisch zum Vor-Session-Stand. Synology-Drive-Sync aktiv (Anwender, manuell).
|
||||||
@@ -2512,157 +2501,6 @@ Der HA-Recorder schreibt **unbegrenzt** — es ist keine `purge_keep_days`-Grenz
|
|||||||
- 🔴 **Kein eigenmächtiger Datenlöschvorgang** — `purge` wirft echte Historie weg; die
|
- 🔴 **Kein eigenmächtiger Datenlöschvorgang** — `purge` wirft echte Historie weg; die
|
||||||
Aufbewahrungsdauer ist eine Nutzerentscheidung. Diese Session hat **nur** vergrössert und gemessen.
|
Aufbewahrungsdauer ist eine Nutzerentscheidung. Diese Session hat **nur** vergrössert und gemessen.
|
||||||
|
|
||||||
### TP41 — CF-Access-Reparatur + Zero-Trust-Konsolidierung ▸ ⏸️ **ZURÜCKGESTELLT 2026-09-01 (User-Entscheid)** — Auth-Kern durch; offen nur Gelis WARP-Client (nicht aus der Ferne machbar), User gibt ggf. später Info
|
|
||||||
|
|
||||||
**Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google
|
|
||||||
„funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet),
|
|
||||||
App-Session 24h, diverse Ordnungs-/Sicherheitslecks. Vollständiger Verlauf + offene Schritte:
|
|
||||||
**[`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md)**. Branch `tp41-cf-access-repair`.
|
|
||||||
|
|
||||||
**✅ Live erledigt:** Session 720h (Wildcard/vw/Warp-Login) · login_method-Loch zu (reusable Policy
|
|
||||||
`bf-users` = nur `email:christoph`+`email:geli`) · `8e44f5d1` von 4 Apps auf **nur Terpenfinder**
|
|
||||||
entkoppelt · Access-**Gruppen** `zt-humans`/`zt-home`/`zt-automation`, Policies zeigen darauf · **vw voll
|
|
||||||
Bypass** (Lebensader offen) + **`vw/admin` per WAF-Regel öffentlich geblockt** (CF-Edge + Mobilfunk
|
|
||||||
verifiziert) · API-Token auf das vollständige `…4148` konsolidiert (User hat Service-Tokens- + Zero-Trust-
|
|
||||||
Write ergänzt), altes `…2174` revoken.
|
|
||||||
|
|
||||||
**✅ T4 DURCH (2026-09-01, Variante B):** ein Access-Service-Token **`zt-svc`** (`65f1821a`, rotiert) über
|
|
||||||
Gruppe `zt-automation`; Terpene-Worker per `wrangler secret put TERPENE_SVC_CLIENT_ID` auf zt-svc
|
|
||||||
umgestellt (Worker-Pin `index.js:44`); `terpene-verify`+`-v2` **gelöscht**; `test_whoami` grün.
|
|
||||||
**Token-Altlast geklärt:** `b00722` = LIVE Caddy-DNS-Token (LXC130 caddy.env, SSoT), `d47435` = stale
|
|
||||||
Kopie (kein Consumer) → **User revoket `d47435`**; `credentials.env`-Kopie auf b00722 realignt.
|
|
||||||
|
|
||||||
**✅ T3 APPLIED (2026-09-01):** Default-Profil (`0197d1b3`; das custom „Cloudflare Mesh" matcht nur den
|
|
||||||
`warp_connector`) `fallback_domains += emsenhuber.net → [10.40.20.36, 10.40.20.37]`; Tunnel-Route
|
|
||||||
`10.40.0.0/16` (`d1da7c8a`) da, kein `10/8`-Exclude → routbar. ⏳ Client-Verify: WARP neu verbinden.
|
|
||||||
**✅ T2 DURCH (2026-09-01):** WAF-Regel `f26744eb` gelöscht; Caddy `vw`-Block `respond @cf_admin 404` bei
|
|
||||||
Header `CF-Connecting-IP` (LXC130). Verify: Edge `/admin`=404, lokal/WARP `/admin`=200. ✅
|
|
||||||
|
|
||||||
**✅ T5 DURCH (2026-09-01, User-bestätigt):** GitHub-Login läuft; Google-Login läuft nach dem
|
|
||||||
Consent-Screen-Fix (§A des Follow-up-Handoffs: User Type *Internal*→*External*/veröffentlicht).
|
|
||||||
Callback für beide `https://emsenhuber.cloudflareaccess.com/cdn-cgi/access/callback`.
|
|
||||||
**❌ Facebook/Meta für TP41 abgesagt (2026-09-01, User-Entscheid):** optional, kein Bedarf — Geli ist
|
|
||||||
über OTP `angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt. **Keine Facebook-App,
|
|
||||||
kein CF-Facebook-IdP angelegt → nichts aufzuräumen.** Instagram hat ohnehin keinen CF-IdP.
|
|
||||||
**✅ App-Launcher (D1):** je eine Bookmark-Kachel vw/hass/terpenfinder, Wildcard-Kachel raus, Launcher
|
|
||||||
gestylt (Follow-up-Handoff §D, Policies unverändert).
|
|
||||||
|
|
||||||
**✅ Geli in WARP — CF-seitig nichts zu tun (2026-09-01 live geprüft):** Device-Enrollment läuft über
|
|
||||||
die App **„Warp Login App"** (`6c32fe23…`, type `warp`) → Policy `bf-users` → Gruppe **`zt-humans`**, die
|
|
||||||
Gelis beide Mails **schon enthält** → sie **darf bereits enrollen** (deshalb war „Device enrollment
|
|
||||||
permissions" nicht als fehlendes Setting zu finden). Terpenfinder-`geli`-Policy lässt sie per E-Mail durch.
|
|
||||||
⇒ verbleibt **nur ein Handgriff auf Gelis Gerät** (WARP installieren, Team `emsenhuber`, Login OTP
|
|
||||||
`angelikahy@outlook.de` / Google `pillepalle670@gmail.com`) — nicht aus der Ferne machbar.
|
|
||||||
🔑 **Permission-Frage beantwortet:** CF-seitig **keine** nötig. Für generelle WARP/Enrollment/User-Verwaltung
|
|
||||||
per Account-Token: *Access: Apps and Policies → Edit* + *Access: Organizations, IdPs, and Groups → Edit*
|
|
||||||
(User erteilt 2026-09-01). **Seat-Verwaltung (`/access/seats`) bleibt per MCP gesperrt** — `10405 Method
|
|
||||||
not allowed for this authentication scheme` (Endpoint will User-Token, nicht Account-Token; kein Scope-Fix,
|
|
||||||
für Enroll irrelevant).
|
|
||||||
|
|
||||||
**🔜 Offen — nur noch außerhalb CF:** **Gelis WARP-Client** auf ihrem Gerät (s.o.) · **T6** optional
|
|
||||||
Rate-Limit · 🔴 `TERPENE_ANDROID_KEYSTORE_PW`-Rotation erwägen (TP30 Posten #7, Transkript-Leak).
|
|
||||||
**d47435-Revoke ✅ · T3-WARP-Client-Verify ✅** (User, 2026-09-01).
|
|
||||||
|
|
||||||
**🔻 Optionaler Security-Cleanup (eigener Punkt, nicht in dieser Runde):** die `geli`-Terpenfinder-Policy
|
|
||||||
hat **OR-Semantik** und enthält den breiten CGNAT-Bereich `94.154.138.0/24` als IP-Include → jeder aus
|
|
||||||
dieser geteilten Range käme **ohne Login** rein. Über WARP ist der Egress ohnehin Cloudflare (nicht diese
|
|
||||||
IP) → der IP-Include wäre dann überflüssig und entfernbar.
|
|
||||||
|
|
||||||
**Abgrenzung:** hass/gitea bleiben (diese Runde) öffentlich; Terpenfinder-Human-Policies nicht angefasst;
|
|
||||||
**Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).
|
|
||||||
- Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.
|
|
||||||
|
|
||||||
### TP42 — Namens-Vereinheitlichung bf.lan → emsenhuber.net ▸ 🔜 ANGELEGT 2026-09-01 (User-Okay erteilt)
|
|
||||||
|
|
||||||
**Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver
|
|
||||||
`10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein
|
|
||||||
zweiter, gedrifteter interner Namensraum: der **Core** trägt in `/ip dns static` 11 Einträge auf
|
|
||||||
`*.bf.lan`, der **Caddyfile** doppelt jeden internen Namen als `*.bf.emsenhuber.net`, und die alten
|
|
||||||
AdGuard `.16`/`.17` antworten noch mit `nginx*.bf.emsenhuber.net` bzw. totem VLAN1 `10.40.41.123`.
|
|
||||||
Christoph: „aus emsenhuber.net wurde schon wieder bf.lan — das müssen wir ein für alle mal aus allen
|
|
||||||
Geräten rausprügeln und in Nautobot festhalten."
|
|
||||||
|
|
||||||
**Ziel:** genau **ein** interner Namensraum `emsenhuber.net` (Split-Horizon `.36/.37 → .40`). `.bf.lan`
|
|
||||||
und `.bf.emsenhuber.net` werden entfernt; Sollzustand in Nautobot dokumentiert.
|
|
||||||
|
|
||||||
**Scope (Reihenfolge = eigene Session):**
|
|
||||||
1. **Bestandsaufnahme** aller Fundstellen: Core `/ip dns static` (`*.bf.lan`), DHCP-Option
|
|
||||||
`domain`/Search-Domain je Netz, Caddyfile-Aliase (`*.bf.emsenhuber.net`), AdGuard-Rewrites `.36/.37`,
|
|
||||||
stale `.16/.17`, Geräte-`identity`.
|
|
||||||
2. **Entscheidung Zielschema** (nur `emsenhuber.net`, ggf. `bf.emsenhuber.net` als *einzige* interne
|
|
||||||
Zone — Christoph-Entscheid) und Migrationsreihenfolge ohne DNS-Loch.
|
|
||||||
3. **Umsetzen:** Core-DNS + DHCP + Caddyfile angleichen; stale `.16/.17` klären/abbauen.
|
|
||||||
4. **Nautobot:** interne Namen/Records als Sollzustand erfassen (Bezug TP37 Datenmodell).
|
|
||||||
|
|
||||||
**Abgrenzung:** kein CF-Public-DNS-Umbau (Zone `emsenhuber.net` öffentlich bleibt); kein Resolver-Swap
|
|
||||||
(`.36/.37` bleiben); `.1`-als-DNS-Reste auf Clients sind separat/tolerierbar (User: „bis jetzt kein
|
|
||||||
Problem"). **Abh.:** TP37 (Nautobot-Datenmodell) für Punkt 4; sonst nichts Offenes.
|
|
||||||
|
|
||||||
#### ✅ Bestandsaufnahme LIVE (2026-09-02) — Drift hat 80 % schon erledigt
|
|
||||||
Gegen die Geräte gemessen (MikroMCP + dig), nicht aus dem Handoff:
|
|
||||||
|
|
||||||
| Fundstelle | Handoff 09-01 | Live 09-02 |
|
|
||||||
|---|---|---|
|
|
||||||
| Core `/ip dns static` | 11× `*.bf.lan` | ✅ **0× `.bf.lan`** — 10× `*.bf.emsenhuber.net` (deckt sich mit GC-Template Z.284–293) |
|
|
||||||
| DHCP `domain` (7 Netze) | „prüfen" | `emsenhuber.net` (GC-Template Z.133 = gleich ⇒ **keine Drift**), DNS `.36/.37` |
|
|
||||||
| `.36/.37` (Soll) | aktiv | `vw.emsenhuber.net→.40` ✅ · `macmini.bf.emsenhuber.net→.20.167` ✅ · **`macmini.emsenhuber.net→leer`** ❌ |
|
|
||||||
| Caddyfile | `*.bf.emsenhuber.net`-Aliase | unverändert, **konsistent** mit Core-Static |
|
|
||||||
| `.16`/`.17` | tot | laufen (:53), liefern totes VLAN1; **kein Core-Objekt** (Firewall/NAT/DNS-Upstream/Addr-List) hängt an `.16` |
|
|
||||||
|
|
||||||
**Kernkonflikt:** DHCP verteilt `emsenhuber.net` (bare), Records leben unter `bf.emsenhuber.net` ⇒ bare `macmini` löst nicht.
|
|
||||||
|
|
||||||
#### ✅ Entscheidungen (User, 2026-09-02)
|
|
||||||
1. **Zielschema = `bf.emsenhuber.net`** (der wenig-Arbeit-Weg): DHCP-domain → `bf.emsenhuber.net`, dann
|
|
||||||
löst `macmini` bare korrekt; Core-Static + Caddyfile bleiben (tragen schon `.bf.`). Öffentliche Zone
|
|
||||||
`emsenhuber.net` bleibt getrennt.
|
|
||||||
2. **`.16` (adguard_bf1) abbauen**, **`.17` (adguard_fm2 = Standort FM) NICHT anfassen.**
|
|
||||||
|
|
||||||
#### ✅ Umsetzung DURCH (2026-09-02)
|
|
||||||
- **(A) DHCP-domain `emsenhuber.net → bf.emsenhuber.net` über GC ✅** — kein Direkt-Write:
|
|
||||||
`mikrotik_routeros.j2` Z.133 gehoben → `push-gc-template.py` (Commit `7a4068c4`, **Nebenbefund:** das
|
|
||||||
Remote-Template hing dem lokalen hinterher — es trug noch `*.bf.lan`+`router.lan`, obwohl Gerät+lokal
|
|
||||||
längst `.bf.emsenhuber.net` fuhren; der Push hat das GC-Repo an den Live-Stand angeglichen) →
|
|
||||||
GitRepo-Sync (SUCCESS) → manueller ConfigPlan **`tp42-dhcp-domain`** (`set [find] domain=…`, weil
|
|
||||||
`add` das Netz doppelt anlegen würde) → Deploy an **crs326-core** → Live **7/7 = bf.emsenhuber.net**,
|
|
||||||
Compliance **140/140, keine Drifts**. Der Job steht dauerhaft in `nautobot/gc-configplan.py`.
|
|
||||||
- **(B) `.16`-Teardown ✅** — `adguard_bf1` lag im geteilten Portainer-Stack **195 `adguard`** (Docker-LXC
|
|
||||||
`.126`, **ep2** — *nicht* ep14) **zusammen mit `adguard_fm2` (`.17`, Standort FM!)**. Deshalb **nicht**
|
|
||||||
Stack löschen, sondern compose auf nur `.17` getrimmt + Stack redeployt (Prune): `.16`-Container weg,
|
|
||||||
`.16:53` tot; **`.17` läuft** (⚠️ ~40 s mit-neugestartet durch den Redeploy — kurzer FM-DNS-Blip,
|
|
||||||
danach sauber). Rollback-compose gesichert. 🧹 Daten-Dirs `/opt/docker/adguard_bf1/*` bleiben als
|
|
||||||
harmlose Leichen auf dem LXC.
|
|
||||||
- **(C) Nautobot-Soll** (Scope-Punkt 4) bleibt an **TP37** gekoppelt (Datenmodell), separat — kein Blocker.
|
|
||||||
|
|
||||||
**Status:** ✅ **Namensraum vereinheitlicht (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`,
|
|
||||||
`.bf.lan` raus, `.16` abgebaut, Compliance 140/140. Offen nur der TP37-gekoppelte Nautobot-Doku-Teil.
|
|
||||||
Handoff: [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
|
|
||||||
|
|
||||||
### TP43 — dw-Standort-DNS-Uplink (site-spezifische Overrides) ▸ 🔜 ANGELEGT 2026-09-01 (User-Okay erteilt)
|
|
||||||
|
|
||||||
**Auslöser (2026-09-01, TP41-Follow-up):** Geli soll `420.teilzeitladen.xyz` am dw-Standort erreichen.
|
|
||||||
Split-Horizon am dw scheitert an der **FritzBox** (`10.95.10.1`): kein Per-Host-A-Override im Standard
|
|
||||||
(nur DNS-Rebind-Ausnahmen ohne IP). Christoph: *„keine Chance im FritzBox-Standard? … lieber ein TP als
|
|
||||||
uns verfranzen. Geli kommt rein, das zählt."*
|
|
||||||
|
|
||||||
**Ziel:** dem dw-Standort einen **eigenen Resolver/DNS-Uplink** geben, der **site-spezifische Overrides**
|
|
||||||
hält (erster Fall `420.teilzeitladen.xyz` → BF-Caddy über WG), als DNS der FritzBox/Clients gesetzt.
|
|
||||||
|
|
||||||
**Kernfrage (explizit):** Verhalten bei **WG-Disconnect.** Ein Override auf eine BF-interne IP
|
|
||||||
(`10.40.20.40`) fällt aus, wenn der Tunnel unten ist. Für einen Login-gegateten Dienst gibt es kein
|
|
||||||
„kein Login **und** funktioniert bei WG-down" — Optionen: (a) Override nur bei WG-up (Failover-Resolver),
|
|
||||||
(b) öffentlicher Pfad (CF, mit Login) als Fallback. Zu entwerfen.
|
|
||||||
|
|
||||||
**Sofortlösung (nicht Teil von TP43):** Geli per **WARP** (Session statt täglichem Login, CGNAT egal,
|
|
||||||
degradiert sauber: WG-up = nahtlos, WG-down = öffentliche URL + Login).
|
|
||||||
|
|
||||||
**BF-seitige Prereqs:** Caddy-vhost `420.teilzeitladen.xyz` → macmini-Worker (`10.40.20.167:8890`) mit
|
|
||||||
Cert (DNS-01, `…4148` hat teilzeitladen.xyz-DNS) · WG-Route dw→`10.40.20.40` · Split-Horizon für
|
|
||||||
`420.teilzeitladen.xyz` (heute liefern auch `.36/.37` public). **Vorher verifizieren:** FritzOS-Version +
|
|
||||||
Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff:
|
|
||||||
[`docs/handoff-20260901-tp41-followup.md`](handoff-20260901-tp41-followup.md).
|
|
||||||
|
|
||||||
**Status:** 🔜 angelegt 2026-09-01, eigene Session.
|
|
||||||
|
|
||||||
### TP37 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren ▸ 🔜 ANGELEGT 2026-08-30 (User-Okay erteilt)
|
### TP37 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren ▸ 🔜 ANGELEGT 2026-08-30 (User-Okay erteilt)
|
||||||
> **Christophs Formulierung (2026-08-30):** *„können wir das setup mit den cf-connectoren dann auch in
|
> **Christophs Formulierung (2026-08-30):** *„können wir das setup mit den cf-connectoren dann auch in
|
||||||
> nautobot abbilden. quasi eine durchgehende netzwerkdoku stück für stück. muss eh fast sein, da du auf
|
> nautobot abbilden. quasi eine durchgehende netzwerkdoku stück für stück. muss eh fast sein, da du auf
|
||||||
@@ -2687,7 +2525,7 @@ Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff:
|
|||||||
„wo läuft was" steht.
|
„wo läuft was" steht.
|
||||||
2. **Dienste abbilden** — `ipam.Service` je exponiertem Port (Caddy `:80/:443`, AdGuard `:53`,
|
2. **Dienste abbilden** — `ipam.Service` je exponiertem Port (Caddy `:80/:443`, AdGuard `:53`,
|
||||||
Nautobot `:8080`, n8n `:5678`, …). Das ist Nautobot-Kernmodell, **keine eigene App nötig**.
|
Nautobot `:8080`, n8n `:5678`, …). Das ist Nautobot-Kernmodell, **keine eigene App nötig**.
|
||||||
3. ✅ **CF-Connectoren — ERLEDIGT via [TP44](#tp44) (Etappe 1, 2026-09-02).** Die Modellwahl ist damit
|
3. ✅ **CF-Connectoren — ERLEDIGT via [TP44](teilprojekte-archiv.md#tp44) (Etappe 1, 2026-09-02).** Die Modellwahl ist damit
|
||||||
entschieden: **native `nautobot.cloud`-Modelle** (Tunnel/WARP = `CloudNetwork`, VPC/Hyperdrive/Worker/
|
entschieden: **native `nautobot.cloud`-Modelle** (Tunnel/WARP = `CloudNetwork`, VPC/Hyperdrive/Worker/
|
||||||
Access-App = `CloudService`, IDs/Ports/Origins in `extra_config`), **keine eigene App**. Offen nur noch
|
Access-App = `CloudService`, IDs/Ports/Origins in `extra_config`), **keine eigene App**. Offen nur noch
|
||||||
TP44 Etappe 2 (CF-API→Nautobot-Sync). ⇒ **Punkt 1+2 unten sind der echte Rest von TP37.**
|
TP44 Etappe 2 (CF-API→Nautobot-Sync). ⇒ **Punkt 1+2 unten sind der echte Rest von TP37.**
|
||||||
@@ -2716,35 +2554,6 @@ Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a id="tp44"></a>
|
|
||||||
### TP44 — Cloudflare-Estate als Dokumentations-SSoT in Nautobot ▸ ⏳ **Etappe 1 ✅ live (2026-09-02), Quick-Index nachgezogen 2026-09-06**
|
|
||||||
> **Entscheid (User, 2026-09-02):** die volle CF-Estate (Tunnel + WARP-Connector + VPC-Service +
|
|
||||||
> Hyperdrive + Workers + alle Access-Apps) als strukturierte, abfragbare SSoT in Nautobot abbilden —
|
|
||||||
> **etappenweise: manuell jetzt, Sync später.** Nautobot kann CF nicht steuern → „SSoT" heißt
|
|
||||||
> **Dokumentations-SSoT; CF bleibt operativer Master.** Detail-Handoff:
|
|
||||||
> [`handoff-20260902-cf-estate-nautobot-ssot.md`](handoff-20260902-cf-estate-nautobot-ssot.md).
|
|
||||||
|
|
||||||
- **Warum eigenes TP, nicht TP36:** TP36 ist eng auf `teilprojekte.md`-Live-Daten geschnitten; gleiche
|
|
||||||
Idee (Nautobot als Doku-SSoT), andere Dimension. TP44 = nächste freie Nummer nach TP43.
|
|
||||||
- **Modell (native `nautobot.cloud`, Nautobot 3.1.7 — keine eigene App):** `Manufacturer "Cloudflare"` →
|
|
||||||
`CloudAccount` (`393c051f…`) → `CloudResourceType ×6` → `CloudNetwork ×4` (`bf-public`/`fm`/`terpene-db`/
|
|
||||||
`bf-core-mesh`) + `CloudService ×14`; Service↔Tunnel über das Through-Modell
|
|
||||||
`cloud-service-network-assignments` (3 Rows). IDs/Ports/Hostnames/Origins je Objekt in `extra_config`.
|
|
||||||
- **Werkzeug:** `nautobot/tp44-cf-estate-seed.py` — idempotenter Seeder (Re-Run = 26/26 „existiert"),
|
|
||||||
Datenstand inline (Live-CF-Inventur 2026-09-02). Token aus `docs/nas-vlan20/stacks/210-nautobot.yml`.
|
|
||||||
- ✅ **Verifikation 2026-09-06 (live gegen die API):** CloudAccount 1 · ResourceType 6 · CloudNetwork 4 ·
|
|
||||||
CloudService 14 — unverändert seit 09-02.
|
|
||||||
- 🔴 **Gotchas für Etappe 2:** `CloudService.cloud_networks` ist `read_only` (nur Through-Rows schreiben/
|
|
||||||
lesen) · VPC-Service-API unter `connectivity/directory/services` (PUT, voller Body) · Repo-`CF_API_TOKEN`
|
|
||||||
darf kein Workers-VPC → dort über den `cloudflare`-MCP-Credential.
|
|
||||||
- **Offen (Etappe 2, eigene Session):** (1) CF-API→Nautobot-Sync (Python/n8n, analog golden-config-bot) →
|
|
||||||
driftfrei; (2) IPAM-Prefixe `10.40.0.0/16` + `10.101.0.0/24` anlegen, dann greifen die Prefix-Assignments
|
|
||||||
automatisch.
|
|
||||||
- **Beziehung zu TP37:** TP44 hat **TP37 Punkt 3** (CF-Connectoren) vorweggenommen — dieselbe native-Modell-
|
|
||||||
Linie, die TP37 P1+2 (Substrat/Dienste) fortsetzt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Abgeschlossene Teilprojekte ▸ [`teilprojekte-archiv.md`](teilprojekte-archiv.md)
|
## Abgeschlossene Teilprojekte ▸ [`teilprojekte-archiv.md`](teilprojekte-archiv.md)
|
||||||
|
|
||||||
**34 abgeschlossene TPs** liegen dort im Volltext, unverändert und mit eigenem Inhaltsverzeichnis.
|
**34 abgeschlossene TPs** liegen dort im Volltext, unverändert und mit eigenem Inhaltsverzeichnis.
|
||||||
@@ -2761,7 +2570,7 @@ Kurzorientierung, wo was zu finden ist:
|
|||||||
| **Firewall / QoS** | [TP5](teilprojekte-archiv.md#tp5) deny-by-default · [TP26](teilprojekte-archiv.md#tp26) `ip-firewall-filter` unter Drift-Kontrolle · [TP6](teilprojekte-archiv.md#tp6) CAKE *(geschlossen statt gebaut)* · [TP6.1](teilprojekte-archiv.md#tp6-1) Upload-Priorisierung |
|
| **Firewall / QoS** | [TP5](teilprojekte-archiv.md#tp5) deny-by-default · [TP26](teilprojekte-archiv.md#tp26) `ip-firewall-filter` unter Drift-Kontrolle · [TP6](teilprojekte-archiv.md#tp6) CAKE *(geschlossen statt gebaut)* · [TP6.1](teilprojekte-archiv.md#tp6-1) Upload-Priorisierung |
|
||||||
| **Discovery / Media** | [TP7](teilprojekte-archiv.md#tp7) mDNS · [TP7.1](teilprojekte-archiv.md#tp7-1) SSDP-Relay · [TP7.2](teilprojekte-archiv.md#tp7-2) HA↔Sonos |
|
| **Discovery / Media** | [TP7](teilprojekte-archiv.md#tp7) mDNS · [TP7.1](teilprojekte-archiv.md#tp7-1) SSDP-Relay · [TP7.2](teilprojekte-archiv.md#tp7-2) HA↔Sonos |
|
||||||
| **Hosts / Migration** | [TP14](teilprojekte-archiv.md#tp14) (+[.1](teilprojekte-archiv.md#tp14-1)/[.2](teilprojekte-archiv.md#tp14-2)/[.2a](teilprojekte-archiv.md#tp14-2a)/[.3](teilprojekte-archiv.md#tp14-3)/[.5](teilprojekte-archiv.md#tp14-5)) VLAN20-Cutover · [TP17](teilprojekte-archiv.md#tp17) Shelly→VLAN30 · [TP19](teilprojekte-archiv.md#tp19) VLAN1-Retirement |
|
| **Hosts / Migration** | [TP14](teilprojekte-archiv.md#tp14) (+[.1](teilprojekte-archiv.md#tp14-1)/[.2](teilprojekte-archiv.md#tp14-2)/[.2a](teilprojekte-archiv.md#tp14-2a)/[.3](teilprojekte-archiv.md#tp14-3)/[.5](teilprojekte-archiv.md#tp14-5)) VLAN20-Cutover · [TP17](teilprojekte-archiv.md#tp17) Shelly→VLAN30 · [TP19](teilprojekte-archiv.md#tp19) VLAN1-Retirement |
|
||||||
| **Control-Plane** | [TP20](teilprojekte-archiv.md#tp20) Platform-Postgres · [TP21](teilprojekte-archiv.md#tp21) Nautobot GC · [TP22](teilprojekte-archiv.md#tp22) Adoption + Compliance-Push · [TP27](teilprojekte-archiv.md#tp27) RouterBOOT 7.24 · [TP-R](teilprojekte-archiv.md#tp-r) Capabilities-Sweep |
|
| **Control-Plane** | [TP20](teilprojekte-archiv.md#tp20) Platform-Postgres · [TP21](teilprojekte-archiv.md#tp21) Nautobot GC · [TP22](teilprojekte-archiv.md#tp22) Adoption + Compliance-Push · [TP27](teilprojekte-archiv.md#tp27) RouterBOOT 7.24 · [TP-R](teilprojekte-archiv.md#tp-r) Capabilities-Sweep · [TP44](teilprojekte-archiv.md#tp44) CF-Estate Doku-SSoT |
|
||||||
| **Zugang** | [TP10](teilprojekte-archiv.md#tp10) Multi-Site-VPN *(offen nur der DDNS-Updater, s. Follow-ups)* · [TP16.1](teilprojekte-archiv.md#tp16-1) natives CF-Mesh |
|
| **Zugang** | [TP10](teilprojekte-archiv.md#tp10) Multi-Site-VPN *(offen nur der DDNS-Updater, s. Follow-ups)* · [TP16.1](teilprojekte-archiv.md#tp16-1) natives CF-Mesh |
|
||||||
| **Verworfen** | [TP9](teilprojekte-archiv.md#tp9) Gateway-HA (User-Entscheid 2026-08-08) |
|
| **Verworfen** | [TP9](teilprojekte-archiv.md#tp9) Gateway-HA (User-Entscheid 2026-08-08) |
|
||||||
|
|
||||||
@@ -2916,3 +2725,78 @@ TP22 · TP-R · **TP3.2** (08-17) · **TP5** (08-18) · **TP7.1** (08-18) · **T
|
|||||||
7. **Foundation (2026-07-14):** **TP15** (MikroTik-Switching / Omada-WLAN, 2× CRS326 bestellt) ∥ **TP20 (Platform-Postgres, single-site replication-ready)** → **TP21 (Nautobot Golden Config)** ∥ **TP20.1 (Vaultwarden→PG + 2-Site-HA)**. TP20 hängt an **TP18 ①** (Garage-Bucket für pgBackRest).
|
7. **Foundation (2026-07-14):** **TP15** (MikroTik-Switching / Omada-WLAN, 2× CRS326 bestellt) ∥ **TP20 (Platform-Postgres, single-site replication-ready)** → **TP21 (Nautobot Golden Config)** ∥ **TP20.1 (Vaultwarden→PG + 2-Site-HA)**. TP20 hängt an **TP18 ①** (Garage-Bucket für pgBackRest).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
### TP43 — dw-Standort: interne BF-DNS-Namen über WG auflösbar machen ▸ ✅ **UMGESETZT 2026-09-11** — dw-FritzBox: Upstream `.36`+`.37`, DNS-Rebind-Ausnahme `bf.emsenhuber.net`, Fallback-Haken „öffentliche NS bei Unreachbarkeit" **gesetzt** (Ideal-Variante: `.37`-Redundanz bleibt, BF-down → public). (verworfene 420-Historie am Ende dieses Abschnitts)
|
||||||
|
|
||||||
|
**Live-Auslöser (2026-09-11):** Christoph am dw-Standort, WG-Tunnel zu BF up, kann `portainer.bf.emsenhuber.net`
|
||||||
|
nicht erreichen. Direkte IP `https://10.40.20.126:9443` geht ⇒ **reines DNS-, kein Routing-Problem.** Der
|
||||||
|
dw-Client nutzt die FritzBox (`10.95.10.1`) als Resolver; die kennt die interne Zone `bf.emsenhuber.net`
|
||||||
|
(Split-Horizon `.36/.37 → 10.40.20.40` Caddy) nicht.
|
||||||
|
|
||||||
|
**Korrektur gegenüber dem verworfenen Scope:** Der ursprüngliche Fall (`420.teilzeitladen.xyz`) ist ein
|
||||||
|
**öffentlicher** CF-Worker und war nie das Problem — der ist via TP41/WARP + CF Access gelöst. Das echte,
|
||||||
|
offene Problem sind **interne** BF-Namen (`*.bf.emsenhuber.net`) über den WG-Tunnel.
|
||||||
|
|
||||||
|
**Live verifiziert (2026-09-11, `dig @10.40.20.36` über den Tunnel):** `portainer.bf.emsenhuber.net
|
||||||
|
→ 10.40.20.40` (Caddy), `macmini.bf.emsenhuber.net → 10.40.20.167` — alle internen bf-Namen landen
|
||||||
|
in `10.40.0.0/16`, über WG erreichbar. `ds-fm`/`gateway-fm.emsenhuber.net → 188.114.96.10/.97.10`
|
||||||
|
= **Cloudflare-Anycast** (public), **nie `10.101.0.x`**. ⇒ **Der fm-Einwand (10.101.x intern ohne
|
||||||
|
Pfad von dw) trifft für `.36/.37` nicht zu** — fm läuft über den public `fm.emsenhuber.net`-Tunnel,
|
||||||
|
nicht über interne Records. (WG-Topologie TP10: dw-Peer AllowedIPs BF-seitig = `10.40.0.0/16`, dw
|
||||||
|
routet **nicht** nach `10.101.0.0/24`.)
|
||||||
|
|
||||||
|
**Constraint am dw:** die 24/7-Geräte `10.95.10.246/.247` sind **Cudy-APs (kein DNS-Host)**; die
|
||||||
|
FritzBox kann **kein Per-Zone-Forwarding** (nur einen globalen Upstream) **und per DHCP nur EINE
|
||||||
|
DNS-IP ausliefern** (⇒ „mehrere NS an die Clients" scheidet aus). Ein echter conditional forwarder
|
||||||
|
(dnsmasq `server=/bf.emsenhuber.net/.36`) bräuchte einen Zusatz-Dauerläufer — abgelehnt.
|
||||||
|
|
||||||
|
**Entscheidung (Christoph, 2026-09-11): FB-Upstream + Rebind-Ausnahme.** Clients bekommen per DHCP
|
||||||
|
`10.95.10.1` (die FB selbst, Default); die **FB forwardet an die BF-Resolver**: *DNS-Server → „Andere
|
||||||
|
DNSv4-Server"* Bevorzugt **`10.40.20.36`**, Alternativ **`10.40.20.37`** (beide BF, beide
|
||||||
|
Voll-Resolver → Redundanz bleibt). Sauberer als der zwischenzeitliche DHCP-Multi-Gedanke: die FB ist
|
||||||
|
der **einzige** Client-Resolver und `.36` löst **intern und public** auf → alles geht zuerst an `.36`,
|
||||||
|
public wird nur als Unreachable-Fallback konsultiert, **nie „zuerst"** → kein NXDOMAIN-Leak im
|
||||||
|
Normalbetrieb.
|
||||||
|
|
||||||
|
🔴 **DNS-Rebind-Schutz-Ausnahme `bf.emsenhuber.net`** (*Heimnetz → Netzwerk → Netzwerkeinstellungen →
|
||||||
|
DNS-Rebind-Schutz*) ist **zwingend**: sonst filtert die FB die Privat-IP-Antworten (`10.40.x`) als
|
||||||
|
Rebind-Angriff weg → bf-Namen liefern nichts, obwohl `.36` korrekt antwortet. Die Ausnahme ist ein
|
||||||
|
reines Namens-Whitelist (keine IP).
|
||||||
|
|
||||||
|
**BF-down-Fallback (Verzweigung am FRITZ!OS-Stand):** Christoph erinnert ein Setting „öffentliche NS
|
||||||
|
verwenden, wenn die primären nicht erreichbar sind". Vorhanden → aktivieren, Upstream bleibt
|
||||||
|
`.36`+`.37`, BF-down geht auf public. Fehlt → Alternativ-Slot statt `.37` auf **public `9.9.9.9`**
|
||||||
|
(kostet `.37`-Redundanz); dann zusätzlich prüfen, dass die FB **strikt primär** fragt (nicht
|
||||||
|
round-robbt), sonst NXDOMAIN-Leak für `bf.*` bei BF-up.
|
||||||
|
|
||||||
|
- **BF-up:** FB → `.36` → interne `bf.*` (→ `.40`, dank Rebind-Ausnahme durchgereicht) **und** public.
|
||||||
|
- **BF-down:** `.36`/`.37` unreachable → public-Fallback → public lebt, `bf.*` = NXDOMAIN (erwartet).
|
||||||
|
|
||||||
|
**Bewusst akzeptierter Tradeoff:** bei BF-up läuft Geli's **gesamter** Haushalt-DNS über Linz
|
||||||
|
(`.36`/AdGuard-Filter) — vermeidbar nur mit conditional forwarder (Zusatz-HW, abgelehnt).
|
||||||
|
|
||||||
|
**On-site-Verifikationen (Christoph, am dw):** (1) Rebind-Ausnahme greift — `nslookup
|
||||||
|
portainer.bf.emsenhuber.net` über die FB → stabil `10.40.20.40`; (2) BF-down-Fallback vorhanden
|
||||||
|
(Setting) oder via public-Alternativ-Slot gesetzt; (3) nur falls BF+public gemischt: FB fragt strikt
|
||||||
|
primär. Rückfall pro Gerät: `/etc/hosts` `10.40.20.40 <name>` oder WARP `fallback_domains
|
||||||
|
bf.emsenhuber.net → [.36,.37]` (TP41-Präzedenz).
|
||||||
|
|
||||||
|
**Sofort-Workaround (läuft unabhängig):** `https://10.40.20.126:9443` direkt bzw.
|
||||||
|
`/etc/hosts`-Zeile `10.40.20.40 portainer.bf.emsenhuber.net`.
|
||||||
|
|
||||||
|
**Abhängigkeiten:** WG-Route dw→`10.40.0.0/16` (steht). Grenzt an TP42 (interner Namensraum
|
||||||
|
`bf.emsenhuber.net`) und TP10 (Multi-Site-VPN). **Umsetzung = eigene On-site-Session** (FritzBox-DHCP
|
||||||
|
ist Christophs manueller Schritt, kein Tool-Zugriff).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Historie — verworfener 420-Scope (angelegt 2026-09-01, verworfen 2026-09-11):**
|
||||||
|
|
||||||
|
**Auslöser (2026-09-01, TP41-Follow-up):** Geli soll `420.teilzeitladen.xyz` am dw-Standort erreichen.
|
||||||
|
Split-Horizon am dw scheitert an der **FritzBox** (`10.95.10.1`): kein Per-Host-A-Override im Standard
|
||||||
|
(nur DNS-Rebind-Ausnahmen ohne IP). Christoph: *„keine Chance im FritzBox-Standard? … lieber ein TP als
|
||||||
|
uns verfranzen. Geli kommt rein, das zählt."* — Diese Prämisse fiel weg: `420.teilzeitladen.xyz` ist ein
|
||||||
|
public CF-Worker hinter CF Access (löst auf CF-Anycast), kein BF-Origin ⇒ dafür braucht es kein
|
||||||
|
Split-Horizon; Zugang via TP41-**WARP** gelöst (Geli 2026-09-10 via Android). Der Override-Bedarf für
|
||||||
|
**interne** Namen blieb dabei übersehen — deshalb reopened, s.o.
|
||||||
|
|||||||
14
jobs/__init__.py
Normal file
14
jobs/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""bf-infra Nautobot-Jobs (Git-Datenquelle).
|
||||||
|
|
||||||
|
Nautobot importiert nur dieses Paket; damit die enthaltenen Jobs sich
|
||||||
|
registrieren, muss jedes Job-Modul hier importiert werden. Neue periodische
|
||||||
|
externe->Nautobot-Syncs kommen als weiteres Modul dazu (kein per-Task-Container).
|
||||||
|
"""
|
||||||
|
from nautobot.apps.jobs import register_jobs
|
||||||
|
|
||||||
|
from .cf_estate_sync import CFEstateSync
|
||||||
|
|
||||||
|
# Nautobot importiert nur dieses Paket (bf_infra.jobs) und erwartet register_jobs()
|
||||||
|
# HIER im __init__ (nicht im Submodul) — sonst: "No jobs were registered on loading
|
||||||
|
# the bf_infra.jobs submodule". Neue Jobs: Klasse importieren + hier mitregistrieren.
|
||||||
|
register_jobs(CFEstateSync)
|
||||||
267
jobs/cf_estate_sync.py
Normal file
267
jobs/cf_estate_sync.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
"""
|
||||||
|
TP44 Teil B — CF-Estate → Nautobot, als geplanter Nautobot-Job (ORM).
|
||||||
|
|
||||||
|
Kanonische Runtime des CF→Nautobot-Sync. Laeuft im nautobot-worker (kein extra
|
||||||
|
Container), schreibt per ORM (keine Nautobot-Exposure), zieht die CF-API outbound.
|
||||||
|
Portierung der Reconcile-Logik aus nautobot/tp44-cf-sync.py (REST) auf das ORM.
|
||||||
|
|
||||||
|
Governance (User 2026-09-09): periodische externe->Nautobot-Syncs leben als
|
||||||
|
Nautobot-Jobs in bf-infra/jobs/ und werden in Nautobot geplant — KEIN per-Task-
|
||||||
|
Container/-Cron. Kuenftige Syncs = weitere Jobs hier.
|
||||||
|
|
||||||
|
Deckung: CloudNetwork (cfd_tunnel + warp_connector), CloudService (workers,
|
||||||
|
access-apps, hyperdrive, VPC), Prefix-Assignments (teamnet/routes), Verwaisung
|
||||||
|
(stale-Markierung statt Loeschen). ID-Matching + extra_config-Merge wie im Skript.
|
||||||
|
|
||||||
|
Token: Nautobot-Secret "cf-api-token" (Provider environment-variable -> CF_API_TOKEN
|
||||||
|
am Worker). CF-Account-ID: Secret "cf-account-id" ODER Konstante unten.
|
||||||
|
|
||||||
|
CF bleibt operativer Master; Nautobot ist Dokumentations-SSoT (read-only Pull).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from nautobot.apps.jobs import BooleanVar, Job
|
||||||
|
from nautobot.cloud.models import (
|
||||||
|
CloudAccount,
|
||||||
|
CloudNetwork,
|
||||||
|
CloudNetworkPrefixAssignment,
|
||||||
|
CloudResourceType,
|
||||||
|
CloudService,
|
||||||
|
)
|
||||||
|
from nautobot.dcim.models import Manufacturer
|
||||||
|
from nautobot.extras.models import Secret
|
||||||
|
from nautobot.ipam.models import Prefix
|
||||||
|
|
||||||
|
CF_API = "https://api.cloudflare.com/client/v4"
|
||||||
|
CF_ACCOUNT_ID = "393c051f3accf51881d6c98fc5b66c36"
|
||||||
|
|
||||||
|
RESOURCE_TYPES = [
|
||||||
|
("Cloudflare cloudflared Tunnel", "cloud", "cloudnetwork"),
|
||||||
|
("Cloudflare WARP Connector", "cloud", "cloudnetwork"),
|
||||||
|
("Cloudflare VPC Service", "cloud", "cloudservice"),
|
||||||
|
("Cloudflare Hyperdrive", "cloud", "cloudservice"),
|
||||||
|
("Cloudflare Worker", "cloud", "cloudservice"),
|
||||||
|
("Cloudflare Access Application", "cloud", "cloudservice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CFEstateSync(Job):
|
||||||
|
dryrun = BooleanVar(
|
||||||
|
default=True,
|
||||||
|
description="Nur berichten, nichts schreiben (empfohlen fuer den ersten Lauf).",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = "CF Estate Sync"
|
||||||
|
description = "Cloudflare-Estate (Tunnel/Worker/Access/Hyperdrive/VPC) nach Nautobot syncen (Doku-SSoT)."
|
||||||
|
grouping = "TP44 / Cloudflare"
|
||||||
|
# Nur ein Boolean (dryrun) als Eingabe; das CF-Token kommt aus dem Nautobot-Secret,
|
||||||
|
# nicht aus einer Job-Variable. Ohne diese Zeile (Default True) verweigert Nautobot
|
||||||
|
# das Scheduling: "Job may have sensitive input variables".
|
||||||
|
has_sensitive_variables = False
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- CF-Pull
|
||||||
|
def _cf_get(self, path):
|
||||||
|
"""(ok, result) — 403/404 werden abgefangen, damit fehlende Scopes den Sync nicht kippen."""
|
||||||
|
req = urllib.request.Request(CF_API + path, headers={"Authorization": "Bearer " + self._tok})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return True, json.load(r).get("result")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code in (403, 404):
|
||||||
|
return False, None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _ingress(self, tunnel_id):
|
||||||
|
ok, res = self._cf_get(f"/accounts/{CF_ACCOUNT_ID}/cfd_tunnel/{tunnel_id}/configurations")
|
||||||
|
rules = ((res or {}).get("config") or {}).get("ingress") or [] if ok else []
|
||||||
|
return [r["hostname"] for r in rules if r.get("hostname")]
|
||||||
|
|
||||||
|
def _load_cf(self):
|
||||||
|
"""networks/services: (name, rt_name, desc, extra, match_key, match_val).
|
||||||
|
routes: {tunnel_id: cidr}. skipped: Liste nicht lesbarer Endpoints."""
|
||||||
|
A = CF_ACCOUNT_ID
|
||||||
|
networks, services, routes, skipped = [], [], {}, []
|
||||||
|
|
||||||
|
_, tuns = self._cf_get(f"/accounts/{A}/cfd_tunnel?is_deleted=false&per_page=50")
|
||||||
|
for t in (tuns or []):
|
||||||
|
networks.append((t["name"], "Cloudflare cloudflared Tunnel",
|
||||||
|
f"cloudflared-Tunnel ({len(t.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": t["id"], "kind": "cloudflared",
|
||||||
|
"connector_count": len(t.get("connections") or []),
|
||||||
|
"ingress_hostnames": self._ingress(t["id"])},
|
||||||
|
"tunnel_id", t["id"]))
|
||||||
|
_, warps = self._cf_get(f"/accounts/{A}/warp_connector?per_page=50")
|
||||||
|
for w in (warps or []):
|
||||||
|
networks.append((w["name"], "Cloudflare WARP Connector",
|
||||||
|
f"WARP-Connector ({len(w.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": w["id"], "kind": "warp_connector",
|
||||||
|
"connector_count": len(w.get("connections") or [])},
|
||||||
|
"tunnel_id", w["id"]))
|
||||||
|
|
||||||
|
_, scripts = self._cf_get(f"/accounts/{A}/workers/scripts")
|
||||||
|
for s in (scripts or []):
|
||||||
|
services.append((s["id"], "Cloudflare Worker", "Cloudflare Worker",
|
||||||
|
{"script": s["id"]}, "script", s["id"]))
|
||||||
|
|
||||||
|
ok, apps = self._cf_get(f"/accounts/{A}/access/apps?per_page=100")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("access/apps")
|
||||||
|
for a in (apps or []):
|
||||||
|
services.append((a.get("name") or a.get("id"), "Cloudflare Access Application",
|
||||||
|
f"Access-App ({a.get('type')})",
|
||||||
|
{"id": a.get("id"), "type": a.get("type"), "domain": a.get("domain")},
|
||||||
|
"id", a.get("id")))
|
||||||
|
|
||||||
|
ok, hyper = self._cf_get(f"/accounts/{A}/hyperdrive/configs")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("hyperdrive/configs")
|
||||||
|
for h in (hyper or []):
|
||||||
|
origin = h.get("origin") or {}
|
||||||
|
services.append((h.get("name") or h.get("id"), "Cloudflare Hyperdrive", "Hyperdrive-Config",
|
||||||
|
{"id": h.get("id"), "database": origin.get("database"),
|
||||||
|
"user": origin.get("user")}, "id", h.get("id")))
|
||||||
|
|
||||||
|
ok, vpc = self._cf_get(f"/accounts/{A}/connectivity/directory/services")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("connectivity/directory/services")
|
||||||
|
for v in (vpc or []):
|
||||||
|
# VPC-Directory-Services tragen ihren Identifier in "service_id" (NICHT "id").
|
||||||
|
sid = v.get("service_id") or v.get("id")
|
||||||
|
services.append((v.get("name") or sid, "Cloudflare VPC Service", "VPC-Service",
|
||||||
|
{"service_id": sid, "type": v.get("type")}, "service_id", sid))
|
||||||
|
|
||||||
|
ok, rts = self._cf_get(f"/accounts/{A}/teamnet/routes?per_page=50")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("teamnet/routes")
|
||||||
|
for r in (rts or []):
|
||||||
|
if r.get("network") and r.get("tunnel_id"):
|
||||||
|
routes[r["tunnel_id"]] = r["network"]
|
||||||
|
|
||||||
|
return networks, services, routes, skipped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- ORM-Upsert
|
||||||
|
def _upsert(self, model, cache, name, rt, acct, desc, extra, mk, mv, label, seen, dryrun):
|
||||||
|
# mv=None nie matchen: sonst greift .get(mk)==None auf das erste Objekt OHNE mk
|
||||||
|
# und mischt fremde extra_config zusammen (z.B. VPC ohne id -> App-Launcher).
|
||||||
|
if mv is None:
|
||||||
|
self.logger.warning("~ %s '%s' ohne Match-Wert (%s) — uebersprungen", label, name, mk)
|
||||||
|
return None
|
||||||
|
ex = next((o for o in cache if (o.extra_config or {}).get(mk) == mv), None)
|
||||||
|
if ex:
|
||||||
|
seen.add(ex.id)
|
||||||
|
merged = dict(ex.extra_config or {})
|
||||||
|
merged.update(extra)
|
||||||
|
if merged == (ex.extra_config or {}):
|
||||||
|
return ex
|
||||||
|
if dryrun:
|
||||||
|
self.logger.info("~ %s '%s': extra_config-Update [DRY-RUN]", label, ex.name)
|
||||||
|
return ex
|
||||||
|
ex.extra_config = merged
|
||||||
|
ex.validated_save()
|
||||||
|
self.logger.info("~ %s '%s' aktualisiert", label, ex.name)
|
||||||
|
return ex
|
||||||
|
if dryrun:
|
||||||
|
self.logger.info("+ %s '%s' [DRY-RUN]", label, name)
|
||||||
|
return None
|
||||||
|
obj = model(name=name, cloud_resource_type=rt, cloud_account=acct,
|
||||||
|
description=desc, extra_config=extra)
|
||||||
|
obj.validated_save()
|
||||||
|
cache.append(obj)
|
||||||
|
seen.add(obj.id)
|
||||||
|
self.logger.info("+ %s '%s' angelegt", label, name)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def _mark_stale(self, cache, seen, label, dryrun):
|
||||||
|
for o in cache:
|
||||||
|
extra = o.extra_config or {}
|
||||||
|
if o.id in seen:
|
||||||
|
if extra.get("stale") and not dryrun:
|
||||||
|
o.extra_config = {k: v for k, v in extra.items() if k != "stale"}
|
||||||
|
o.validated_save()
|
||||||
|
self.logger.info("↩ %s '%s': stale entfernt (wieder da)", label, o.name)
|
||||||
|
continue
|
||||||
|
if extra.get("stale"):
|
||||||
|
continue
|
||||||
|
if dryrun:
|
||||||
|
self.logger.warning("⚠ %s '%s' fehlt im Live-Pull → wuerde stale markiert [DRY-RUN]", label, o.name)
|
||||||
|
continue
|
||||||
|
o.extra_config = dict(extra, stale=True)
|
||||||
|
o.validated_save()
|
||||||
|
self.logger.warning("⚠ %s '%s' als stale markiert (nicht mehr in CF)", label, o.name)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- run
|
||||||
|
def run(self, dryrun=True):
|
||||||
|
try:
|
||||||
|
self._tok = Secret.objects.get(name="cf-api-token").get_value()
|
||||||
|
except Secret.DoesNotExist:
|
||||||
|
raise RuntimeError("Nautobot-Secret 'cf-api-token' fehlt — bitte anlegen (Provider env, CF_API_TOKEN).")
|
||||||
|
|
||||||
|
networks, services, routes, skipped = self._load_cf()
|
||||||
|
self.logger.info("Live-Pull: %d CloudNetworks · %d CloudServices%s",
|
||||||
|
len(networks), len(services),
|
||||||
|
f" · SKIP: {', '.join(skipped)}" if skipped else " · alle Endpoints lesbar")
|
||||||
|
|
||||||
|
mfr, _ = Manufacturer.objects.get_or_create(name="Cloudflare")
|
||||||
|
acct, created = CloudAccount.objects.get_or_create(
|
||||||
|
name="Cloudflare", defaults={"account_number": CF_ACCOUNT_ID, "provider": mfr})
|
||||||
|
if created and dryrun:
|
||||||
|
pass
|
||||||
|
|
||||||
|
rt_by_name = {}
|
||||||
|
for name, app, model_name in RESOURCE_TYPES:
|
||||||
|
ct = ContentType.objects.get(app_label=app, model=model_name)
|
||||||
|
rt, _ = CloudResourceType.objects.get_or_create(name=name, defaults={"provider": mfr})
|
||||||
|
rt.content_types.add(ct)
|
||||||
|
rt_by_name[name] = rt
|
||||||
|
|
||||||
|
net_cache = list(CloudNetwork.objects.filter(cloud_account=acct))
|
||||||
|
svc_cache = list(CloudService.objects.filter(cloud_account=acct))
|
||||||
|
seen_net, seen_svc = set(), set()
|
||||||
|
|
||||||
|
self.logger.info("[CloudNetworks]")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in networks:
|
||||||
|
self._upsert(CloudNetwork, net_cache, name, rt_by_name.get(rt_name), acct,
|
||||||
|
desc, extra, mk, mv, "CloudNetwork", seen_net, dryrun)
|
||||||
|
|
||||||
|
self.logger.info("[CloudServices]")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in services:
|
||||||
|
self._upsert(CloudService, svc_cache, name, rt_by_name.get(rt_name), acct,
|
||||||
|
desc, extra, mk, mv, "CloudService", seen_svc, dryrun)
|
||||||
|
|
||||||
|
self.logger.info("[Prefix-Assignments]")
|
||||||
|
for tunnel_id, cidr in routes.items():
|
||||||
|
net = next((n for n in net_cache if (n.extra_config or {}).get("tunnel_id") == tunnel_id), None)
|
||||||
|
if not net or getattr(net, "id", None) is None:
|
||||||
|
continue
|
||||||
|
pfx = Prefix.objects.filter(prefix=cidr).first()
|
||||||
|
if not pfx:
|
||||||
|
self.logger.info(" ~ Route %s: Prefix nicht in IPAM → uebersprungen", cidr)
|
||||||
|
continue
|
||||||
|
if CloudNetworkPrefixAssignment.objects.filter(cloud_network=net, prefix=pfx).exists():
|
||||||
|
continue
|
||||||
|
if dryrun:
|
||||||
|
self.logger.info(" + %s ↔ %s [DRY-RUN]", net.name, cidr)
|
||||||
|
continue
|
||||||
|
CloudNetworkPrefixAssignment(cloud_network=net, prefix=pfx).validated_save()
|
||||||
|
self.logger.info(" + %s ↔ %s angelegt", net.name, cidr)
|
||||||
|
|
||||||
|
self.logger.info("[Verwaisung]")
|
||||||
|
# CloudServices nur markieren, wenn ALLE Service-Endpoints lesbar waren — sonst
|
||||||
|
# wuerde ein fehlender Scope echte Objekte faelschlich als verwaist markieren.
|
||||||
|
svc_safe = not ({"access/apps", "hyperdrive/configs", "connectivity/directory/services"} & set(skipped))
|
||||||
|
if svc_safe:
|
||||||
|
self._mark_stale(svc_cache, seen_svc, "CloudService", dryrun)
|
||||||
|
else:
|
||||||
|
self.logger.info(" · CloudService-Verwaisung uebersprungen (Token-Scope unvollstaendig)")
|
||||||
|
self._mark_stale(net_cache, seen_net, "CloudNetwork", dryrun)
|
||||||
|
|
||||||
|
self.logger.info("Fertig (%s).", "DRY-RUN — nichts geschrieben" if dryrun else "COMMIT")
|
||||||
|
|
||||||
|
|
||||||
|
# Registrierung zentral in jobs/__init__.py (Nautobot importiert nur das Paket).
|
||||||
@@ -16,6 +16,9 @@ Umfang (User-Default 2026-09-07):
|
|||||||
Nautobot:8080, AdGuard:53 (bf1+bf2, tcp+udp), platform-pg:5432, mariadb:3306,
|
Nautobot:8080, AdGuard:53 (bf1+bf2, tcp+udp), platform-pg:5432, mariadb:3306,
|
||||||
influxdb2:8086, mqtt5:1883, portainer:9443,
|
influxdb2:8086, mqtt5:1883, portainer:9443,
|
||||||
MCP-Dienste (pve-mcp:3000, shelly-mcp:3030, n8n-mcp:3001, gitea-mcp:3000).
|
MCP-Dienste (pve-mcp:3000, shelly-mcp:3030, n8n-mcp:3001, gitea-mcp:3000).
|
||||||
|
+ Smart-Home (vervollstaendigt 2026-09-08, Ports per Socket-Probe verifiziert):
|
||||||
|
RaspberryMatic/CCU (80/443/2001/2010), iob-master:8081, z2m25:8080.
|
||||||
|
iob-exposed/iob_slave BEWUSST ohne Service (kein exponierter Port gefunden).
|
||||||
|
|
||||||
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
||||||
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||||
@@ -118,6 +121,15 @@ SERVICES = [
|
|||||||
("shelly-mcp", "shelly-mcp", [3030], "tcp", "Shelly MCP"),
|
("shelly-mcp", "shelly-mcp", [3030], "tcp", "Shelly MCP"),
|
||||||
("n8n-mcp", "n8n-mcp", [3001], "tcp", "n8n MCP"),
|
("n8n-mcp", "n8n-mcp", [3001], "tcp", "n8n MCP"),
|
||||||
("gitea-mcp", "gitea-mcp", [3000], "tcp", "Gitea MCP"),
|
("gitea-mcp", "gitea-mcp", [3000], "tcp", "Gitea MCP"),
|
||||||
|
# Smart-Home / IoT — Ports live per Socket-Probe verifiziert 2026-09-08 (NICHT raten!)
|
||||||
|
("RaspberryMatic","ccu-http", [80], "tcp", "Homematic CCU Web (HTTP)"),
|
||||||
|
("RaspberryMatic","ccu-https", [443], "tcp", "Homematic CCU Web (HTTPS)"),
|
||||||
|
("RaspberryMatic","ccu-rpc-bidcos", [2001], "tcp", "Homematic BidCos-RF XML-RPC"),
|
||||||
|
("RaspberryMatic","ccu-rpc-hmip", [2010], "tcp", "Homematic-IP XML-RPC"),
|
||||||
|
("iob-master", "iobroker-admin", [8081], "tcp", "ioBroker Admin"),
|
||||||
|
("z2m25", "zigbee2mqtt", [8080], "tcp", "Zigbee2MQTT Frontend"),
|
||||||
|
# iob-exposed (.19) + iob_slave (.33): kein exponierter Port gefunden (13 Ports, alle Timeout)
|
||||||
|
# -> BEWUSST kein Service, bis ein Port live belegt ist. Nicht "hilfreich" einen erfinden.
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
315
nautobot/tp44-cf-sync.py
Normal file
315
nautobot/tp44-cf-sync.py
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP44 Etappe 2 Teil B — CF-Estate LIVE aus der Cloudflare-API nach Nautobot syncen.
|
||||||
|
|
||||||
|
Ersetzt den Inline-Snapshot von tp44-cf-estate-seed.py durch einen periodischen
|
||||||
|
Live-Pull (get-or-create + Merge). Idempotent, Dry-Run default. Kein Push nach CF —
|
||||||
|
read-only Pull, Nautobot bleibt Dokumentations-SSoT, CF operativer Master.
|
||||||
|
|
||||||
|
Deckung (Live-Pull, je Endpoint 403-tolerant):
|
||||||
|
CloudNetwork cfd_tunnel + warp_connector (4)
|
||||||
|
CloudService workers/scripts + access/apps (11) [Repo-Token OK]
|
||||||
|
CloudService hyperdrive/configs + VPC directory (3) [braucht Token-Scope]
|
||||||
|
|
||||||
|
Verwaisung: ein CF-Objekt, das in Nautobot steht, aber im Live-Pull fehlt, wird
|
||||||
|
NICHT geloescht, sondern extra_config.stale=true gesetzt (Doku-SSoT bewahrt Historie).
|
||||||
|
Ein Objekt, das wieder auftaucht, verliert die Markierung.
|
||||||
|
|
||||||
|
Tokens:
|
||||||
|
CF : CF_API_TOKEN + CF_ACCOUNT_ID aus ~/.secrets/credentials.env (nie im Output)
|
||||||
|
Nautobot: NAUTOBOT_SUPERUSER_API_TOKEN aus docs/nas-vlan20/stacks/210-nautobot.yml (gitignored)
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 nautobot/tp44-cf-sync.py # Dry-Run (default) — zeigt Diff
|
||||||
|
python3 nautobot/tp44-cf-sync.py --commit # schreibt
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Cloudflare-Seite
|
||||||
|
def cf_secrets():
|
||||||
|
env = {}
|
||||||
|
with open(os.path.expanduser("~/.secrets/credentials.env")) as fh:
|
||||||
|
for line in fh:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
env[k.strip()] = v.strip().strip("'\"")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
_CF = cf_secrets()
|
||||||
|
CF_TOK, CF_ACCT = _CF.get("CF_API_TOKEN"), _CF.get("CF_ACCOUNT_ID")
|
||||||
|
if not CF_TOK or not CF_ACCT:
|
||||||
|
sys.exit("CF_API_TOKEN/CF_ACCOUNT_ID fehlen in ~/.secrets/credentials.env")
|
||||||
|
|
||||||
|
|
||||||
|
def cf_get(path):
|
||||||
|
"""(ok, result) — faengt 403/404 ab, damit fehlende Token-Scopes den Sync nicht kippen."""
|
||||||
|
req = urllib.request.Request("https://api.cloudflare.com/client/v4" + path,
|
||||||
|
headers={"Authorization": "Bearer " + CF_TOK})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return True, json.load(r).get("result")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code in (403, 404):
|
||||||
|
return False, None
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def cf_ingress(tunnel_id):
|
||||||
|
ok, res = cf_get(f"/accounts/{CF_ACCT}/cfd_tunnel/{tunnel_id}/configurations")
|
||||||
|
rules = ((res or {}).get("config") or {}).get("ingress") or [] if ok else []
|
||||||
|
return [r["hostname"] for r in rules if r.get("hostname")]
|
||||||
|
|
||||||
|
|
||||||
|
def load_cf_estate():
|
||||||
|
"""Baut (networks, services) aus dem Live-Pull. Jedes Objekt:
|
||||||
|
(name, resource_type, description, extra_config, match_key, match_val).
|
||||||
|
match_key/-val = stabile CF-ID → idempotentes Matching trotz kuratierter Namen.
|
||||||
|
Live-Zaehl-/Statusfelder liegen unter eigenen Keys (connector_count, ingress_hostnames),
|
||||||
|
damit der Merge kuratierte Strings (z.B. connectors='3x @ ap1/ap2/ap3') nicht ueberschreibt."""
|
||||||
|
networks, services = [], []
|
||||||
|
skipped = []
|
||||||
|
|
||||||
|
# --- CloudNetworks: cloudflared-Tunnel (+ Connector-Zahl, Ingress-Hostnames)
|
||||||
|
ok, tuns = cf_get(f"/accounts/{CF_ACCT}/cfd_tunnel?is_deleted=false&per_page=50")
|
||||||
|
for t in (tuns or []):
|
||||||
|
networks.append((t["name"], "Cloudflare cloudflared Tunnel",
|
||||||
|
f"cloudflared-Tunnel ({len(t.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": t["id"], "kind": "cloudflared",
|
||||||
|
"connector_count": len(t.get("connections") or []),
|
||||||
|
"ingress_hostnames": cf_ingress(t["id"])},
|
||||||
|
"tunnel_id", t["id"]))
|
||||||
|
# --- CloudNetworks: WARP-Connector
|
||||||
|
ok, warps = cf_get(f"/accounts/{CF_ACCT}/warp_connector?per_page=50")
|
||||||
|
for w in (warps or []):
|
||||||
|
networks.append((w["name"], "Cloudflare WARP Connector",
|
||||||
|
f"WARP-Connector ({len(w.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": w["id"], "kind": "warp_connector",
|
||||||
|
"connector_count": len(w.get("connections") or [])},
|
||||||
|
"tunnel_id", w["id"]))
|
||||||
|
|
||||||
|
# --- CloudServices: Workers
|
||||||
|
ok, scripts = cf_get(f"/accounts/{CF_ACCT}/workers/scripts")
|
||||||
|
for s in (scripts or []):
|
||||||
|
services.append((s["id"], "Cloudflare Worker", "Cloudflare Worker",
|
||||||
|
{"script": s["id"]}, "script", s["id"]))
|
||||||
|
|
||||||
|
# --- CloudServices: Access-Apps
|
||||||
|
ok, apps = cf_get(f"/accounts/{CF_ACCT}/access/apps?per_page=100")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("access/apps")
|
||||||
|
for a in (apps or []):
|
||||||
|
services.append((a.get("name") or a.get("id"), "Cloudflare Access Application",
|
||||||
|
f"Access-App ({a.get('type')})",
|
||||||
|
{"id": a.get("id"), "type": a.get("type"), "domain": a.get("domain")},
|
||||||
|
"id", a.get("id")))
|
||||||
|
|
||||||
|
# --- CloudServices: Hyperdrive (braucht Scope)
|
||||||
|
ok, hyper = cf_get(f"/accounts/{CF_ACCT}/hyperdrive/configs")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("hyperdrive/configs")
|
||||||
|
for h in (hyper or []):
|
||||||
|
origin = h.get("origin") or {}
|
||||||
|
services.append((h.get("name") or h.get("id"), "Cloudflare Hyperdrive",
|
||||||
|
"Hyperdrive-Config",
|
||||||
|
{"id": h.get("id"), "database": origin.get("database"),
|
||||||
|
"user": origin.get("user")}, "id", h.get("id")))
|
||||||
|
|
||||||
|
# --- CloudServices: VPC-Service (braucht Scope)
|
||||||
|
ok, vpc = cf_get(f"/accounts/{CF_ACCT}/connectivity/directory/services")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("connectivity/directory/services")
|
||||||
|
for v in (vpc or []):
|
||||||
|
services.append((v.get("name") or v.get("id"), "Cloudflare VPC Service",
|
||||||
|
"VPC-Service",
|
||||||
|
{"service_id": v.get("id"), "type": v.get("type")},
|
||||||
|
"service_id", v.get("id")))
|
||||||
|
|
||||||
|
return networks, services, skipped
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Nautobot-Seite
|
||||||
|
def nb_token():
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
NB_HDRS = {"Authorization": f"Token {nb_token()}", "Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def nb(method, path, body=None):
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
r = urllib.request.Request(f"{NB}{path}", data=data, headers=NB_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 nb_all(endpoint, params):
|
||||||
|
"""Alle Rows eines Endpoints (paginiert)."""
|
||||||
|
out, url = [], f"/api/{endpoint}/?{urllib.parse.urlencode(dict(params, depth=0, limit=200))}"
|
||||||
|
while url:
|
||||||
|
st, d = nb("GET", url)
|
||||||
|
out += d.get("results", [])
|
||||||
|
nxt = d.get("next")
|
||||||
|
url = nxt.replace(NB, "") if nxt else None
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_INDEX = {} # endpoint -> [rows] (einmal je Account geladen)
|
||||||
|
|
||||||
|
|
||||||
|
def index(endpoint, acct_id):
|
||||||
|
if endpoint not in _INDEX:
|
||||||
|
_INDEX[endpoint] = nb_all(endpoint, {"cloud_account": acct_id})
|
||||||
|
return _INDEX[endpoint]
|
||||||
|
|
||||||
|
|
||||||
|
def upsert(endpoint, name, payload, label, seen, acct_id=None,
|
||||||
|
match_key=None, match_val=None):
|
||||||
|
"""get-or-create + Merge von extra_config.
|
||||||
|
Matching: bei (match_key,match_val) nach der stabilen CF-ID in extra_config
|
||||||
|
(Rename-fest), sonst nach Name. Trackt die getroffene id in seen[endpoint]."""
|
||||||
|
ex = None
|
||||||
|
if match_val is not None:
|
||||||
|
for row in index(endpoint, acct_id):
|
||||||
|
if (row.get("extra_config") or {}).get(match_key) == match_val:
|
||||||
|
ex = row
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for row in (index(endpoint, acct_id) if acct_id else
|
||||||
|
nb_all(endpoint, {"name": name})):
|
||||||
|
if row.get("name") == name:
|
||||||
|
ex = row
|
||||||
|
break
|
||||||
|
if ex:
|
||||||
|
seen.setdefault(endpoint, set()).add(ex["id"])
|
||||||
|
merged = dict(ex.get("extra_config") or {})
|
||||||
|
merged.update(payload.get("extra_config") or {})
|
||||||
|
if merged == (ex.get("extra_config") or {}):
|
||||||
|
print(f" = {label}: '{name}' unveraendert")
|
||||||
|
return ex
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" ~ {label}: '{name}' extra_config-Update [DRY-RUN]")
|
||||||
|
return ex
|
||||||
|
st, d = nb("PATCH", f"/api/{endpoint}/{ex['id']}/", {"extra_config": merged})
|
||||||
|
print(f" ~ {label}: '{name}' aktualisiert" if st == 200
|
||||||
|
else f" ! {label}: '{name}' PATCH {st}: {json.dumps(d)[:200]}")
|
||||||
|
return d if st == 200 else ex
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" + {label}: '{name}' [DRY-RUN]")
|
||||||
|
return {"id": f"<dry:{name}>"}
|
||||||
|
st, d = nb("POST", f"/api/{endpoint}/", payload)
|
||||||
|
if st in (200, 201):
|
||||||
|
seen.setdefault(endpoint, set()).add(d["id"])
|
||||||
|
print(f" + {label}: '{name}' angelegt")
|
||||||
|
return d
|
||||||
|
print(f" ! {label}: '{name}' FEHLER {st}: {json.dumps(d)[:200]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def mark_stale(endpoint, acct_id, seen_ids, label):
|
||||||
|
"""CF-Objekte in Nautobot (per id), die im Live-Pull fehlen → extra_config.stale=true.
|
||||||
|
Ein wieder aufgetauchtes Objekt verliert die Markierung."""
|
||||||
|
for row in index(endpoint, acct_id):
|
||||||
|
extra = row.get("extra_config") or {}
|
||||||
|
nm = row.get("name")
|
||||||
|
if row["id"] in seen_ids:
|
||||||
|
if extra.get("stale") and COMMIT:
|
||||||
|
nb("PATCH", f"/api/{endpoint}/{row['id']}/",
|
||||||
|
{"extra_config": {k: v for k, v in extra.items() if k != "stale"}})
|
||||||
|
print(f" ↩ {label}: '{nm}' stale-Markierung entfernt (wieder da)")
|
||||||
|
continue
|
||||||
|
if extra.get("stale"):
|
||||||
|
continue
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" ⚠ {label}: '{nm}' fehlt im Live-Pull → wuerde stale markiert [DRY-RUN]")
|
||||||
|
continue
|
||||||
|
nb("PATCH", f"/api/{endpoint}/{row['id']}/", {"extra_config": dict(extra, stale=True)})
|
||||||
|
print(f" ⚠ {label}: '{nm}' als stale markiert (nicht mehr in CF)")
|
||||||
|
|
||||||
|
|
||||||
|
RESOURCE_TYPES = [
|
||||||
|
("Cloudflare cloudflared Tunnel", "cloud.cloudnetwork"),
|
||||||
|
("Cloudflare WARP Connector", "cloud.cloudnetwork"),
|
||||||
|
("Cloudflare VPC Service", "cloud.cloudservice"),
|
||||||
|
("Cloudflare Hyperdrive", "cloud.cloudservice"),
|
||||||
|
("Cloudflare Worker", "cloud.cloudservice"),
|
||||||
|
("Cloudflare Access Application", "cloud.cloudservice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP44 CF-Sync ({'COMMIT' if COMMIT else 'DRY-RUN'}) CF→{NB} ==")
|
||||||
|
networks, services, skipped = load_cf_estate()
|
||||||
|
print(f"Live-Pull: {len(networks)} CloudNetworks · {len(services)} CloudServices"
|
||||||
|
+ (f" · SKIP (Token-Scope): {', '.join(skipped)}" if skipped else " · alle Endpoints lesbar"))
|
||||||
|
|
||||||
|
print("\n[1] Manufacturer + CloudAccount")
|
||||||
|
mfr = upsert("dcim/manufacturers", "Cloudflare", {"name": "Cloudflare"}, "Manufacturer", {})
|
||||||
|
mfr_id = mfr["id"]
|
||||||
|
acct = upsert("cloud/cloud-accounts", "Cloudflare",
|
||||||
|
{"name": "Cloudflare", "account_number": CF_ACCT, "provider": mfr_id},
|
||||||
|
"CloudAccount", {})
|
||||||
|
acct_id = acct["id"]
|
||||||
|
|
||||||
|
print("\n[2] CloudResourceTypes")
|
||||||
|
rt_ids = {}
|
||||||
|
for name, ct in RESOURCE_TYPES:
|
||||||
|
rt = upsert("cloud/cloud-resource-types", name,
|
||||||
|
{"name": name, "provider": mfr_id, "content_types": [ct]}, "ResourceType", {})
|
||||||
|
rt_ids[name] = rt["id"]
|
||||||
|
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
print("\n[3] CloudNetworks")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in networks:
|
||||||
|
upsert("cloud/cloud-networks", name,
|
||||||
|
{"name": name, "cloud_resource_type": rt_ids.get(rt_name), "cloud_account": acct_id,
|
||||||
|
"description": desc, "extra_config": extra}, "CloudNetwork", seen,
|
||||||
|
acct_id=acct_id, match_key=mk, match_val=mv)
|
||||||
|
|
||||||
|
print("\n[4] CloudServices")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in services:
|
||||||
|
upsert("cloud/cloud-services", name,
|
||||||
|
{"name": name, "cloud_resource_type": rt_ids.get(rt_name), "cloud_account": acct_id,
|
||||||
|
"description": desc, "extra_config": extra}, "CloudService", seen,
|
||||||
|
acct_id=acct_id, match_key=mk, match_val=mv)
|
||||||
|
|
||||||
|
print("\n[5] Verwaisung (stale-Markierung; nur Endpoints, die gelesen wurden)")
|
||||||
|
# Nur markieren, wenn der zugehoerige Endpoint erreichbar war — sonst wuerde ein
|
||||||
|
# fehlender Token-Scope echte Objekte faelschlich als verwaist markieren.
|
||||||
|
if "access/apps" not in skipped and "hyperdrive/configs" not in skipped \
|
||||||
|
and "connectivity/directory/services" not in skipped:
|
||||||
|
mark_stale("cloud/cloud-services", acct_id, seen.get("cloud/cloud-services", set()), "CloudService")
|
||||||
|
else:
|
||||||
|
print(" · CloudService-Verwaisung uebersprungen (Token-Scope unvollstaendig — kein sicherer Vergleich)")
|
||||||
|
mark_stale("cloud/cloud-networks", acct_id, seen.get("cloud/cloud-networks", set()), "CloudNetwork")
|
||||||
|
|
||||||
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN — nichts geschrieben'}) ==")
|
||||||
|
if skipped:
|
||||||
|
print(f"Hinweis: {', '.join(skipped)} nicht lesbar → Token-Scope erweitern, dann syncen sie mit.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
119
nautobot/tp44-ipam-prefixes.py
Normal file
119
nautobot/tp44-ipam-prefixes.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP44 Etappe 2 Teil A — die zwei fehlenden IPAM-Prefixe der CF-CloudNetworks anlegen.
|
||||||
|
|
||||||
|
Zwei CloudNetworks (tp44-cf-estate-seed.py) tragen eine Route, die der Seeder als
|
||||||
|
cloud-network-prefix-assignment verknuepfen wuerde -- aber nur, wenn der Prefix in
|
||||||
|
IPAM existiert (dort Schritt [7], sonst "~ Prefix ... nicht in IPAM"):
|
||||||
|
|
||||||
|
10.40.0.0/16 -> bf-core-mesh (WARP-Connector auf crs326-core, TP16.1) [container]
|
||||||
|
10.101.0.0/24 -> fm.emsenhuber.net (Standort FM, cloudflared-Tunnel) [network]
|
||||||
|
|
||||||
|
Idempotenter get-or-create (Dry-Run default). Nach dem --commit den Seeder erneut
|
||||||
|
laufen lassen -- er haengt die zwei Assignments dann automatisch ein.
|
||||||
|
|
||||||
|
10.40.0.0/16 als 'container', weil die VLAN-/24 (10.40.10/20/30/...) aus
|
||||||
|
ingest-baseline.py bereits als Prefixe existieren; Nautobot nistet sie
|
||||||
|
automatisch darunter (rein hierarchisch, nicht-destruktiv).
|
||||||
|
|
||||||
|
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
||||||
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 nautobot/tp44-ipam-prefixes.py # Dry-Run (default)
|
||||||
|
python3 nautobot/tp44-ipam-prefixes.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 find(endpoint: str, params: dict, match_key: str, match_val: str):
|
||||||
|
q = urllib.parse.urlencode(dict(params, depth=0))
|
||||||
|
st, d = req("GET", f"/api/{endpoint}/?{q}")
|
||||||
|
for row in d.get("results", []):
|
||||||
|
if row.get(match_key) == match_val:
|
||||||
|
return row
|
||||||
|
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
|
||||||
|
# (prefix, type, description)
|
||||||
|
PREFIXES = [
|
||||||
|
("10.40.0.0/16", "container",
|
||||||
|
"BF-Supernet; Route des CF WARP-Connectors bf-core-mesh (crs326-core, TP16.1)"),
|
||||||
|
("10.101.0.0/24", "network",
|
||||||
|
"Standort FM; Route des CF cloudflared-Tunnels fm.emsenhuber.net"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP44 IPAM-Prefixe ({'COMMIT' if COMMIT else 'DRY-RUN'}) @ {NB} ==")
|
||||||
|
st_active = status_id("Active")
|
||||||
|
ns_global = namespace_id("Global")
|
||||||
|
|
||||||
|
for prefix, ptype, desc in PREFIXES:
|
||||||
|
existing = find("ipam/prefixes", {"prefix": prefix}, "prefix", prefix)
|
||||||
|
if existing:
|
||||||
|
print(f" = Prefix: '{prefix}' existiert ({existing['id']})")
|
||||||
|
continue
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" + Prefix: '{prefix}' [{ptype}] [DRY-RUN]")
|
||||||
|
continue
|
||||||
|
payload = {"prefix": prefix, "status": st_active, "namespace": ns_global,
|
||||||
|
"type": ptype, "description": desc}
|
||||||
|
st, d = req("POST", "/api/ipam/prefixes/", payload)
|
||||||
|
if st in (200, 201):
|
||||||
|
print(f" + Prefix: '{prefix}' [{ptype}] angelegt ({d.get('id')})")
|
||||||
|
else:
|
||||||
|
print(f" ! Prefix: '{prefix}' FEHLER {st}: {json.dumps(d)[:300]}")
|
||||||
|
|
||||||
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN — nichts geschrieben'}) ==")
|
||||||
|
if not COMMIT:
|
||||||
|
print("Danach: python3 nautobot/tp44-cf-estate-seed.py --commit (haengt die 2 Assignments ein)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user