Compare commits
10 Commits
tp45-schem
...
2f3d44d4be
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3d44d4be | |||
| fe11ae2a5c | |||
| 7b365e1586 | |||
| 6d1d56e463 | |||
| 4720b5d223 | |||
| 3f774b7b17 | |||
| 180c5b6c57 | |||
| 7b03e01fab | |||
| 4e61c63ae7 | |||
| 08bbe1830f |
14
CLAUDE.md
14
CLAUDE.md
@@ -6,11 +6,15 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
||||
|
||||
## Pflichtlektüre pro Sitzung (Reihenfolge)
|
||||
|
||||
0. 🔴 **TP45 = aktuell dringendster Posten (2026-09-21, Gitea #71):** interne `*.bf.emsenhuber.net`
|
||||
lösen im Browser **nicht** auf (AGH wertet Rewrites im **Filter-Pfad** aus → für unfiltrierte
|
||||
Clients übersprungen → Fallback auf den Core, der nur 10 von 26 Namen hält), und `ds-bf` ist
|
||||
**öffentlich kaputt** (einziger Ingress-Eintrag, der Caddy umgeht → TP5-Drop). Reihenfolge:
|
||||
VLAN20 ins TP32-Schema → VLAN30-Pool-Grenze → DNS/FQDN. Ausführlich: `### TP45`.
|
||||
0. 🟢 **TP45 (Gitea #71) — Schicht C (DNS/FQDN) ist durch (2026-09-22, Commit `6d1d56e`), vorgezogen.**
|
||||
Die internen `*.bf.emsenhuber.net` lösen wieder auf (der Core hält jetzt **27** Statics statt 10),
|
||||
`ds-bf` ist öffentlich heil (läuft über Caddy), AGH macht **nur noch Filtering** (Upstream = Core).
|
||||
Der *sichtbare* Schaden ist damit weg — der Ursachen-Kontext steht unter `### TP45` (AGH wertete
|
||||
Rewrites im Filter-Pfad aus → unfiltrierte Clients fielen auf den Core zurück).
|
||||
**Offen sind die IP-Schichten, in dieser Reihenfolge:** **A** VLAN20 ins TP32-Schema (Nautobot
|
||||
`.30–.34` → `.154–.158` zuerst, dann AGH `.36/.37` → `.30/.31` — ⚠️ AGH ist **doppelt adressiert**,
|
||||
beide Adressen live) → **B** VLAN30-Pool-Grenze (braucht den DHCP-Reservierungs-Renderer) →
|
||||
**E** nas/pve/macmini umziehen (eigene Session) → **C4** (TP34: FQDN als Daten).
|
||||
**`### TP46`** = CF-/Tunnel-SSoT + Recovery (eigene Baustelle).
|
||||
1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)*
|
||||
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
|
||||
|
||||
@@ -31,10 +31,11 @@ dns:
|
||||
ratelimit_subnet_len_ipv6: 56
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: true
|
||||
# TP45 C2 (2026-09-22): der Core ist der EINZIGE Upstream — AGH macht nur noch
|
||||
# Filtering. Die interne Zone und die bare Namen kommen damit aus derselben Quelle
|
||||
# wie fuer jeden anderen Client. DoH nach draussen macht allein der Core.
|
||||
upstream_dns:
|
||||
- https://px35ynlol3.cloudflare-gateway.com/dns-query
|
||||
- '[/bf.emsenhuber.net/]10.40.20.1'
|
||||
- '[/40.10.in-addr.arpa/]10.40.20.1'
|
||||
- 10.40.20.1
|
||||
upstream_dns_file: ""
|
||||
bootstrap_dns:
|
||||
- 1.1.1.1
|
||||
@@ -71,7 +72,7 @@ dns:
|
||||
ipset_file: ""
|
||||
bootstrap_prefer_ipv6: false
|
||||
upstream_timeout: 10s
|
||||
private_<redacted>: []
|
||||
private_networks: []
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
use_dns64: false
|
||||
@@ -158,85 +159,9 @@ filtering:
|
||||
blocking_mode: default
|
||||
parental_block_host: family-block.dns.adguard.com
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
rewrites:
|
||||
- domain: pbs.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: portainer.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: pve.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: gitea-mcp.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: n8n-mcp.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: ha-mcp.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: pve-mcp.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: z2m25.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: iobroker.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: ccu.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: nautobot.bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: vw.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: hass.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: n8n.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: gitea.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: ds-bf.emsenhuber.net
|
||||
answer: 10.40.20.40
|
||||
enabled: true
|
||||
- domain: nas.bf.emsenhuber.net
|
||||
answer: 10.40.20.10
|
||||
enabled: true
|
||||
- domain: macmini.bf.emsenhuber.net
|
||||
answer: 10.40.20.167
|
||||
enabled: true
|
||||
- domain: mfc.bf.emsenhuber.net
|
||||
answer: 10.40.10.6
|
||||
enabled: true
|
||||
- domain: tv.bf.emsenhuber.net
|
||||
answer: 10.40.40.11
|
||||
enabled: true
|
||||
- domain: docker.bf.emsenhuber.net
|
||||
answer: 10.40.20.126
|
||||
enabled: true
|
||||
- domain: platform-pg.bf.emsenhuber.net
|
||||
answer: 10.40.20.20
|
||||
enabled: true
|
||||
- domain: core.bf.emsenhuber.net
|
||||
answer: 10.40.99.1
|
||||
enabled: true
|
||||
- domain: ap1.bf.emsenhuber.net
|
||||
answer: 10.40.99.11
|
||||
enabled: true
|
||||
- domain: ap2.bf.emsenhuber.net
|
||||
answer: 10.40.99.12
|
||||
enabled: true
|
||||
- domain: branch.bf.emsenhuber.net
|
||||
answer: 10.40.99.2
|
||||
enabled: true
|
||||
# TP45 C2 (2026-09-22): alle 26 Rewrites entfernt — sie waren die zweite, stille
|
||||
# Definitionsquelle. Der Core haelt jeden dieser Namen als /ip dns static.
|
||||
rewrites: []
|
||||
safe_fs_patterns:
|
||||
- /opt/AdGuardHome/userfilters/*
|
||||
max_http_size: 256MB
|
||||
|
||||
75
docs/handoff-20260922-claude-md-ausduennen.md
Normal file
75
docs/handoff-20260922-claude-md-ausduennen.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Handoff 2026-09-22 — CLAUDE.md ausdünnen (strukturelle Hygiene, kein Bug)
|
||||
|
||||
**Anlass:** Christoph stellt fest, dass CLAUDE.md „eh schon so groß" ist. Das ist kein kosmetischer
|
||||
Punkt — es ist eine strukturelle Drift, die jede künftige Session belastet. Dieser Handoff beschreibt
|
||||
das Problem aus der Innenperspektive einer Session (die CLAUDE.md bei jedem Start komplett lädt) und
|
||||
gibt einen Scope für eine eigene Cleanup-Session.
|
||||
|
||||
## Das Problem, aus Sicht der Session
|
||||
|
||||
Jede Session lädt CLAUDE.md **vollständig** in den Kontext. Was dabei konkret wehtut:
|
||||
|
||||
1. **Reine Kontext-Kosten.** Die Datei ist >58 KB. Ein großer Teil davon ist nicht „Kontext für die
|
||||
anstehende Arbeit", sondern Historie und abgeschlossene TPs. Der Token-Preis wird bei jeder Session
|
||||
fällig, egal worum es geht.
|
||||
|
||||
2. **Signal-to-noise im Gotcha-Block.** Der „Kritische Gotchas"-Abschnitt mischt **aktive Fallen** mit
|
||||
**erledigten/gegenstandslosen** Einträgen. Beispiele direkt aus dem Bestand: `~~ALLNET~~` und
|
||||
`~~Omada~~` sind mehrfach als „gegenstandslos/historisch" markiert, stehen aber weiterhin voll im
|
||||
Text. Mehrere Einträge tragen „korrigiert am X" — die **korrigierte** Fassung steht neben der
|
||||
**falschen**, die damit weiter mitgelesen wird. Beim Suchen nach *einer* relevanten Falle scannt
|
||||
die Session dutzende Zeilen, von denen ein guter Teil totes Gewicht ist.
|
||||
|
||||
3. **Redundanz über drei Orte.** Dieselbe Information (z. B. die AP-Positionen, der FT-Verlauf) steht
|
||||
in CLAUDE.md **und** in `teilprojekte.md` **und** teils in Handoffs. Eine Korrektur an einer Stelle
|
||||
lässt die anderen stumm veralten — genau die Drift, vor der die eigene CLAUDE.md warnt
|
||||
(„Manuell gepflegte Doku verliert gegen die Realität, wenn sie lang wird").
|
||||
|
||||
4. **Der gefährlichste Effekt: die Bloat untergräbt „live prüfen".** Die Projektregel lautet „nicht
|
||||
raten, live prüfen oder fragen". Eine Riesen-CLAUDE.md mit eingestreuten Stale-Angaben verleitet
|
||||
genau dazu, sich auf die Datei zu verlassen statt nachzusehen — die Datei ist Teil des Problems,
|
||||
das sie zu lösen vorgibt.
|
||||
|
||||
## Was wohin gehört (SSoT-Dreiteilung, TP36)
|
||||
|
||||
| Inhalt | Soll-Ort |
|
||||
|---|---|
|
||||
| VLAN-Modell, Hardware-Tabelle (aktuell), Tooling-Pointer | CLAUDE.md (bleibt) |
|
||||
| **Aktive, querschnittliche** Fallen (Firewall deny-by-default, sshpass-Regel, cap-wifiN-Karte …) | CLAUDE.md (bleibt, komprimiert) |
|
||||
| TP-spezifische Gotchas & Detail-Verläufe | `teilprojekte.md` `### TPnn` |
|
||||
| Erledigte/historische Geräte & Entscheidungen | `teilprojekte-archiv.md` |
|
||||
| „korrigiert am X"-Notizen | Original-Eintrag bereinigen (Stale-Wert raus), Korrektur **ersetzt** den Irrtum, statt neben ihm zu stehen |
|
||||
|
||||
## Konkrete Kandidaten zum Ausdünnen (Startpunkte, nicht abschließend)
|
||||
|
||||
- **„Kritische Gotchas":** alle `~~durchgestrichenen~~`/„gegenstandslos"-Einträge (ALLNET, Omada,
|
||||
sw1, ES208G, „ALLNET VLAN-DB") → Archiv oder löschen.
|
||||
- **„korrigiert"-Notizen:** die falsche Angabe entfernen, nur die korrigierte behalten (z. B.
|
||||
Nautobot-IP, „2×/Tag war nie real", „`ft=no` war NICHT uniform").
|
||||
- **„Pflichtlektüre":** ist von „1–7" auf effektiv „0–7" angewachsen; die ✅-GESCHLOSSEN-Punkte
|
||||
(TP16.3 §5, Fund E) sind abgeschlossene Historie → aus dem Pflichtteil in einen Verweis.
|
||||
- **„Lessons for Sessions":** mehrere Meta-Lektionen überlappen sich (Maskierungs-Muster kommt 3×,
|
||||
„erst das Werkzeug verdächtigen" 2×) → auf je eine Fassung verdichten.
|
||||
- **Hardware-Tabelle:** ausgemusterte Geräte (Omada, ALLNET) sind noch als Zeilen da, teils nur mit
|
||||
❌ — prüfen, ob sie ganz raus können (Archiv) statt als Toten in der aktuellen Tabelle zu stehen.
|
||||
|
||||
## Vorgehen für die Cleanup-Session
|
||||
|
||||
1. **Bestandsaufnahme zuerst:** CLAUDE.md abschnittsweise sichten und je Eintrag klassifizieren
|
||||
(aktiv-querschnittlich / TP-spezifisch / historisch / redundant). Drift-Tabelle vorlegen, **fragen**
|
||||
was wohin soll — nicht stillschweigend umsortieren.
|
||||
2. **Nichts löschen ohne Zielort:** TP-spezifisches wandert nach `teilprojekte.md`, Historisches nach
|
||||
`teilprojekte-archiv.md`. Erst wenn der Zielort steht, aus CLAUDE.md entfernen.
|
||||
3. **Zielbild:** CLAUDE.md zurück auf einen **schlanken Index** (VLAN-Modell, Hardware, Tooling, die
|
||||
~10 wirklich querschnittlichen aktiven Fallen). Detail gehört nach `teilprojekte.md`.
|
||||
4. **Abnahme:** CLAUDE.md-Wortzahl/Zeilen vorher/nachher notieren; sicherstellen, dass kein
|
||||
noch-aktiver Gotcha (Firewall, sshpass, cap-wifiN, slaves-datapath, WLAN-Logs-Topic) verloren geht.
|
||||
|
||||
## Übergabeprompt (nächste Session)
|
||||
|
||||
> CLAUDE.md ausdünnen: die Datei ist >58 KB und mischt aktive Fallen mit erledigter Historie
|
||||
> (durchgestrichene „gegenstandslos"-Einträge, „korrigiert"-Notizen neben der falschen Angabe,
|
||||
> Redundanz mit teilprojekte.md). Aufgabe: Bestandsaufnahme + Drift-Tabelle, dann TP-spezifisches
|
||||
> nach teilprojekte.md, Historisches ins Archiv, CLAUDE.md zurück auf einen schlanken Index
|
||||
> (~10 querschnittliche aktive Gotchas behalten). Nicht stillschweigend umsortieren — erst klassifizieren
|
||||
> und fragen. Siehe [[handoff-20260922-claude-md-ausduennen]].
|
||||
@@ -1113,6 +1113,8 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
|
||||
|
||||
**Stand 2026-09-15 — AP-Zuordnung DEFINITIV geklärt, Tausch-Befund ap2↔ap3** ([Handoff](handoff-20260915-tp25-zuordnung-und-redundanz.md)). Positionen per IP+MAC+Switch-Port (MNDP/Bridge-Host/Port-Link) live belegt: **ap1 = Sideboard/Wohnzimmer (`core ether3`), ap2 = Technik UG (`core ether21`), ap3 = Technik OG (`branch ether24`)**. Der geplante Tausch (ap1↔ap3, cAP ax → Sideboard) hat faktisch **ap2↔ap3** gedreht — ap1 blieb stehen, der cAP ax steht jetzt in OG. MAC-Korrektur: ap2 `ether1` ist `:39`, nicht `:3A` (`:3A` = Bridge/Mgmt-MAC). Rundgang (natürlicher Zustand, 1-s-Takt): **0 Disconnects**, −45…−77 dBm, Roaming über alle 3 APs; **Terrasse −58 stabil** (jetzt cAP ax/OG, 2,4G), ap1 nur kurz gestreift. ap1-Aus-Test: Main-Client voll abgedeckt (tiefster Wert −80), **alle 27 IOT kehren zurück**, aber ~4 Wohnzimmer-IOT marginal −77…−80 ⇒ ap1 technisch redundant, nicht ausfallkritisch. ap1 wieder an (Revert). Offen: (1) ap1 behalten/abbauen, (2) Tausch auf Plan korrigieren (cAP ax → Sideboard?), (3) Drei-Räume-Schatten (Schlafzimmer/Bad/Küche) gezielt nachmessen — noch nicht verifiziert, (4) Doku-Drift (diese Session).
|
||||
|
||||
**Merke:** `no cached PMK` im Log ≠ FT-Problem — es tritt auch auf `ft=no`-SSIDs (IOT) auf und ist ein harmloser PMK-Cache-Miss (voller Handshake statt Fast-Reassoc). Der 09-15-Abriss war die Terrassen-Bandkante, nicht PMK/FT.
|
||||
|
||||
**Stand 2026-09-14, 20:05 — ap2-In-situ: Hardware entlastet, Terrasse = 5-GHz-Zellkante,
|
||||
Band-Boden nicht sauber ausdrückbar** ([Handoff](handoff-20260914-tp25-ap2-insitu-terrassse.md)).
|
||||
Live-Session auf der Terrasse (Christoph kooperierte physisch — der Test, der am 08-31 „nachts
|
||||
@@ -1867,15 +1869,16 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört
|
||||
> Gerät. Deshalb steht hier die Liste, nicht der Termin.
|
||||
|
||||
- **Auslösekriterium (statt Datum):** wenn die offenen TPs abgearbeitet sind bzw. Christoph das
|
||||
Projekt schließt. 🔴 **Harte Ausnahme, die den Termin vorzieht:** ein *neuer* Leak, ein
|
||||
fremdzugängliches Gerät oder ein Personenwechsel — dann sofort und einzeln, nicht gebündelt.
|
||||
Projekt schließt. *(2026-09-21: eine frühere „harte Ausnahme für neue Leaks" wurde **gestrichen** —
|
||||
sie widersprach dem Bündelungszweck dieses TPs und der Praxis: auch die leak-getriebenen Posten
|
||||
#1 und #5–#9 stehen unverändert im Bündel. Ein Transkript-Leak zieht **keinen** Posten vor.)*
|
||||
|
||||
| # | Credential | Umfang | Anlass | Vorarbeit | Preis der Rotation |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **MikroTik `admin`** | core · branch · ap1 · ap2 (**ein** geteilter Wert) | 🔴 **zweimal in ein Transkript geraten** (2026-08-22 via `sshpass -p`; 2026-08-23 durch einen Enumerationsfehler) | ✅ **Etappe 1 durch** (s.u.) | nach Etappe 2 nur noch `~/.secrets/credentials.env` + `~/.claude.json` + MikroMCP-Neustart |
|
||||
| 2 | **IoT-WLAN-PSK** (`sec-iot`) | `dirtyvader-iot`, 26 Clients | steht **im Klartext** im TP17-Text ⇒ auch in der **Git-Historie**; Redigieren holt ihn dort nicht raus | Hebel `multi-passphrase` am Core verifiziert vorhanden, Tabelle leer | 23 Shellys per Skript + **3 von Hand** (`.249` NSPanel Pro, `.244` Nuki Bridge, `.227` SwitchBot) |
|
||||
| 3 | **Shelly-Gerätepasswort** *(vormals TP17.2)* | 23 Shellys | Härtung (bisher **keine** Auth auf den Geräten) | ✅ Werkzeug fertig (`migrate.py cmd_auth` + Digest-Fallback) | **je Gerät ein HA-Re-Auth-Flow** — das ist der Aufwand, nicht `SetAuth` |
|
||||
| 4 | **CF-Access-Service-Token** (`CF_API_TOKEN`) | Cloudflare-Zugang, s. TP14.4 | ⏳ **läuft am 2027-01-09 ohnehin ab** — harter Termin, unabhängig vom Projektende | Runbook liegt (`tp14.4-cf-access-token-runbook.md`) | Token neu ausstellen + `~/.secrets` nachziehen; **account-owned**, anderer verify-Endpoint |
|
||||
| 4 | **CF-Access-Service-Token** (`CF_API_TOKEN`) | Cloudflare-Zugang, s. TP14.4 | 🔴 **2026-09-21 erneut ins Transkript geraten** (Session-`grep` unmaskiert). Der bisherige harte Termin ist **entfallen**: ⏳ „2027-01-09" galt für das längst revokte Alt-Token `9984078812bc…`; das **live** `CF_API_TOKEN` (id `493ed8bd…`) hat **kein** Ablaufdatum — verifiziert 2026-09-21 via `/accounts/{id}/tokens/verify`, `expires_on: null` | Runbook liegt (`tp14.4-cf-access-token-runbook.md`) | Token neu ausstellen + `~/.secrets` nachziehen; **account-owned**, anderer verify-Endpoint |
|
||||
| 5 | **Nautobot-Tokens** (`NAUTOBOT_SUPERUSER_API_TOKEN`, `NAUTOBOT_GITEA_TOKEN`) | Nautobot-Superuser + Gitea-Anbindung | 🔴 in der TP16.3-§2/§3-Session (2026-08-31) durch schwache `sed`-Maskierung **ins Transkript** geraten | Werte in `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored) | neu ausstellen + Stack-YML nachziehen; Superuser-Token wird von den `gc-*`-Skripten gelesen |
|
||||
| 6 | **MariaDB `root`** (LXC 302, `10.40.20.23`) | root-DB-Login | 🔴 am 2026-09-01 (TP39) ins Transkript geraten — die `/root/.my.cnf` trug `password <wert>` **ohne `=`**, die Feld-Maskierung ankerte auf `=` und griff nicht | Wert in `/root/.my.cnf` (chmod 600) | `SET PASSWORD` + `.my.cnf` nachziehen; **kein** externer Konsument (Dienste nutzen eigene DB-User) ⇒ billigste Rotation der Liste |
|
||||
| 7 | **`TERPENE_ANDROID_KEYSTORE_PW`** | Terpene-Android-TWA-Signing (`android/android.keystore`, alias `terpene`) | 🔴 am 2026-09-01 (TP41) durch **Regex-Suffix-Loch** (`_PW` nicht abgedeckt) ins Transkript geraten | Wert in `~/.secrets/credentials.env` + Keystore-Datei | **Store-/Key-Passwort** via `keytool -storepasswd`/`-keypasswd` ändern + `credentials.env` nachziehen; Konsument = Android-Build/Play-Signing (**Cross-Projekt blaueblüten**) — den **Key selbst NICHT** neu erzeugen |
|
||||
@@ -2037,8 +2040,12 @@ Es ist **ein** Passwort für alle vier Geräte — die „Überlebensversicherun
|
||||
- **CF-Access-Service-Token** — **Rotationsposten #4**, am 2026-08-24 aus den Follow-ups hierher
|
||||
gezogen (stand dort als „TP14.4 — CF-Access-Service-Token rotieren"). Sachlich gehört er zu
|
||||
**TP14.4** (CF-Access-Profil für Claude, Runbook `docs/tp14.4-cf-access-token-runbook.md`); hier
|
||||
steht er, damit die Rotationsliste vollständig ist. ⚠️ Der `CF_API_TOKEN` läuft ohnehin
|
||||
**2027-01-09** ab — spätestens dann ist dieser Posten fällig, unabhängig vom Projektende.
|
||||
steht er, damit die Rotationsliste vollständig ist. ⚠️ ~~Der `CF_API_TOKEN` läuft ohnehin
|
||||
**2027-01-09** ab~~ — **korrigiert 2026-09-21:** dieses Datum gehörte zum revokten Alt-Token
|
||||
`9984078812bc…`; das **live** `CF_API_TOKEN` (id `493ed8bd…`) hat **kein** Ablaufdatum
|
||||
(`expires_on: null`, via `/accounts/{id}/tokens/verify`). Der harte Termin ist damit weg —
|
||||
**Anlass** ist jetzt der **Leak vom 2026-09-21** (🔴 siehe Posten #4 oben). Der Posten bleibt
|
||||
**gebündelt** und wird nicht vorgezogen (Auslösekriterium oben).
|
||||
|
||||
#### Posten #3 im Detail — Shelly-Gerätepasswort (`SetAuth`), vormals TP17.2
|
||||
*Am 2026-08-24 als eigenes TP aufgelöst und hierher gezogen — Text unverändert übernommen.*
|
||||
@@ -2737,11 +2744,41 @@ unter `*1A TP5-WOULD-DROP` / `*2D TP5: deny-by-default` (621 Pakete).
|
||||
|---|---|---|
|
||||
| **A** | VLAN20 virtuelle Gäste ins Schema — **Nautobot zuerst** (`.30–.34` → `.154–.156`/`.158`), dann AGH | 🔄 **Nautobot ✅ 2026-09-21** (s. u.) · AGH + übrige Gäste offen |
|
||||
| **B** | VLAN30 — **nur die Pool-Grenze**, Geräte bleiben (Christoph 2026-09-21) | offen |
|
||||
| **C** | DNS/FQDN: `ds-bf` über Caddy, `coolify.bf` neu, `.bf` vollständig auf den Core, AGH nur Filtering, TP34 | offen |
|
||||
| **C** | DNS/FQDN: `ds-bf` über Caddy, `coolify.bf` neu, `.bf` vollständig auf den Core, AGH nur Filtering, TP34 | ✅ **C1–C3 durch 2026-09-22** (s. u.) · C4 (TP34) + `coolify.bf` offen |
|
||||
| **D** | Tunnel-Inventar + GC + generische Namen, TP30 #10, Leak-Ursachen | offen |
|
||||
| **E** | nas/pve/macmini mit Doppeladresse — **eigene Session**, pve2 nur reservieren | offen |
|
||||
| **E** | nas/pve/macmini — **neue IPs zuerst am Core** (Sekundär-IP + 1:1-NAT), dann messen und umstellen; nas zuletzt | offen |
|
||||
| **F** | CF-/Tunnel-SSoT + Recovery → **TP46** | offen |
|
||||
|
||||
##### Schicht E — Mechanik (Christoph-Entscheid 2026-09-21, ersetzt „Doppeladresse am Host zuerst")
|
||||
|
||||
**Neue IPs zuerst am Core, nicht am Gerät:** Sekundär-IPs `.110` (nas) · `.121` (pve) · `.130`
|
||||
(macmini) auf der Core-`vlan20` **plus 1:1-NAT** auf das Gerät (`dst-nat` + `src-nat`, platziert
|
||||
**vor** der `no-NAT inter-VLAN`-Regel). 🔴 **Routing allein reicht nicht** — im selben Subnetz
|
||||
antwortet das Gerät dem Client direkt per L2, der Client verwirft eine Antwort von `.10` auf eine
|
||||
Verbindung zu `.110`; der Rückweg braucht `src-nat`, und die Adresse muss **am Core liegen**, sonst
|
||||
beantwortet er kein ARP dafür (**nicht** `proxy-arp` — das antwortet für *jede* freie Adresse im
|
||||
/24). Heute ist die Core-NAT-Kette leer (nur `no-NAT inter-VLAN` + WAN-Masquerade) ⇒ Neubau.
|
||||
|
||||
**Ab dann gemessen:** zwei Zähler je Host — auf der **neuen** IP (NAT-Regel: „wer ist schon
|
||||
umgestellt") und auf der **Alt-IP** (`action=passthrough`, `comment="dualaddr-watch: …"`: „ist noch
|
||||
was los"), beide ohne `log`. **Erst wenn der Alt-IP-Zähler über N Tage stillsteht**, wird das Gerät
|
||||
**zum Siedeln vorgemerkt**: neue IP aufs Gerät → NAT + Sekundär-IP + Zähler raus → Alt-IP entfernen
|
||||
→ Doku/Nautobot nachziehen. Alles **ins Soll**, nicht als Transaktion (Christoph: *„wir haben einen
|
||||
Ist- und einen Sollzustand; die Regel kann uns nicht vorschreiben, wie lange wir für die Überführung
|
||||
brauchen"*).
|
||||
|
||||
**Reihenfolge:** `nas` siedelt **zuletzt und gemeinsam** (die meisten Abhängigkeiten: SMB, Time
|
||||
Machine, Caddy-Backend `ds-bf`), `pve`/`macmini` vorher; **`pve2` nur reservieren** (`.120`).
|
||||
|
||||
**Vorabcheck ✅ 2026-09-21:** NAS-NFS (`/etc/exports`, 9 Shares) und iSCSI (`.10`/`.11`:3260) sind
|
||||
IP-ACL'd, haben aber **keine lebenden Nutzer** — pve hat **null** NAS-Mounts (einzige Remote-Storage
|
||||
ist `pbs 10.40.20.135`), kein LXC hängt am NAS, die Synology-Firewall hat keine Regeln ⇒ der
|
||||
src-nat-Umweg (gleich-subnetzige Clients sehen den Core als Quelle) **bricht nichts**.
|
||||
🔴 **Eigener Aufräumposten:** dieselben Exports stehen komplett auf totem **`10.40.41.x`** plus drei
|
||||
`10.0.0.0/8`-Freigaben (offen für jeden 10/8-Client).
|
||||
⚠️ Der NAS trägt **zwei** VLAN20-Adressen (`.10` `ovs_eth0`, `.11` `ovs_eth1`) — laut Christoph
|
||||
haben die beiden nichts miteinander zu tun; `.11` gehört **nicht** in die Umzugslogik.
|
||||
|
||||
🔴 **Warum Nautobot zuerst.** AGH will `.30`/`.31` — dort saß Nautobot (`.30`/`.31`/`.32`/`.34`).
|
||||
**28 Skript-/Config-Dateien** referenzierten `10.40.20.30` (alle `gc-*.py`, 14× `ingest-*.py`,
|
||||
`tp44-*`, `routeros/fetch-core-cutover-config.py`) — alle am 2026-09-21 mitgezogen. Und: **der
|
||||
@@ -2797,6 +2834,66 @@ bleibt als Aufräumposten offen.
|
||||
verschoben). Bei einem **erfolgreichen** Deploy ist das kosmetisch, bei einem **gescheiterten**
|
||||
nimmt es die Fehlermeldung weg — genau dann, wenn man sie braucht.
|
||||
|
||||
#### ✅ Schicht C durch (2026-09-22) — FQDN-Wiederherstellung, vorgezogen (C1–C3, Commit `6d1d56e`)
|
||||
|
||||
Auslöser: die IP-Umstellung (A/B/E) dauert länger; die internen `*.bf.emsenhuber.net`-Namen mussten
|
||||
**sofort** zurück. Drei Züge, jeder am **Gerät** *und* am **Client** belegt — nicht am Plan-Status.
|
||||
|
||||
**C1 — 17 Namen in den Core** (`nautobot/mikrotik_routeros.j2`, Deploy über den neuen GC-Job
|
||||
`tp45-c-dns`). Der Core hält jetzt **27** `/ip dns static` (vorher 10): die 9 Caddy-Frontends
|
||||
(`nautobot` `pbs` `portainer` `ccu` `iobroker` `z2m25` `ha-mcp` `n8n-mcp` `gitea-mcp`) → `.40` ·
|
||||
`tv.bf`→`10.40.40.11` · `mfc.bf`→`10.40.10.6` · **`ap3.bf`→`.99.13`** (existierte in *keiner* Quelle) ·
|
||||
`pve.bf`/`pve-mcp.bf` von Host-IP auf `.40` · dazu die **5 bare Namen** (`vw` `hass` `n8n` `gitea`
|
||||
`ds-bf`) → `.40` (Entscheid 09-22: lokaler Split-Horizon ist **gewollt**, weil `/admin` nur header-frei
|
||||
geht). 🔑 Der Job liest die Zeilen **aus dem Template selbst** und macht daraus einen **idempotenten
|
||||
Upsert** (`:if ([:len [/ip dns static find where name=X]] = 0) do={ … }`) — `add` allein ist nicht
|
||||
wiederholbar. Abnahme: Geräte-Read **27 Statics** · `--refresh-compliance` → **145/145, 0 Drifts**.
|
||||
|
||||
**C2 — AGH entdoppelt** (`adguard/AdGuardHome.yaml`, Weg `python3 adguard/agh-sync.py push`).
|
||||
`upstream_dns` = **nur** `10.40.20.1`; CF-DoH-Zeile und conditional `[/bf.emsenhuber.net/]`-Forward
|
||||
entfallen; **alle 26 Rewrites raus**. Der Core ist damit die **einzige** Quelle — auch für die bare
|
||||
Namen, die vorher über DoH **öffentlich** aufgelöst wurden und den Client an den CF-Edge schickten
|
||||
(dort setzt der Tunnel `CF-Connecting-IP` → `@cf_admin`-Guard → 404). Vorher/Nachher aller 26 Rewrites
|
||||
geprüft: **verhaltensneutral**, identisches Ziel, eine Quelle statt zwei.
|
||||
Abnahme: `agh-sync.py check` → `.36 == .37` · `/admin` **header-frei 200**, mit `CF-Connecting-IP`
|
||||
weiter **404** · Filter intakt (`doubleclick` → `0.0.0.0`) · öffentliche Auflösung intakt.
|
||||
|
||||
**C3 — `ds-bf` über Caddy** (CF-Write am Tunnel `bf-public` `48d3b0b6`): Ingress-Origin von
|
||||
`http://10.40.20.10:8080` auf `https://10.40.20.40:443` + `originRequest.originServerName`. Öffentlich
|
||||
**200 in 0,24 s** (vorher `000` nach 20 s); `vw`/`hass`/`n8n`/`gitea` unverändert. ⚠️ Der **CF-MCP darf
|
||||
lesen, aber nicht schreiben** (`1001: Not authorized` auf `PUT …/configurations`) — der Write lief über
|
||||
`CF_API_TOKEN` aus `mcp/cloudflare/.env` (Wert nie ausgegeben); Original-Ingress vorher gesichert,
|
||||
nachher gegengeprüft.
|
||||
|
||||
**WARP-Abnahme (Christophs Bedingung) ✅.** Unter verbundenem WARP löst `127.0.2.2/.3` den Namen auf
|
||||
**`.40`** auf, die Route geht über `utun8` (Mesh), `/admin` = **200**. Der zuvor gemeldete 404 des Users
|
||||
kam aus dem **Browser-/mDNSResponder-Cache** (dort lag noch die CF-Anycast-A); nach TTL-Ablauf sauber.
|
||||
**Hebel (a) (Zero-Trust Local Domain Fallback) war nicht nötig.**
|
||||
|
||||
🔴 **Befund, bewusst offen — AAAA bleibt öffentlich.** Der Core hält nur **A**-Statics und ist kein
|
||||
Zonen-Eigentümer: eine AAAA-Abfrage **reicht er weiter** und liefert für die bare Namen die **CF-IPv6**
|
||||
(`2a06:98c1::`). **Heute wirkungslos** — im Netz läuft kein globales IPv6 (der Mac hat keine globale
|
||||
Adresse; `curl -6` landet über `::ffff:10.40.20.40`). **Latent:** sobald IPv6 kommt, geht der
|
||||
`/admin`-Weg wieder über die Edge und die Tür ist zu. Christoph-Entscheid 2026-09-22: **dokumentieren,
|
||||
nicht ändern** — `aaaa_disabled: true` in AGH wäre die Ein-Zeilen-Abhilfe, ist aber netzweit „IPv4-only"
|
||||
und passt nicht zu „AGH filtert nur".
|
||||
|
||||
🔴 **Zweiter Befund — AGH ist doppelt adressiert.** LXC 110/111 tragen im Container **zwei**
|
||||
VLAN20-Adressen: `eth0` `.36`/`.37` (aus `net0`) **plus** `eth1` `.30`/`.31`. Schicht A ist dort
|
||||
**halb** umgestellt — die neue IP ist live und wird per DHCP verteilt (`dns-server=10.40.20.30,10.40.20.31`),
|
||||
die Alt-IP lebt noch. Gehört in die „siedeln"-Messung, nicht in C. ℹ️ **Korrektur einer eigenen
|
||||
Fehlannahme:** `adguard/agh-sync.py` nennt `.36`/`.37` — das ist **kein** Drift, sondern die
|
||||
`net0`-Adresse; nur die Konstante kennt die zweite Adresse nicht.
|
||||
|
||||
🧪 **Messfalle, die fast ein Fehlalarm wurde:** `dig nautobot.bf` **ohne** `.emsenhuber.net` fragt den
|
||||
Namen `nautobot.bf.` im **Root** ab und liefert leer — sieht wie ein nicht gefixter Name aus, war aber
|
||||
die eigene Abfrage. Dieselbe Klasse wie `nc -z` (09-01) und `pct push` (09-21): **erst das eigene
|
||||
Werkzeug verdächtigen.**
|
||||
|
||||
**Offen in C:** `coolify.bf` (nicht ausgerollt, Port unbekannt — **nicht raten**) · **C4** (TP34: FQDN
|
||||
als **Daten** in Nautobot). Kosmetisch zurückgestellt: die `ds-bf`-Site steht im `caddy/Caddyfile`
|
||||
weiter im INTERN-Block (nur Beschriftung; ein Caddy-Redeploy allein dafür wäre unverhältnismäßig).
|
||||
|
||||
#### Soll-Oktette (Nachschlagetabelle, s. TP32 für die Herleitung)
|
||||
| Oktett | Rolle | Soll |
|
||||
|---|---|---|
|
||||
@@ -2823,7 +2920,8 @@ NAT/mangle/WG sind Template-Literale, der Firewall-Filter kommt als ConfigContex
|
||||
Lichterketten `.236`/`.237`, Withings `.230`/`.231` … → Christoph: **„VLAN30 nur Pool-Grenze"**.
|
||||
- Pool-Fund: `pool-v20` `.100–.200` hat **`used=0`** (gratis verschiebbar); `pool-v30` `.100–.250`
|
||||
enthält **~25 Statics** → Pool und Statics tauschen praktisch die Plätze.
|
||||
- ⚠️ **`ap3.bf` existiert nirgends** — weder Core-Static noch AGH-Rewrite, obwohl ap3 seit 08-30 live ist.
|
||||
- ⚠️ **`ap3.bf` existierte nirgends** — weder Core-Static noch AGH-Rewrite, obwohl ap3 seit 08-30 live
|
||||
ist. ✅ **Behoben in C1 (09-22):** `ap3.bf` → `.99.13` als Core-Static.
|
||||
|
||||
- **Abh.:** TP32 (Systematik) · TP37 ✅ (Nautobot-Datenmodell) · TP21 ✅. **Blockiert:** TP34 (rendert
|
||||
die FQDNs, die hier Daten werden). **Abgrenzung:** VMID-Umnummerierung bleibt draußen; Token-**Rotation**
|
||||
|
||||
@@ -64,7 +64,12 @@ lokal" — das ist nach dieser Rotation nicht mehr wahr.
|
||||
|
||||
## 4. Fußnote mit Verfallsdatum
|
||||
|
||||
⏰ **Der `CF_API_TOKEN` selbst läuft am 2027-01-09 ab** (Memory `ref_cf_mcp_readonly_token_writes`).
|
||||
⏰ ~~Der `CF_API_TOKEN` selbst läuft am 2027-01-09 ab~~ — **korrigiert 2026-09-21:** das Datum
|
||||
gehörte zum längst **revokten** Alt-Token `9984078812bc…`; das **live** `CF_API_TOKEN`
|
||||
(id `493ed8bd…`) hat **kein** Ablaufdatum (verifiziert via `/accounts/{id}/tokens/verify`,
|
||||
`expires_on: null`). Die Rotation ist damit nicht mehr termin-, sondern **anlassgetrieben**:
|
||||
**Anlass** ist der **Leak vom 2026-09-21**. Sie bleibt **gebündelt** in TP30 (Posten #4) und wird
|
||||
nicht vorgezogen. Memory `ref_cf_mcp_readonly_token_writes` ist nachgezogen.
|
||||
Er ist **account-owned**, verifiziert also über einen anderen Endpoint als ein User-Token — wer das
|
||||
verwechselt, hält ein funktionierendes Token für kaputt. Ohne ihn ist auch diese Rotation nicht mehr
|
||||
möglich, ebensowenig der TP16.3-Schnitt.
|
||||
|
||||
@@ -84,7 +84,7 @@ dns:
|
||||
ipset_file: ""
|
||||
bootstrap_prefer_ipv6: false
|
||||
upstream_timeout: 10s
|
||||
private_<redacted>: []
|
||||
private_networks: []
|
||||
use_private_ptr_resolvers: false
|
||||
local_ptr_upstreams: []
|
||||
use_dns64: false
|
||||
|
||||
@@ -618,7 +618,7 @@ def _tp45_leases_module():
|
||||
return mod
|
||||
|
||||
|
||||
def _lease_move(mac, soll):
|
||||
def _lease_move(mac, soll, tag="tp45-v30"):
|
||||
"""Eine Umzugs-Zeile — EINZEILIG und klammer-vollstaendig (E1015-Lehre, s. _pool_set).
|
||||
|
||||
Der Selektor wird GEZAEHLT: ein `find where`, das 0 trifft, sieht wie
|
||||
@@ -653,8 +653,8 @@ def _lease_move(mac, soll):
|
||||
arg = f'[find where mac-address={mac}]'
|
||||
return (f':if ([:len [/ip dhcp-server lease {find}]] = 1) do={{ '
|
||||
f'/ip dhcp-server lease set {arg} address={soll}; '
|
||||
f':log info ("tp45-v30: {mac} -> {soll}") }} '
|
||||
f'else={{ :log info ("tp45-v30: {mac} NICHT EINDEUTIG -- nicht angefasst") }}')
|
||||
f':log info ("{tag}: {mac} -> {soll}") }} '
|
||||
f'else={{ :log info ("{tag}: {mac} NICHT EINDEUTIG -- nicht angefasst") }}')
|
||||
|
||||
|
||||
def job_tp45_v30_leases():
|
||||
@@ -723,6 +723,209 @@ def job_tp45_v30_leases():
|
||||
return [(["crs326-core"], "\n".join(lines), "TP45-v30-leases")]
|
||||
|
||||
|
||||
def job_tp45_v30_revert():
|
||||
"""RUECKNAHME 2026-09-21: die 26 Shelly-Reservierungen zurueck auf ihre ALTEN Adressen.
|
||||
|
||||
Christoph-Direktive, sofort: *„mach die shellys sofort rueckgaengig! das gibt
|
||||
sonst ein riesen durcheinander mit home assistant"*. Grund: HA hat die Shellys
|
||||
mit ihren alten Adressen eingebunden -- ein Umzug zerreisst die Integrationen.
|
||||
|
||||
🔴 **NUR die Shellys** — das war die erste, eng gefasste Ruecknahme. Die
|
||||
uebrigen 10 Umzuege aus `job_tp45_v30_leases` (NukiBridge .11, P750W .13,
|
||||
DTUBI .14, NukiSmartlock .177, SwitchBot .178, Twinkly .179, lichterketten
|
||||
.190/.191, Withings .195/.196) und die beiden Sonderfaelle (CCU-Leiche
|
||||
entfernt, NSPanelPro statisch auf .12) liess er unangetastet.
|
||||
|
||||
⚠️ **Noch am selben Tag ueberholt:** auf die weitergehende Direktive
|
||||
(„wir sollten ALLE vlan30-aenderungen rueckgaengig machen" — Sperr-Risiko
|
||||
Nuki) folgte `job_tp45_v30_restore`, der auch diese 10 plus beide
|
||||
Sonderfaelle zurueckdreht. Dieser Job bleibt als der engere, zuerst
|
||||
gelaufene Schritt bestehen.
|
||||
|
||||
Die Tabelle steht in `tp45-leases.py` als `REVERT` (abgeleitet aus `MOVES`,
|
||||
gefiltert auf die 26 Shelly-MACs) -- NICHT hier, aus demselben Grund wie
|
||||
`MOVES`: eine zweite Kopie laeuft still auseinander.
|
||||
|
||||
⚠️ **Reihenfolge:** erst `python3 nautobot/tp45-leases.py revert --apply`
|
||||
(Soll zurueck), DANN diesen Job. Umgekehrt meldet GC sofort wieder Drift --
|
||||
das Geraet waere richtig und das Soll falsch.
|
||||
|
||||
Abnahme: Geraete-Read `proplist=address,comment where server=dhcp-v30` -- die
|
||||
26 Shellys muessen auf .202-.226/.233 stehen, die uebrigen 13 Eintraege
|
||||
unveraendert. Danach `--refresh-compliance`, erwartet 145/145 und 0 Drifts.
|
||||
"""
|
||||
mod = _tp45_leases_module()
|
||||
lines = [_lease_move(mac, alt, tag="tp45-v30-revert")
|
||||
for mac, (_neu, alt) in sorted(mod.REVERT.items())]
|
||||
|
||||
# Abnahme-Zeile: die alten Baender muessen wieder voll sein, die neuen leer.
|
||||
# Bewusst ein Bereichs-Paar statt aller 26 Adressen -- .202/.233 rahmen den
|
||||
# alten Shelly-Block ein, .151/.176 den neuen.
|
||||
lines.append(
|
||||
':log info ("tp45-v30-revert: .151=" . [:tostr [:len [/ip dhcp-server lease '
|
||||
'find where address=10.40.30.151]]] . " .176=" . [:tostr [:len '
|
||||
'[/ip dhcp-server lease find where address=10.40.30.176]]] . " .202=" . '
|
||||
'[:tostr [:len [/ip dhcp-server lease find where address=10.40.30.202]]] . '
|
||||
' " .233=" . [:tostr [:len [/ip dhcp-server lease '
|
||||
'find where address=10.40.30.233]]])')
|
||||
|
||||
return [(["crs326-core"], "\n".join(lines), "TP45-v30-shelly-revert")]
|
||||
|
||||
|
||||
def job_tp45_v30_restore():
|
||||
"""VOLLRUECKNAHME 2026-09-21: alle VLAN30-Aenderungen aus B2 zuruecknehmen.
|
||||
|
||||
Christoph-Direktive: *„wir sollten alle vlan30-aenderungen rueckgaengig machen.
|
||||
du koenntest mich auf diesem wege aus meiner wohnung aussperren."*
|
||||
|
||||
Was B2 am 2026-09-21 getan hat, und was hier einzeln zurueckgedreht wird:
|
||||
|
||||
1. **36 Reservierungs-Adressen** verschoben -> hier ALLE 36 zurueck auf ihre
|
||||
`MOVES`-Ist-Adresse (idempotent: die 26 Shellys stehen schon dort, ein
|
||||
zweites `set` auf dieselbe Adresse ist ein No-Op).
|
||||
2. **CCU-Leiche entfernt** (`BC:24:11:8E:0A:57`, .30.232) -> wieder angelegt,
|
||||
wortgleich der Stand vor B2. Ob die Leiche spaeter geloescht werden soll,
|
||||
ist eine **eigene** Entscheidung, nicht Teil dieser Ruecknahme.
|
||||
3. **SONOFF-NSPanelPro auf statisch .12 gepinnt** -> Reservierung entfernt;
|
||||
vor B2 hatte es keine (reiner DHCP-Client auf .249). `remove` ist der
|
||||
Weg, weil RouterOS 7.24 kein `make-dynamic` kennt (s. u.).
|
||||
|
||||
⚠️ **Reihenfolge:** erst `python3 nautobot/tp45-leases.py restore --apply`
|
||||
(Soll zurueck), DANN diesen Job.
|
||||
|
||||
🔴 **Warum die Eile berechtigt war:** die Reservierung eines Geraets mit
|
||||
**lokaler** IP zu aendern, bewegt das Geraet nicht — aber bei einem Geraet,
|
||||
das **DHCP** nutzt, haette die naechste Erneuerung es auf die neue Adresse
|
||||
gezogen, und IP-gebundene Einbindungen (Home Assistant: Nuki, Shelly, …)
|
||||
waeren gerissen. Dass hier nichts passiert ist, war Glueck, kein Entwurf.
|
||||
|
||||
Abnahme: Geraete-Read der v30-Leases + ARP-Band-Zaehlung; danach
|
||||
`--refresh-compliance`, erwartet 145/145 und 0 Drifts.
|
||||
"""
|
||||
mod = _tp45_leases_module()
|
||||
lines = [_lease_move(mac, ist, tag="tp45-v30-restore")
|
||||
for mac, (ist, _soll) in sorted(mod.MOVES.items())]
|
||||
|
||||
# NSPanelPro: die statische Reservierung ENTFERNEN -- vor B2 hatte es keine,
|
||||
# es war ein reiner DHCP-Client (dynamische Lease auf .249).
|
||||
#
|
||||
# 🔴 `make-dynamic` gibt es in RouterOS 7.24 NICHT: `/ip dhcp-server lease
|
||||
# make-dynamic` antwortet `bad command name make-dynamic` (live gemessen
|
||||
# 2026-09-21). Der erste Versuch dieses Jobs ist daran gestorben und hat den
|
||||
# Plan auf `Failed` gesetzt, OBWOHL alle uebrigen Statements gelandet sind —
|
||||
# genau die Klasse „Completed/Failed beweist nichts, das Geraet ist die
|
||||
# Wahrheit". `make-static` existiert (B2 hat es benutzt), das Gegenstueck
|
||||
# fehlt; der Rueckweg ist `remove`.
|
||||
#
|
||||
# ⚠️ Das Panel haelt .12 noch bis zum Lease-Ablauf (~20 h, `status=bound`,
|
||||
# `active-address=10.40.30.12`) und faellt danach in den Pool zurueck.
|
||||
# Das ist eine von B2 erzeugte Uebergangserscheinung, keine neue Aenderung.
|
||||
for add in mod.ADDS:
|
||||
mac = re.search(r"mac-address=(\S+)", add).group(1)
|
||||
find = f'find where mac-address={mac}'
|
||||
arg = f'[find where mac-address={mac}]'
|
||||
lines.append(f':if ([:len [/ip dhcp-server lease {find}]] = 1) do={{ '
|
||||
f'/ip dhcp-server lease remove {arg}; '
|
||||
f':log info ("tp45-v30-restore: NSPanelPro-Reservierung entfernt") }} '
|
||||
f'else={{ :log info ("tp45-v30-restore: NSPanelPro hat keine Reservierung") }}')
|
||||
|
||||
# CCU-Leiche wieder anlegen -- nur wenn sie fehlt.
|
||||
for mac in sorted(mod.REMOVE):
|
||||
find = f'find where mac-address={mac}'
|
||||
lines.append(f':if ([:len [/ip dhcp-server lease {find}]] = 0) do={{ '
|
||||
f'/ip dhcp-server lease {mod.RESTORE_ADD}; '
|
||||
f':log info ("tp45-v30-restore: CCU-Leiche wieder angelegt") }} '
|
||||
f'else={{ :log info ("tp45-v30-restore: CCU-Leiche ist schon da") }}')
|
||||
|
||||
# Abnahme: die alten Baender muessen voll sein, die neuen leer.
|
||||
lines.append(
|
||||
':log info ("tp45-v30-restore: alt .202=" . [:tostr [:len [/ip dhcp-server lease '
|
||||
'find where address=10.40.30.202]]] . " .233=" . [:tostr [:len '
|
||||
'[/ip dhcp-server lease find where address=10.40.30.233]]] . " .244=" . '
|
||||
'[:tostr [:len [/ip dhcp-server lease find where address=10.40.30.244]]] . '
|
||||
' " neu .11=" . [:tostr [:len [/ip dhcp-server lease '
|
||||
'find where address=10.40.30.11]]] . " .151=" . [:tostr [:len '
|
||||
'[/ip dhcp-server lease find where address=10.40.30.151]]] . " .177=" . '
|
||||
'[:tostr [:len [/ip dhcp-server lease find where address=10.40.30.177]]])')
|
||||
|
||||
return [(["crs326-core"], "\n".join(lines), "TP45-v30-restore")]
|
||||
|
||||
|
||||
def _dns_static_lines():
|
||||
"""Die `/ip dns static add`-Zeilen aus dem GC-Template, wortgetreu.
|
||||
|
||||
🔴 **Quelle ist das Template selbst, NICHT eine zweite Kopie.** Eine handgepflegte
|
||||
Liste hier liefe beim ersten Template-Edit auseinander — still, weil beide Seiten
|
||||
fuer sich plausibel aussehen; am Geraet faellt nur auf, dass ein Name fehlt.
|
||||
Und die Kommentare muessen **zeichengleich** sein: Golden Config vergleicht
|
||||
`comment=` mit, ein abweichender Text ist eine Drift.
|
||||
|
||||
Der `/ip dns static`-Block ist ein Literal im Template (im Gegensatz zu
|
||||
`firewall_filter` und `dhcp_leases`, die als Daten aus
|
||||
`local_config_context_data` kommen) — deshalb ist das Parsen hier zulaessig.
|
||||
"""
|
||||
text = (pathlib.Path(__file__).with_name("mikrotik_routeros.j2")).read_text()
|
||||
return [ln.strip() for ln in text.splitlines()
|
||||
if ln.strip().startswith("/ip dns static add ")]
|
||||
|
||||
|
||||
def _dns_upsert(line):
|
||||
"""Eine Template-Zeile -> idempotenter Upsert, EINZEILIG und klammer-vollstaendig.
|
||||
|
||||
`/ip dns static add` ist NICHT idempotent — ein zweiter Lauf legte den Namen ein
|
||||
zweites Mal an. Deshalb: nur anlegen, wenn der Name fehlt, sonst in place setzen.
|
||||
Der Selektor wird wie in `_lease_move` gezaehlt (`find where …` in `[:len …]`,
|
||||
`[find where …]` als Argument) — die vertauschte Form ist ein Syntaxfehler, der
|
||||
als `Completed` durchlaeuft und das Geraet unveraendert laesst.
|
||||
"""
|
||||
addr = re.search(r"address=(\S+)", line).group(1)
|
||||
name = re.search(r"name=(\S+)", line).group(1)
|
||||
cm = re.search(r'comment=("(?:[^"\\]|\\.)*"|\S+)', line)
|
||||
comment = f" comment={cm.group(1)}" if cm else ""
|
||||
return (f':if ([:len [/ip dns static find where name={name}]] = 0) do={{ '
|
||||
f'{line} }} '
|
||||
f'else={{ /ip dns static set [find where name={name}] address={addr}{comment} }}')
|
||||
|
||||
|
||||
def job_tp45_c_dns():
|
||||
"""TP45 Schicht C (2026-09-22): die internen Namen in den CORE — der FQDN-Fix.
|
||||
|
||||
Hintergrund: AGH wertet seine DNS-Rewrites nur im **Filter-Pfad** aus. Fuer
|
||||
unfiltrierte Clients (`dns-bypass-exempt` = beide Mac-NICs, `grp-infra`,
|
||||
`grp-media`, `grp-mgmt`) greifen sie nicht, und die fallen auf den conditional
|
||||
forward `[/bf.emsenhuber.net/] -> 10.40.20.1` zurueck — den Core. Der hielt
|
||||
aber nur 10 Statics; 12 Namen existierten nur als AGH-Rewrite und waren damit
|
||||
**NXDOMAIN**. Vom Client gemessen 2026-09-22: alle 12 leer, `vw.emsenhuber.net`
|
||||
lieferte die Cloudflare-Anycast-IP statt `.40` — und genau daher kam der
|
||||
404 auf `/admin` (der CF-Weg setzt `CF-Connecting-IP`, der `@cf_admin`-Guard
|
||||
antwortet darauf mit 404).
|
||||
|
||||
Dieser Job bringt die Statics auf das Geraet. Danach beantwortet der Core alle
|
||||
internen Namen unabhaengig vom Filterstatus des Clients — und, weil die Namen
|
||||
ueber Caddy laufen, mit gueltigem TLS.
|
||||
|
||||
⚠️ **Die 5 bare Namen (`vw`, `hass`, `n8n`, `gitea`, `ds-bf`) kommen bewusst mit
|
||||
auf `.40`** (Christoph-Entscheid 2026-09-22): der lokale, header-freie Weg ist
|
||||
die einzige Tuer zum Vault-Admin. ⚠️ Nebenwirkung: diese Namen werden damit fuer
|
||||
**alle** VLANs aufloesbar, nicht nur fuer MAIN — MEDIA (`*A`) und GUEST
|
||||
(`*B`+`*A`) erreichen `.40:443` ebenfalls. Einhegung ist ein eigener Schritt.
|
||||
|
||||
Rückweg: die Vorher-Werte stehen als `A1`/`A2`-Tabellen im Plan
|
||||
(`~/.claude/plans/…`); vor dem Deploy liegt ein Backup auf dem Core.
|
||||
"""
|
||||
lines = [_dns_upsert(ln) for ln in _dns_static_lines()]
|
||||
# Abnahme-Zeile: die Zahl der Statics plus drei Stichproben, die den Ausfall
|
||||
# repraesentieren (ap3 gab es vorher nirgends, pve zeigt auf Caddy, vw muss
|
||||
# `.40` sein — nicht die CF-Anycast-IP).
|
||||
lines.append(
|
||||
':do { :log info ("tp45-c-dns: statics=" . [:tostr [:len [/ip dns static find]]] '
|
||||
'. " ap3=" . [:tostr [:len [/ip dns static find where name=ap3.bf.emsenhuber.net]]] '
|
||||
'. " vw=" . [/ip dns static get [find where name=vw.emsenhuber.net] address] '
|
||||
'. " pve=" . [/ip dns static get [find where name=pve.bf.emsenhuber.net] address]) } '
|
||||
f'on-error={{ :log info "tp45-c-dns: Abnahme-Zeile geworfen" }}')
|
||||
return [(["crs326-core"], "\n".join(lines), "TP45-c-dns")]
|
||||
|
||||
|
||||
JOBS = {
|
||||
"drift-cleanup": job_drift_cleanup,
|
||||
"mgmt-acl": job_mgmt_acl,
|
||||
@@ -730,8 +933,11 @@ JOBS = {
|
||||
"tp38-oob": job_tp38_oob,
|
||||
"tp45-agh-fw": job_tp45_agh_fw,
|
||||
"tp45-agh-dhcp": job_tp45_agh_dhcp,
|
||||
"tp45-c-dns": job_tp45_c_dns,
|
||||
"tp45-pool-v20": job_tp45_pool_v20,
|
||||
"tp45-v30-leases": job_tp45_v30_leases,
|
||||
"tp45-v30-revert": job_tp45_v30_revert,
|
||||
"tp45-v30-restore": job_tp45_v30_restore,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -312,18 +312,49 @@
|
||||
# FT-Transition statt Voll-Reassoziation. Das Band-Springen selbst endet erst
|
||||
# mit dem Deckungsfix (TP25 Variante 3). Siehe teilprojekte.md TP25 (Stand 09-14).
|
||||
|
||||
# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich):
|
||||
# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12
|
||||
# Kanonischer Mgmt-Nummernplan (User-Bestaetigung 2026-08-13, verbindlich; ap3 ergaenzt 2026-09-22):
|
||||
# core .99.1 | branch .99.2 | ap1 .99.11 | ap2 .99.12 | ap3 .99.13
|
||||
#
|
||||
# 🔴 TP45 Schicht C (2026-09-22): DIESE Liste ist die EINZIGE Quelle fuer interne Namen.
|
||||
# AGH wertet seine DNS-Rewrites nur im Filter-Pfad aus; fuer unfiltrierte Clients
|
||||
# (dns-bypass-exempt, grp-infra/-media/-mgmt) greifen sie nicht — die landen hier.
|
||||
# Hier fehlten 12 Namen, genau das war der FQDN-Ausfall.
|
||||
# Merkregel: was in AGH als Rewrite steht, muss auch hier stehen.
|
||||
/ip dns static add address=10.40.99.1 comment="crs326-core (L3)" name=core.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.99.2 comment=crs326-branch name=branch.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.99.11 comment="ap1 = reiner AP" name=ap1.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.99.12 comment="ap2 = reiner AP" name=ap2.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.99.13 comment="ap3 = reiner CAP (cAP ax)" name=ap3.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.167 name=macmini.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.100 name=pve.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.173 name=pve-mcp.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.10 name=nas.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.126 name=docker.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.20 name=platform-pg.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.10.6 comment="Brother MFC (VLAN10)" name=mfc.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.40.11 comment="TV (VLAN40)" name=tv.bf.emsenhuber.net type=A
|
||||
|
||||
# Caddy-Frontends (TP31): Dienste mit eigener Site laufen ueber den Reverse Proxy, damit das
|
||||
# TLS-Zertifikat gueltig ist. Quelle: caddy/Caddyfile (live geprueft 2026-09-22).
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> nautobot :8080" name=nautobot.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> pbs :8007" name=pbs.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> portainer :9443" name=portainer.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> ccu :80" name=ccu.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> iobroker :8081" name=iobroker.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> z2m25 :8080" name=z2m25.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> home-assistant mcp :9583" name=ha-mcp.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> n8n-mcp :3001" name=n8n-mcp.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> gitea-mcp :3000" name=gitea-mcp.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> pve :8006" name=pve.bf.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="caddy -> pve-mcp :3000" name=pve-mcp.bf.emsenhuber.net type=A
|
||||
|
||||
# Oeffentliche Namen (bare emsenhuber.net) — real via CF Access + Tunnel. Sie stehen hier NUR,
|
||||
# damit LAN-Clients direkt auf Caddy kommen: der CF-Weg setzt CF-Connecting-IP, und der
|
||||
# @cf_admin-Guard in caddy/Caddyfile antwortet darauf mit 404 (TP41 T2). Der lokale,
|
||||
# header-freie Weg ist die einzige Tuer zum Vault-Admin (Christoph-Entscheid 2026-09-22).
|
||||
/ip dns static add address=10.40.20.40 comment="vw via caddy (Vault-Admin nur lokal)" name=vw.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="hass via caddy" name=hass.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="n8n via caddy" name=n8n.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="gitea via caddy" name=gitea.emsenhuber.net type=A
|
||||
/ip dns static add address=10.40.20.40 comment="ds-bf via caddy" name=ds-bf.emsenhuber.net type=A
|
||||
|
||||
# [7] NAT
|
||||
/ip firewall nat add action=accept chain=srcnat comment="TP4: no-NAT inter-VLAN (LAN-internal, before masquerade)" dst-address=10.40.0.0/16
|
||||
|
||||
@@ -49,6 +49,10 @@ Drift mehr (genau der Grund, warum `ingest-core-firewall.py` manuell bleibt).
|
||||
python3 nautobot/tp45-leases.py seed --force --apply # Neuschnitt, ueberschreibt
|
||||
python3 nautobot/tp45-leases.py move # Dry-Run, s. MOVES
|
||||
python3 nautobot/tp45-leases.py move --apply # B2
|
||||
python3 nautobot/tp45-leases.py revert # Dry-Run, s. REVERT (nur Shellys)
|
||||
python3 nautobot/tp45-leases.py revert --apply # Ruecknahme 2026-09-21
|
||||
python3 nautobot/tp45-leases.py restore # Dry-Run: ALLES zurueck auf vor B2
|
||||
python3 nautobot/tp45-leases.py restore --apply # Vollruecknahme 2026-09-21
|
||||
|
||||
Token: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored), NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||
Der Wert wird nie ausgegeben.
|
||||
@@ -116,12 +120,49 @@ MOVES = {
|
||||
# .150 (Ender3V3KE) und .180 (espressif/Airfryer) bleiben, wo sie sind.
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RUECKNAHME 2026-09-21 (Christoph-Direktive, sofort): NUR die Shellys.
|
||||
#
|
||||
# Die 26 Shelly-Reservierungen gehen auf ihre ALTEN Adressen zurueck. Grund:
|
||||
# Home Assistant hat sie mit den alten Adressen eingebunden -- der Umzug haette
|
||||
# die HA-Integrationen zerrissen. Die uebrigen 10 Umzuege (NukiBridge, P750W,
|
||||
# DTUBI, NukiSmartlock, SwitchBot, Twinkly, Lichterketten, Withings) BLEIBEN.
|
||||
#
|
||||
# Bewusst als eigene Menge und nicht als Bereichs-Ableitung: welche MAC eine
|
||||
# Shelly ist, steht nur im Kommentar der Soll-Zeile, nicht im MOVES-Key.
|
||||
# ---------------------------------------------------------------------------
|
||||
SHELLY = {
|
||||
"E4:B0:63:D7:C7:A4", "28:37:2F:36:F4:7C", "E4:B0:63:E4:EF:7C",
|
||||
"E4:B0:63:E4:0F:30", "D8:85:AC:0C:5C:C8", "D0:CF:13:D8:18:FC",
|
||||
"D0:CF:13:D9:94:E8", "E4:B0:63:FA:FD:E0", "E8:F6:0A:61:88:08",
|
||||
"E4:B0:63:FC:57:E4", "E4:B0:63:FA:6C:00", "48:F6:EE:C8:E6:AC",
|
||||
"48:F6:EE:DD:6A:F4", "E4:B0:63:FC:59:48", "E4:B0:63:FA:D5:34",
|
||||
"10:20:BA:FA:F5:40", "10:20:BA:FB:6D:64", "48:F6:EE:CC:16:E4",
|
||||
"10:20:BA:FB:70:FC", "98:A3:16:7C:5F:EC", "20:6E:F1:01:F9:F8",
|
||||
"A0:85:E3:C8:65:34", "AC:15:18:7B:62:20", "14:33:5C:D8:2D:7C",
|
||||
"98:A3:16:79:38:50", "98:A3:16:7A:A3:54",
|
||||
}
|
||||
if len(SHELLY) != 26:
|
||||
raise SystemExit("SHELLY hat %d MACs statt 26 -- Tabelle beschaedigt." % len(SHELLY))
|
||||
# Schluessel = MAC, Wert = (aktuelle Soll-Adresse, Adresse davor) -- also die
|
||||
# Umkehrung von MOVES, aber nur fuer den Shelly-Block.
|
||||
REVERT = {mac: (soll, ist) for mac, (ist, soll) in MOVES.items() if mac in SHELLY}
|
||||
if len(REVERT) != 26:
|
||||
raise SystemExit("REVERT hat %d Eintraege statt 26 -- MOVES/SHELLY passen nicht zusammen."
|
||||
% len(REVERT))
|
||||
|
||||
# CCU-Leiche: VLAN30-Reservierung `pve-homematic-raspi-ccu`, status=waiting,
|
||||
# last-seen=never. Die CCU ist live in VLAN20 (10.40.20.232, ARP+HTTP belegt,
|
||||
# Caddys ccu.bf proxyt dorthin) — dieser Eintrag ist ein Zombie aus dem
|
||||
# Core-Cutover. Er wird ENTFERNT, nicht umgezogen.
|
||||
REMOVE = {"BC:24:11:8E:0A:57"}
|
||||
|
||||
# Die Zeile, mit der B2 die CCU-Leiche entfernt hat -- wortgleich der Zustand
|
||||
# VOR B2, Quelle `routeros/tp21-core-dhcp-from-ap1.rsc` Z. 70 (dort ohne
|
||||
# `disabled`). Fuer den vollstaendigen Rueckweg wird sie wieder angelegt.
|
||||
RESTORE_ADD = ("add address=10.40.30.232 comment=pve-homematic-raspi-ccu "
|
||||
"mac-address=BC:24:11:8E:0A:57 server=dhcp-v30")
|
||||
|
||||
# SONOFF-NSPanelPro hat KEINE Reservierung, nur eine dynamische Lease auf .249.
|
||||
# Wird in B2 per make-static + set address gepinnt. Zeile hier nur als Soll.
|
||||
ADDS = [
|
||||
@@ -207,7 +248,7 @@ def show(old, new):
|
||||
|
||||
def main():
|
||||
argv = sys.argv[1:]
|
||||
if not argv or argv[0] not in ("seed", "move", "check"):
|
||||
if not argv or argv[0] not in ("seed", "move", "revert", "restore", "check"):
|
||||
raise SystemExit(__doc__.strip().splitlines()[-2].strip())
|
||||
cmd, apply_ = argv[0], "--apply" in argv
|
||||
dev = device()
|
||||
@@ -232,6 +273,57 @@ def main():
|
||||
new, when = backup_lines()
|
||||
print("== seed aus Backup %s (%d Zeilen)%s =="
|
||||
% (when, len(new), " [--force, ueberschreibt %d Zeilen]" % len(soll) if soll else ""))
|
||||
elif cmd == "restore":
|
||||
if not soll:
|
||||
raise SystemExit("Soll ist leer — nichts wiederherzustellen.")
|
||||
drop = {mac_of(a) for a in ADDS}
|
||||
new, seen, moved, dropped = [], set(), [], []
|
||||
for line in soll:
|
||||
mac = mac_of(line)
|
||||
if mac in drop:
|
||||
dropped.append(mac)
|
||||
continue
|
||||
if mac in MOVES:
|
||||
ist_ip, soll_ip = MOVES[mac]
|
||||
cur = re.search(r"address=(\S+)", line).group(1)
|
||||
if cur != ist_ip:
|
||||
if cur != soll_ip:
|
||||
raise SystemExit("%s: Soll traegt weder %s noch %s — Soll, MOVES und "
|
||||
"Wirklichkeit auseinander. Zeile: %s"
|
||||
% (mac, ist_ip, soll_ip, line[:120]))
|
||||
line = set_address(line, ist_ip)
|
||||
moved.append(mac)
|
||||
seen.add(mac)
|
||||
new.append(line)
|
||||
fehlend = set(MOVES) - seen
|
||||
if fehlend:
|
||||
raise SystemExit("MOVES nennt MACs, die im Soll fehlen: %s"
|
||||
% ", ".join(sorted(fehlend)))
|
||||
for mac in sorted(REMOVE):
|
||||
if mac not in seen:
|
||||
new.append(RESTORE_ADD)
|
||||
moved.append(mac)
|
||||
print("== restore: %d Adressen zurueck, %d Zeilen entfernt, %d wieder angelegt "
|
||||
"(Soll jetzt %d Zeilen) ==" % (len(moved), len(dropped), len(REMOVE), len(new)))
|
||||
elif cmd == "revert":
|
||||
if not soll:
|
||||
raise SystemExit("Soll ist leer — es gibt nichts zurueckzunehmen.")
|
||||
new, seen = [], set()
|
||||
for line in soll:
|
||||
mac = mac_of(line)
|
||||
if mac in REVERT:
|
||||
neu_ip, alt_ip = REVERT[mac]
|
||||
if re.search(r"address=(\S+)", line).group(1) != neu_ip:
|
||||
raise SystemExit("%s: Soll traegt nicht %s — Soll und REVERT auseinander. "
|
||||
"Zeile: %s" % (mac, neu_ip, line[:120]))
|
||||
line = set_address(line, alt_ip)
|
||||
seen.add(mac)
|
||||
new.append(line)
|
||||
if seen != set(REVERT):
|
||||
raise SystemExit("REVERT nennt MACs, die im Soll fehlen: %s"
|
||||
% ", ".join(sorted(set(REVERT) - seen)))
|
||||
print("== revert: %d Shelly-Reservierungen zurueck auf die alten Adressen =="
|
||||
% len(REVERT))
|
||||
else:
|
||||
if not soll:
|
||||
raise SystemExit("Soll ist leer — erst `seed` laufen lassen (Schicht B1).")
|
||||
|
||||
Reference in New Issue
Block a user