TP45 Schicht A: Nautobot .30/.31/.32/.34 -> .154/.155/.156/.158
Oktett-Klasse .150-.199 (App-Services). Nautobot gibt damit .30/.31 an AGH frei. .157 ist gitea, deshalb nimmt redis .158. - Datenmigration ueber neues nautobot/tp45-ip-move.py: Nautobot verbietet das Aendern einer IP nach dem Anlegen -> anlegen/primary_ip4 umhaengen/ Altobjekt loeschen, in dieser Reihenfolge - Stack-Redeploy (PUT /api/stacks/203), Caddy-Backend, Core-Literal *24 ueber GC deployt (Plan 51a90e25) mit Wirksamkeitspruefung am Geraet - caddy/deploy.sh: push_verified() ersetzt den push||push-Fallback. `pct push` meldet Exit 0 AUCH wenn es die Quelle nicht lesen kann, der Fallback griff deshalb nie und `caddy validate` pruefte die ALTE Datei -> gruene Abnahme bei unveraendertem Zustand (Symptom: 502 bei gesundem Backend) - 39 Referenzen in 28 Dateien mitgezogen; lebende Doku nachgezogen (CLAUDE.md, ip-plan.md, cf-tunnel-mapping §0-Kopf, TP16.3/TP31-Prosa) - GC-Compliance 140/140, 0 Drifts Neue Funde (TP45-Prosa + CLAUDE.md): - Die Core-Adressliste `nautobot` wird von GC nirgends gerendert -> ein Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt. Gleiche Klasse wie die fehlenden DHCP-Reservierungen. - Regel *24 (Literal) und *28 (Liste) sind Dubletten; der Literal steht bei 0 Paketen. Zusammenlegung bleibt offen. - gc-manual-plan.py: /api/extras/job-log-entries/ liefert 404 (Nautobot 3.x) -> bei gescheitertem Deploy fehlt die Fehlermeldung. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
21
CLAUDE.md
21
CLAUDE.md
@@ -118,7 +118,7 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
|
|||||||
| **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. |
|
| **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. |
|
||||||
| NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) |
|
| NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) |
|
||||||
| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** |
|
| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** |
|
||||||
| Nautobot (Portainer-Stack, NAS) | 10.40.20.30 | Portainer ep14, Stack Id 203 · **TP21 ✅ 2026-07-16: `ghcr.io/nautobot/nautobot:3.1.7`, DB=platform-pg, Creds in `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored)** · **TP22 ✅ 2026-08-13: Golden-Config-App + Device-Adoption DURCH** — alle 4 MikroTik adoptiert, Deploy-Pipeline scharf (Image `3.1.7-gc9`), **68/68 Compliance-Checks grün / 0 Drifts** (2026-08-16) |
|
| Nautobot (Portainer-Stack, NAS) | **10.40.20.154** (TP45 Schicht A 2026-09-21: `.30/.31/.32/.34` → `.154/.155/.156/.158`) | Portainer ep14, Stack Id 203 · **TP21 ✅ 2026-07-16: `ghcr.io/nautobot/nautobot:3.1.7`, DB=platform-pg, Creds in `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored)** · **TP22 ✅ 2026-08-13: Golden-Config-App + Device-Adoption DURCH** — alle 4 MikroTik adoptiert, Deploy-Pipeline scharf (Image `3.1.7-gc9`), **68/68 Compliance-Checks grün / 0 Drifts** (2026-08-16) |
|
||||||
| ~~Omada-Controller~~ | — | ❌ **entfernt 2026-07-16** (User hat den Container gelöscht — overkill für 1 Switch auf der Abschussliste, s. TP2.1/TP15) |
|
| ~~Omada-Controller~~ | — | ❌ **entfernt 2026-07-16** (User hat den Container gelöscht — overkill für 1 Switch auf der Abschussliste, s. TP2.1/TP15) |
|
||||||
| ~~omada-mcp~~ | — | ❌ **entfernt 2026-07-16** — mit dem Controller gelöscht. ✅ **Aufräumen erledigt/gegenstandslos (geprüft 2026-08-16):** nicht mehr in `mcpServers` registriert (nur noch als „disabled" in ein paar Projekt-Scopes = harmloser Merkposten), und **kein `omada`-Hostname mehr im CF-Ingress** |
|
| ~~omada-mcp~~ | — | ❌ **entfernt 2026-07-16** — mit dem Controller gelöscht. ✅ **Aufräumen erledigt/gegenstandslos (geprüft 2026-08-16):** nicht mehr in `mcpServers` registriert (nur noch als „disabled" in ein paar Projekt-Scopes = harmloser Merkposten), und **kein `omada`-Hostname mehr im CF-Ingress** |
|
||||||
| pve-mcp | 10.40.20.173 | **StreamableHTTP `http://10.40.20.173:3000/` (Root-Pfad, NICHT /sse)** · NAS-Stack ep14 (=nas-bf `.20.10`, NICHT LXC .126!) id 202 (vlan20-only, TP14.1 2026-07-10) · **mcp-pve v0.6.1 (Upgrade 2026-07-10: Single-Session-Bug 0.4.8 „Server already initialized" gefixt, TP14.2a)** · `~/.claude.json` ✓ |
|
| pve-mcp | 10.40.20.173 | **StreamableHTTP `http://10.40.20.173:3000/` (Root-Pfad, NICHT /sse)** · NAS-Stack ep14 (=nas-bf `.20.10`, NICHT LXC .126!) id 202 (vlan20-only, TP14.1 2026-07-10) · **mcp-pve v0.6.1 (Upgrade 2026-07-10: Single-Session-Bug 0.4.8 „Server already initialized" gefixt, TP14.2a)** · `~/.claude.json` ✓ |
|
||||||
@@ -156,6 +156,25 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
|
|||||||
⚠️ **`POST /api/plugins/golden-config/config-plan/` ist `405` — das ViewSet ist read-only.** ConfigPlans
|
⚠️ **`POST /api/plugins/golden-config/config-plan/` ist `405` — das ViewSet ist read-only.** ConfigPlans
|
||||||
entstehen nur über den Job **`Generate Config Plans`** (`plan_type=manual` + `commands`-TextVar).
|
entstehen nur über den Job **`Generate Config Plans`** (`plan_type=manual` + `commands`-TextVar).
|
||||||
- **Docker-Management:** Portainer API `https://10.40.20.126:9443`, Key in `mcp/portainer/.env`.
|
- **Docker-Management:** Portainer API `https://10.40.20.126:9443`, Key in `mcp/portainer/.env`.
|
||||||
|
- 🔴 **`pct push` meldet Exit 0, auch wenn es die Quelldatei NICHT lesen kann** (gemessen 2026-09-21).
|
||||||
|
`pct push 130 <fehlende-datei> /etc/caddy/Caddyfile` → `failed to open … for reading`, **`rc=0`**.
|
||||||
|
Ein `&&`/`||` hinter dem Push **beweist nichts**, und ein anschließendes `validate`/`reload` prüft
|
||||||
|
dann die **alte** Datei → **grüne Abnahme bei unverändertem Zustand**. Genau so sah der
|
||||||
|
Nautobot-Umzug am 2026-09-21 „gelandet" aus, während live noch die alte Backend-IP stand
|
||||||
|
(Symptom: 502 bei gesundem Backend). **Regel: Push-Werkzeug wird geprüft, nicht geglaubt** — nach
|
||||||
|
jedem `pct push` den Inhalt vergleichen (`md5sum` diesseits/jenseits in *einem* Remote-Aufruf,
|
||||||
|
Abbruch bei Abweichung). Muster: `push_verified()` in `caddy/deploy.sh`. Dieselbe Klasse wie
|
||||||
|
`nc -z` (09-01) und das `grep|sed`-Maskierungsmuster (08-31): **erst das eigene Werkzeug
|
||||||
|
verdächtigen, dann das Netz.**
|
||||||
|
- 🔴 **Die Core-Adressliste `nautobot` wird von Golden Config NIRGENDS gerendert** (Fund 2026-09-21).
|
||||||
|
Der Mgmt-Zugang des Cores für die Deploy-Pipeline läuft über `src-address-list=nautobot`
|
||||||
|
(Regel `*28`) — die Liste selbst stammt aber aus dem **handgelaufenen**
|
||||||
|
`routeros/tp5-etappe2-whitelists.rsc` und steht in **keinem** Template (`grep address-list` im
|
||||||
|
Template findet nur Kommentare). 🔴 **Ein GC-Recovery von null erzeugt damit einen Core, in den
|
||||||
|
Nautobot nicht mehr per SSH kommt** — die Pipeline wäre tot und könnte sich nicht selbst
|
||||||
|
reparieren. Gleiche Lücke wie die DHCP-Reservierungen. **Merkregel: was auf dem Gerät existiert,
|
||||||
|
aber nicht im Template, ist eine Recovery-Lücke, auch wenn Compliance 140/140 grün meldet** —
|
||||||
|
die Zahl prüft nur, was gerendert wird.
|
||||||
Ablösung durch Komodo (native MCP) geplant.
|
Ablösung durch Komodo (native MCP) geplant.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ gitea.emsenhuber.net {
|
|||||||
# INTERN (Mesh/LAN; Split-Horizon zeigt diese Namen auf 10.40.20.40)
|
# INTERN (Mesh/LAN; Split-Horizon zeigt diese Namen auf 10.40.20.40)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
nautobot.bf.emsenhuber.net {
|
nautobot.bf.emsenhuber.net {
|
||||||
reverse_proxy http://10.40.20.30:8080
|
reverse_proxy http://10.40.20.154:8080
|
||||||
}
|
}
|
||||||
|
|
||||||
# grafana.emsenhuber.net — 2026-08-25 entfernt: LXC 104 war seit Langem gestoppt und stand
|
# grafana.emsenhuber.net — 2026-08-25 entfernt: LXC 104 war seit Langem gestoppt und stand
|
||||||
|
|||||||
@@ -19,12 +19,35 @@ if [ -z "${TOKEN}" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 🔴 `pct push` meldet Exit 0 AUCH wenn es die Quelle nicht lesen kann
|
||||||
|
# ("failed to open <src> for reading", rc=0 — live gemessen 2026-09-21).
|
||||||
|
# Ein `&&`/`||` hinter dem Push beweist deshalb NICHTS, und die anschliessende
|
||||||
|
# `caddy validate` validiert dann die ALTE Datei -> gruene Abnahme bei
|
||||||
|
# unveraendertem Zustand. Genau so ist der Nautobot-Umzug am 2026-09-21
|
||||||
|
# scheinbar gelandet, obwohl live noch die alte Backend-IP stand.
|
||||||
|
# Deshalb: Push + md5-Vergleich in EINEM Remote-Aufruf, der bei Abweichung
|
||||||
|
# hart abbricht. Gepusht wird nur der lokal bekannte Pfad — die frueheren
|
||||||
|
# `\$\$`/`$$`-Verrenkungen waren der Ausloeser.
|
||||||
|
push_verified() { # push_verified <quelle-auf-pve> <ziel-im-ct> <perms> <label>
|
||||||
|
local src=$1 dst=$2 perms=$3 label=$4
|
||||||
|
ssh "${PVE}" "set -e
|
||||||
|
pct push ${CTID} '${src}' '${dst}' --perms ${perms}
|
||||||
|
want=\$(md5sum '${src}' | cut -d' ' -f1)
|
||||||
|
got=\$(pct exec ${CTID} -- md5sum '${dst}' | cut -d' ' -f1)
|
||||||
|
rm -f '${src}'
|
||||||
|
if [ \"\$want\" != \"\$got\" ]; then
|
||||||
|
echo \"FEHLER: ${label} nicht gelandet (\$want != \$got)\" >&2; exit 1
|
||||||
|
fi
|
||||||
|
echo \" ${label} verifiziert (\$got)\""
|
||||||
|
}
|
||||||
|
|
||||||
echo "-> Caddyfile"
|
echo "-> Caddyfile"
|
||||||
scp -q "${HERE}/Caddyfile" "${PVE}:/tmp/.caddyfile.$$"
|
scp -q "${HERE}/Caddyfile" "${PVE}:/tmp/.caddyfile.$$"
|
||||||
ssh "${PVE}" "pct push ${CTID} /tmp/.caddyfile.\$\$ /etc/caddy/Caddyfile --perms 644 2>/dev/null || pct push ${CTID} /tmp/.caddyfile.$$ /etc/caddy/Caddyfile --perms 644; rm -f /tmp/.caddyfile.$$"
|
push_verified "/tmp/.caddyfile.$$" "/etc/caddy/Caddyfile" 644 "Caddyfile"
|
||||||
|
|
||||||
echo "-> Token (maskiert: Laenge ${#TOKEN})"
|
echo "-> Token (maskiert: Laenge ${#TOKEN})"
|
||||||
printf 'CF_DNS_API_TOKEN=%s\n' "${TOKEN}" | ssh "${PVE}" "cat > /tmp/.cenv.$$ && chmod 600 /tmp/.cenv.$$ && pct push ${CTID} /tmp/.cenv.$$ /etc/caddy/caddy.env --perms 600 && rm -f /tmp/.cenv.$$"
|
printf 'CF_DNS_API_TOKEN=%s\n' "${TOKEN}" | ssh "${PVE}" "cat > /tmp/.cenv.$$ && chmod 600 /tmp/.cenv.$$"
|
||||||
|
push_verified "/tmp/.cenv.$$" "/etc/caddy/caddy.env" 600 "caddy.env"
|
||||||
|
|
||||||
echo "-> validate"
|
echo "-> validate"
|
||||||
ssh "${PVE}" "pct exec ${CTID} -- sh -c 'set -a; . /etc/caddy/caddy.env; set +a; /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile' 2>&1 | tail -3"
|
ssh "${PVE}" "pct exec ${CTID} -- sh -c 'set -a; . /etc/caddy/caddy.env; set +a; /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile' 2>&1 | tail -3"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 🆕 **SSoT-Wechsel im Aufbau (TP44, seit 2026-09-02):** Die CF-Estate (Tunnel, WARP-Connector,
|
> 🆕 **SSoT-Wechsel im Aufbau (TP44, seit 2026-09-02):** Die CF-Estate (Tunnel, WARP-Connector,
|
||||||
> VPC-Services, Hyperdrive, Workers, Access-Apps) wird als **native `nautobot.cloud`-Objekte** in
|
> VPC-Services, Hyperdrive, Workers, Access-Apps) wird als **native `nautobot.cloud`-Objekte** in
|
||||||
> Nautobot (`10.40.20.30`) geführt — Etappe 1 (manuelle Erstbefüllung) ✅. Seeder:
|
> Nautobot (`10.40.20.154`, TP45 Schicht A 2026-09-21; vorher `.30`) geführt — Etappe 1 (manuelle Erstbefüllung) ✅. Seeder:
|
||||||
> `nautobot/tp44-cf-estate-seed.py`, Detail: `docs/handoff-20260902-cf-estate-nautobot-ssot.md`.
|
> `nautobot/tp44-cf-estate-seed.py`, Detail: `docs/handoff-20260902-cf-estate-nautobot-ssot.md`.
|
||||||
> **CF bleibt operativer Master, Nautobot = Dokumentations-SSoT.** Dieses File bleibt gültige Historie
|
> **CF bleibt operativer Master, Nautobot = Dokumentations-SSoT.** Dieses File bleibt gültige Historie
|
||||||
> 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)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ den *damals geplanten* Werten und ⬜-Status. Sie sind erledigt bzw. gegenstands
|
|||||||
| ~~FritzBox 6660~~ | — | WAN/Transfer | — | — | ❌ **physisch entfernt** | Liwest-Kabelmodemtausch 2026-07-01 (Edge-Cutover), kein Zurück — Arris-Kabelmodem im Bridge-Mode ersetzt sie komplett |
|
| ~~FritzBox 6660~~ | — | WAN/Transfer | — | — | ❌ **physisch entfernt** | Liwest-Kabelmodemtausch 2026-07-01 (Edge-Cutover), kein Zurück — Arris-Kabelmodem im Bridge-Mode ersetzt sie komplett |
|
||||||
|
|
||||||
| platform-pg (LXC 105, pve) | — (neu) | Platform-Postgres (TP20), PG16, DB `nautobot` | 20 INFRA | `10.40.20.20` | ✅ **live** | TP20 2026-07-16: erstellt via `pct create`, Ubuntu 24.04 (native PG16, kein PGDG-Repo nötig), UTF8/en_US.UTF-8, `wal_level=replica`, `archive_mode` bewusst noch aus (s. Memory) bis pgBackRest/Garage-S3 da ist; Creds `postgres/.secrets` |
|
| platform-pg (LXC 105, pve) | — (neu) | Platform-Postgres (TP20), PG16, DB `nautobot` | 20 INFRA | `10.40.20.20` | ✅ **live** | TP20 2026-07-16: erstellt via `pct create`, Ubuntu 24.04 (native PG16, kein PGDG-Repo nötig), UTF8/en_US.UTF-8, `wal_level=replica`, `archive_mode` bewusst noch aus (s. Memory) bis pgBackRest/Garage-S3 da ist; Creds `postgres/.secrets` |
|
||||||
| nautobot (Portainer-Stack, NAS) | — (neu) | Nautobot Golden Config (TP21), App/API | 20 INFRA | `10.40.20.30` | ✅ **live** | TP21 2026-07-16: `ghcr.io/nautobot/nautobot:3.1.7`, ep14 Stack Id 203, DB=platform-pg; Begleitcontainer `nautobot-worker`.20.31, `nautobot-scheduler`.20.32, `nautobot-redis`.20.34 |
|
| nautobot (Portainer-Stack, NAS) | — (neu) | Nautobot Golden Config (TP21), App/API | 20 INFRA | `10.40.20.154` | ✅ **live** | TP21 2026-07-16: `ghcr.io/nautobot/nautobot:3.1.7`, ep14 Stack Id 203, DB=platform-pg; Begleitcontainer `nautobot-worker`.20.155, `nautobot-scheduler`.20.156, `nautobot-redis`.20.158. **TP45 Schicht A 2026-09-21:** Oktett-Klasse `.150–.199` (App-Services) — die alten `.30–.34` gibt Nautobot an AGH `.30`/`.31` frei |
|
||||||
|
|
||||||
> Weitere Geräte (Home Assistant, HA-IoT, Drucker-Reservierung, etc.) hier ergänzen, sobald relevant.
|
> Weitere Geräte (Home Assistant, HA-IoT, Drucker-Reservierung, etc.) hier ergänzen, sobald relevant.
|
||||||
|
|
||||||
|
|||||||
@@ -344,12 +344,14 @@ dann Tunnel** (umgekehrt gäbe es 530er).
|
|||||||
- 📌 **Und die Erwartung wird damit ehrlicher:** Nautobot war nie ohne Access-Login öffentlich
|
- 📌 **Und die Erwartung wird damit ehrlicher:** Nautobot war nie ohne Access-Login öffentlich
|
||||||
erreichbar. Der Schnitt schließt **keine offene Tür**, er entfernt eine **überflüssige zweite**.
|
erreichbar. Der Schnitt schließt **keine offene Tür**, er entfernt eine **überflüssige zweite**.
|
||||||
- ⚠️ **Vorprüfung, die vor jedem weiteren Schnitt zu wiederholen ist:** hängt Werkzeug am
|
- ⚠️ **Vorprüfung, die vor jedem weiteren Schnitt zu wiederholen ist:** hängt Werkzeug am
|
||||||
öffentlichen Namen? Hier nein — `gc-configplan.py` spricht `http://10.40.20.30:8080/api`, und
|
öffentlichen Namen? Hier nein — `gc-configplan.py` spricht `http://10.40.20.154:8080/api`
|
||||||
|
(TP45 Schicht A 2026-09-21; zum Messzeitpunkt war es `.20.30`), und
|
||||||
kein Repo-Skript nutzt den FQDN.
|
kein Repo-Skript nutzt den FQDN.
|
||||||
- ✅ **Betriebsfenster ausgewertet 2026-08-30 (Tag 6/7): `nautobot` bleibt mesh-only.** Kein Rückweg
|
- ✅ **Betriebsfenster ausgewertet 2026-08-30 (Tag 6/7): `nautobot` bleibt mesh-only.** Kein Rückweg
|
||||||
gebraucht, kein Ausfall. `nautobot.emsenhuber.net` hat inzwischen auch **keinen DNS-Record** mehr
|
gebraucht, kein Ausfall. `nautobot.emsenhuber.net` hat inzwischen auch **keinen DNS-Record** mehr
|
||||||
(mit den CF-Leichen am 08-29 gelöscht, Commit `39ba5da`) — der Dienst ist vollständig aus dem
|
(mit den CF-Leichen am 08-29 gelöscht, Commit `39ba5da`) — der Dienst ist vollständig aus dem
|
||||||
öffentlichen Strang heraus und ausschließlich über `10.40.20.30:8080` bzw. intern über Caddy erreichbar.
|
öffentlichen Strang heraus und ausschließlich über `10.40.20.154:8080` (TP45 Schicht A 2026-09-21)
|
||||||
|
bzw. intern über Caddy erreichbar.
|
||||||
|
|
||||||
##### ✅ §2/§3 durchgezogen — Vor-Ort-Session 2026-08-31
|
##### ✅ §2/§3 durchgezogen — Vor-Ort-Session 2026-08-31
|
||||||
|
|
||||||
@@ -632,12 +634,13 @@ und Letzteres verlangt die **physische Bestätigung am Gerät** — ap1 Technik
|
|||||||
häufigsten per Mesh angefasste Dienst, und ein Fehlschlag kostet nichts Produktives), eine Woche fahren, dann
|
häufigsten per Mesh angefasste Dienst, und ein Fehlschlag kostet nichts Produktives), eine Woche fahren, dann
|
||||||
entscheiden. **Rollback = eine Ingress-Zeile zurück.**
|
entscheiden. **Rollback = eine Ingress-Zeile zurück.**
|
||||||
- 📋 **Runbook: `docs/tp16.3-nautobot-mesh-only-runbook.md`** (2026-08-19) — der Schnitt oben, ausführbar
|
- 📋 **Runbook: `docs/tp16.3-nautobot-mesh-only-runbook.md`** (2026-08-19) — der Schnitt oben, ausführbar
|
||||||
ausformuliert: Vorbedingungen (Mesh-Route, Zugriff über `10.40.20.30:8080` **mit WARP von außerhalb**),
|
ausformuliert: Vorbedingungen (Mesh-Route, Zugriff über `10.40.20.154:8080` **mit WARP von außerhalb**),
|
||||||
Ingress-`PUT` über `CF_API_TOKEN` (**nicht** über den CF-MCP, der ist read-only), Rollback aus der
|
Ingress-`PUT` über `CF_API_TOKEN` (**nicht** über den CF-MCP, der ist read-only), Rollback aus der
|
||||||
gesicherten Config.
|
gesicherten Config.
|
||||||
- 🔴 **Der Abnahmepunkt, den man übersieht:** Nautobot ist nicht nur Web-UI, sondern die
|
- 🔴 **Der Abnahmepunkt, den man übersieht:** Nautobot ist nicht nur Web-UI, sondern die
|
||||||
**Deploy-Pipeline**. Nach dem Schnitt gehört ein Job-Lauf zur Abnahme, nicht nur ein Browser-Aufruf
|
**Deploy-Pipeline**. Nach dem Schnitt gehört ein Job-Lauf zur Abnahme, nicht nur ein Browser-Aufruf
|
||||||
— der Netmiko-Deploy kommt von `10.40.20.31` (`nautobot-worker`), nicht von `.20.30`.
|
— der Netmiko-Deploy kommt von `10.40.20.155` (`nautobot-worker`), nicht von `.154` (Werte nach
|
||||||
|
TP45 Schicht A 2026-09-21; zum Messzeitpunkt `.31` / `.30`).
|
||||||
- ⚠️ Beim Kürzen des `ingress`-Arrays muss die **Catch-all-Regel die letzte bleiben**, sonst greift
|
- ⚠️ Beim Kürzen des `ingress`-Arrays muss die **Catch-all-Regel die letzte bleiben**, sonst greift
|
||||||
der Ingress für alle übrigen Hostnames nicht mehr.
|
der Ingress für alle übrigen Hostnames nicht mehr.
|
||||||
- **Abh.:** TP16.1 ✅ (Mesh live), TP16 (Public-Strang + dessen HA-Degradierung), **TP16.2** (der `vw`-Constraint
|
- **Abh.:** TP16.1 ✅ (Mesh live), TP16 (Public-Strang + dessen HA-Degradierung), **TP16.2** (der `vw`-Constraint
|
||||||
@@ -2732,17 +2735,67 @@ unter `*1A TP5-WOULD-DROP` / `*2D TP5: deny-by-default` (621 Pakete).
|
|||||||
#### Schichten (Reihenfolge = Abhängigkeit)
|
#### Schichten (Reihenfolge = Abhängigkeit)
|
||||||
| Schicht | Inhalt | Stand |
|
| Schicht | Inhalt | Stand |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **A** | VLAN20 virtuelle Gäste ins Schema — **Nautobot zuerst** (`.30–.34` → `.154–.156`/`.158`), dann AGH | offen |
|
| **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 |
|
| **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 | offen |
|
||||||
| **D** | Tunnel-Inventar + GC + generische Namen, TP30 #10, Leak-Ursachen | 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 mit Doppeladresse — **eigene Session**, pve2 nur reservieren | offen |
|
||||||
| **F** | CF-/Tunnel-SSoT + Recovery → **TP46** | offen |
|
| **F** | CF-/Tunnel-SSoT + Recovery → **TP46** | offen |
|
||||||
|
|
||||||
🔴 **Warum Nautobot zuerst.** AGH will `.30`/`.31` — dort sitzt heute Nautobot (`.30`/`.31`/`.32`/`.34`).
|
🔴 **Warum Nautobot zuerst.** AGH will `.30`/`.31` — dort saß Nautobot (`.30`/`.31`/`.32`/`.34`).
|
||||||
**27 Skript-/Config-Dateien** referenzieren `10.40.20.30` (alle `gc-*.py`, 14× `ingest-*.py`,
|
**28 Skript-/Config-Dateien** referenzierten `10.40.20.30` (alle `gc-*.py`, 14× `ingest-*.py`,
|
||||||
`tp44-*`, `routeros/fetch-core-cutover-config.py`). Und: **der GC-Pfad läuft über Nautobot** — AGH
|
`tp44-*`, `routeros/fetch-core-cutover-config.py`) — alle am 2026-09-21 mitgezogen. Und: **der
|
||||||
zuerst zu ziehen hieße, die Werkzeugkette gegen sich selbst laufen zu lassen.
|
GC-Pfad läuft über Nautobot** — AGH zuerst zu ziehen hieße, die Werkzeugkette gegen sich selbst
|
||||||
|
laufen zu lassen.
|
||||||
|
|
||||||
|
##### ✅ Nautobot-Umzug durch (2026-09-21) — `.30/.31/.32/.34` → `.154/.155/.156/.158`
|
||||||
|
|
||||||
|
`.157` ist gitea, deshalb nimmt redis `.158`. Reihenfolge und Belege:
|
||||||
|
|
||||||
|
1. **Core-Adressliste `nautobot` additiv erweitert** (`.154/.155/.156` **dazu**, Alt-Einträge bleiben).
|
||||||
|
Das ist die wirksame Ebene: der Mgmt-Zugang des Cores läuft über `src-address-list=nautobot`
|
||||||
|
(Regel `*28`, 113 Pakete), **nicht** über den Literal `*24`.
|
||||||
|
2. **Nautobot-Datenmigration** über das neue [`nautobot/tp45-ip-move.py`](../nautobot/tp45-ip-move.py).
|
||||||
|
Nautobot **verbietet** das Ändern einer IP nach dem Anlegen (`Host address cannot be changed once
|
||||||
|
created`) → Umzug ist Dreischritt *anlegen → `primary_ip4` umhängen → Altobjekt löschen*.
|
||||||
|
3. **Stack-YML + Redeploy** (`PUT /api/stacks/203?endpointId=14`), Container auf den neuen IPs,
|
||||||
|
`http://10.40.20.154:8080/health/` = **200**, alle vier Alt-IPs **000** (keine Doppeladresse —
|
||||||
|
so entschieden).
|
||||||
|
4. **Caddy** `nautobot.bf` → `.154`, deployt und **verifiziert**.
|
||||||
|
5. **Core-Literal `*24` auf `.154`** über GC (`gc-manual-plan.py --deploy`, Plan `51a90e25`,
|
||||||
|
Completed) mit Wirksamkeitsprüfung am Gerät.
|
||||||
|
6. **`python3 nautobot/gc-configplan.py --refresh-compliance` → 140/140, 0 Drifts.**
|
||||||
|
|
||||||
|
##### 🔴 Zwei Werkzeug-Fehler, die genau dabei Zeit gekostet haben (beide neu)
|
||||||
|
|
||||||
|
**(a) `pct push` meldet Exit 0, auch wenn es die Quelle nicht lesen kann.**
|
||||||
|
Gemessen: `pct push 130 <nicht-vorhandene-datei> /etc/caddy/Caddyfile` →
|
||||||
|
`failed to open … for reading`, **`rc=0`**.
|
||||||
|
Folge in `caddy/deploy.sh`: die Push-Zeile war als `push || push`-Fallback gebaut (wegen einer
|
||||||
|
`$$`-Escaping-Verwechslung); der Fallback griff **nie**, weil der kaputte Push „Erfolg" meldete. Die
|
||||||
|
anschließende `caddy validate` prüfte damit die **alte** Datei und war grün — **grüne Abnahme bei
|
||||||
|
unverändertem Zustand**. Symptom war `nautobot.bf` **502** bei gleichzeitig gesundem Backend.
|
||||||
|
**Fix:** `push_verified()` in `caddy/deploy.sh` — Push **und** md5-Vergleich in *einem* Remote-Aufruf,
|
||||||
|
harter Abbruch bei Abweichung. Merkregel: **ein Push-Werkzeug, dessen Exit-Code nicht stimmt, wird
|
||||||
|
geprüft, nicht geglaubt** — dieselbe Klasse wie `nc -z` (09-01) und das `sed`-Muster (08-31).
|
||||||
|
|
||||||
|
**(b) 🔴 Die Firewall-Adressliste `nautobot` wird von Golden Config NIRGENDS gerendert.**
|
||||||
|
Der Core-Zugang für Nautobot hängt an `src-address-list=nautobot` — die Liste stammt aber aus dem
|
||||||
|
**von Hand gelaufenen** `routeros/tp5-etappe2-whitelists.rsc` und existiert im Template nicht
|
||||||
|
(`grep` auf `address-list` findet nur Kommentare). **Ein Golden-Config-Recovery von null erzeugt
|
||||||
|
damit einen Core, in den Nautobot nicht mehr per SSH kommt** — die Deploy-Pipeline wäre tot und
|
||||||
|
würde sich selbst nicht reparieren können. Gleiche Klasse wie die fehlenden DHCP-Reservierungen.
|
||||||
|
Der Renderer gehört in dieselbe Baustelle wie Punkt „DHCP-Reservierungen" oben.
|
||||||
|
|
||||||
|
**(c) Nebenfund — Regel `*24`/`*28` sind Dubletten.** Dieselbe Erlaubnis existiert zweimal:
|
||||||
|
`src-address=10.40.20.30` (Literal, **0 Pakete** = tot) und `src-address-list=nautobot`. Nur die
|
||||||
|
Liste ist wartbar. Der Literal wurde auf `.154` gezogen (Soll-Konsistenz), die **Zusammenlegung**
|
||||||
|
bleibt als Aufräumposten offen.
|
||||||
|
|
||||||
|
**(d) Nebenfund — `gc-manual-plan.py` Diagnosepfad ist tot.** Die Log-Abfrage
|
||||||
|
`/api/extras/job-log-entries/?job_result=…` liefert **HTTP 404** (Nautobot 3.x hat den Endpunkt
|
||||||
|
verschoben). Bei einem **erfolgreichen** Deploy ist das kosmetisch, bei einem **gescheiterten**
|
||||||
|
nimmt es die Fehlermeldung weg — genau dann, wenn man sie braucht.
|
||||||
|
|
||||||
#### Soll-Oktette (Nachschlagetabelle, s. TP32 für die Herleitung)
|
#### Soll-Oktette (Nachschlagetabelle, s. TP32 für die Herleitung)
|
||||||
| Oktett | Rolle | Soll |
|
| Oktett | Rolle | Soll |
|
||||||
|
|||||||
15
docs/transaktionen/TP45-firewall-nautobot-154.commands.rsc
Normal file
15
docs/transaktionen/TP45-firewall-nautobot-154.commands.rsc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# TP45 Schicht A — Nautobot-Literal in der Core-Firewall von .30 auf .154
|
||||||
|
#
|
||||||
|
# KEINE Transaktion: das ist eine dauerhafte Soll-Aenderung (Nautobot ist umgezogen,
|
||||||
|
# das Soll in Nautobot wurde mitgezogen). Die Datei liegt nur hier, weil
|
||||||
|
# gc-manual-plan.py eine Commands-Datei braucht.
|
||||||
|
#
|
||||||
|
# Kontext: die Regel *24 "TP5-E2: Nautobot->MGMT ssh" ist der LITERAL-Pfad und stand
|
||||||
|
# auf 10.40.20.30. Sie steht bei 0 Paketen — die Arbeit macht die Parallelregel *28
|
||||||
|
# "TP5-E2: Nautobot->MGMT ssh (list)" ueber src-address-list=nautobot (113 Pakete,
|
||||||
|
# Liste am 2026-09-21 um .154/.155/.156 erweitert). Der Literal ist damit
|
||||||
|
# funktional redundant, wird aber auf .154 gezogen, damit Soll (Nautobot-Daten
|
||||||
|
# `firewall_filter`) und Ist nicht auseinanderlaufen.
|
||||||
|
#
|
||||||
|
# Rollback: siehe TP45-firewall-nautobot-154.rollback.rsc
|
||||||
|
/ip firewall filter set [find comment="TP5-E2: Nautobot->MGMT ssh"] src-address=10.40.20.154
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Rueckbau TP45 Schicht A: Nautobot-Literal zurueck auf .30
|
||||||
|
# (nur noetig, wenn Nautobot wieder auf .30 zurueckzieht — die Adressliste ist die
|
||||||
|
# wirksame Ebene, dieser Literal ist der tote Parallelpfad.)
|
||||||
|
/ip firewall filter set [find comment="TP5-E2: Nautobot->MGMT ssh"] src-address=10.40.20.30
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Ist-Stand der Regel VOR dem TP45-Schicht-A-Deploy (2026-09-21, gelesen per MikroMCP).
|
||||||
|
/ip firewall filter
|
||||||
|
add action=accept chain=forward comment="TP5-E2: Nautobot->MGMT ssh" dst-port=22 out-interface=vlan99 protocol=tcp src-address=10.40.20.30
|
||||||
@@ -60,7 +60,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
|
|
||||||
# Status-UUIDs fuer nautobot_golden_config.configplan (2026-08-16 aus der API gelesen)
|
# Status-UUIDs fuer nautobot_golden_config.configplan (2026-08-16 aus der API gelesen)
|
||||||
@@ -289,7 +289,7 @@ def job_mgmt_acl():
|
|||||||
das ist auf dem Core passiert. Umgekehrt fuhren branch/ap1/ap2 ssh/www/winbox/api/ftp/
|
das ist auf dem Core passiert. Umgekehrt fuhren branch/ap1/ap2 ssh/www/winbox/api/ftp/
|
||||||
telnet komplett OHNE Address-Beschraenkung; winbox war auf allen vier offen.
|
telnet komplett OHNE Address-Beschraenkung; winbox war auf allen vier offen.
|
||||||
|
|
||||||
KEIN LOCKOUT-RISIKO: der Deploy laeuft von Nautobot (10.40.20.30) aus, also aus
|
KEIN LOCKOUT-RISIKO: der Deploy laeuft von Nautobot (10.40.20.154) aus, also aus
|
||||||
10.40.0.0/16 -- die Adresse bleibt in jeder gesetzten ACL enthalten. RouterOS wendet
|
10.40.0.0/16 -- die Adresse bleibt in jeder gesetzten ACL enthalten. RouterOS wendet
|
||||||
`address=` ohnehin erst auf NEUE Verbindungen an, die laufende Session ueberlebt.
|
`address=` ohnehin erst auf NEUE Verbindungen an, die laufende Session ueberlebt.
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ def job_tp38_oob():
|
|||||||
* branch: ether23 untaggen, VLAN99 um untagged=ether23 erweitern, 8 Input-Regeln
|
* branch: ether23 untaggen, VLAN99 um untagged=ether23 erweitern, 8 Input-Regeln
|
||||||
(deny-by-default). Die Regeln werden in der Reihenfolge angelegt, in der das
|
(deny-by-default). Die Regeln werden in der Reihenfolge angelegt, in der das
|
||||||
Template sie rendert — established zuerst, drop zuletzt — damit kein LOCKOUT:
|
Template sie rendert — established zuerst, drop zuletzt — damit kein LOCKOUT:
|
||||||
die laufende Nautobot-SSH-Session (10.40.20.30) matcht auf Regel 1 (established),
|
die laufende Nautobot-SSH-Session (10.40.20.154) matcht auf Regel 1 (established),
|
||||||
bevor Regel 8 (drop) greift.
|
bevor Regel 8 (drop) greift.
|
||||||
* core: ether23 untaggen, VLAN99 untagged=ether23, EINE Regel "OOB->core mgmt" mit
|
* core: ether23 untaggen, VLAN99 untagged=ether23, EINE Regel "OOB->core mgmt" mit
|
||||||
place-before vor die Log-Regel (sonst landet sie hinter dem finalen drop und
|
place-before vor die Log-Regel (sonst landet sie hinter dem finalen drop und
|
||||||
@@ -368,7 +368,7 @@ def job_tp38_oob():
|
|||||||
|
|
||||||
KEIN LOCKOUT-RISIKO: ether23 ist auf beiden Geraeten frei (running=false); der
|
KEIN LOCKOUT-RISIKO: ether23 ist auf beiden Geraeten frei (running=false); der
|
||||||
VLAN99-Umbau aendert nur `untagged`, nicht `tagged`; der Deploy laeuft von Nautobot
|
VLAN99-Umbau aendert nur `untagged`, nicht `tagged`; der Deploy laeuft von Nautobot
|
||||||
(10.40.20.30), das in jeder gesetzten Firewall-Freigabe enthalten bleibt.
|
(10.40.20.154), das in jeder gesetzten Firewall-Freigabe enthalten bleibt.
|
||||||
"""
|
"""
|
||||||
bridge_set = (
|
bridge_set = (
|
||||||
':do { /interface bridge port set [find interface=ether23] pvid=99 '
|
':do { /interface bridge port set [find interface=ether23] pvid=99 '
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
GEN_JOB = "f93bfa53-2332-4fa2-a6ab-5921c802fcee" # Generate Config Plans
|
GEN_JOB = "f93bfa53-2332-4fa2-a6ab-5921c802fcee" # Generate Config Plans
|
||||||
DEPLOY_JOB = "31c3f093-eeec-43fa-ba4a-0aed2f3c67aa" # Deploy Config Plans
|
DEPLOY_JOB = "31c3f093-eeec-43fa-ba4a-0aed2f3c67aa" # Deploy Config Plans
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import urllib.parse
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
COMMIT = "--commit" in sys.argv
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ HIER = os.path.dirname(os.path.abspath(__file__))
|
|||||||
REPO = os.path.dirname(HIER)
|
REPO = os.path.dirname(HIER)
|
||||||
AKTEN = os.path.join(REPO, "docs", "transaktionen")
|
AKTEN = os.path.join(REPO, "docs", "transaktionen")
|
||||||
STACK = os.path.join(REPO, "docs", "nas-vlan20", "stacks", "210-nautobot.yml")
|
STACK = os.path.join(REPO, "docs", "nas-vlan20", "stacks", "210-nautobot.yml")
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
|
|
||||||
PW_ENV = {"crs326-core": "CRS326_CORE_PASS", "crs326-branch": "CRS326_BRANCH_PASS",
|
PW_ENV = {"crs326-core": "CRS326_CORE_PASS", "crs326-branch": "CRS326_BRANCH_PASS",
|
||||||
"ap1": "ROUTER_AP1_PASS", "ap2": "ROUTER_AP2_PASS"}
|
"ap1": "ROUTER_AP1_PASS", "ap2": "ROUTER_AP2_PASS"}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"""Restdaten-Ingest 2026-07-22: cudy1/cudy2, sw1-Ports, crs326-core Zielport."""
|
"""Restdaten-Ingest 2026-07-22: cudy1/cudy2, sw1-Ports, crs326-core Zielport."""
|
||||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ["NAUTOBOT_TOKEN"]
|
TOKEN = os.environ["NAUTOBOT_TOKEN"]
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
log = []
|
log = []
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ ConfigContext und DCIM-Modell nicht auseinanderlaufen.
|
|||||||
"""
|
"""
|
||||||
import json, os, sys, urllib.error, urllib.parse, urllib.request
|
import json, os, sys, urllib.error, urllib.parse, urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ PSKs werden bewusst NICHT modelliert (liegen lokal am Geraet).
|
|||||||
"""
|
"""
|
||||||
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
log = []
|
log = []
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ tatsaechlichen Umzug wandert er zurueck auf Sideboard.
|
|||||||
"""
|
"""
|
||||||
import json, os, sys, urllib.error, urllib.parse, urllib.request
|
import json, os, sys, urllib.error, urllib.parse, urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Quelle: ap1 live via MikroMCP + docs/teilprojekte.md.
|
|||||||
"""
|
"""
|
||||||
import json, sys, urllib.request, urllib.error
|
import json, sys, urllib.request, urllib.error
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
import os
|
import os
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "") # aus docs/nas-vlan20/stacks/210-nautobot.yml (gitignored)
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "") # aus docs/nas-vlan20/stacks/210-nautobot.yml (gitignored)
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import pathlib
|
|||||||
import sys
|
import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
DEVICE = "crs326-core"
|
DEVICE = "crs326-core"
|
||||||
SECRETS = pathlib.Path.home() / ".secrets" / "credentials.env"
|
SECRETS = pathlib.Path.home() / ".secrets" / "credentials.env"
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Idempotent; verwendet GraphQL fuer tagged_vlans (REST liefert null).
|
|||||||
"""
|
"""
|
||||||
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
log = []
|
log = []
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Verifikation NUR per GraphQL (tagged_vlans liest sich per REST als null).
|
|||||||
"""
|
"""
|
||||||
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
log = []
|
log = []
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Jinja-Template denselben String emittieren kann (sonst Dauer-Diff in der Complia
|
|||||||
"""
|
"""
|
||||||
import json, os, re, sys, urllib.error, urllib.parse, urllib.request
|
import json, os, re, sys, urllib.error, urllib.parse, urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
APPLY = "--apply" in sys.argv
|
APPLY = "--apply" in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
APPLY = "--apply" in sys.argv
|
APPLY = "--apply" in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
APPLY = "--apply" in sys.argv
|
APPLY = "--apply" in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ UMGEKEHRT GILT: die `vlanXX`-Descriptions auf dem Core NICHT anfassen, die lande
|
|||||||
"""
|
"""
|
||||||
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
log = []
|
log = []
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
|
|
||||||
BRANCH_RULES = [
|
BRANCH_RULES = [
|
||||||
'add action=accept chain=input comment="TP38: established" connection-state=established,related,untracked',
|
'add action=accept chain=input comment="TP38: established" connection-state=established,related,untracked',
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
DRY = "--apply" not in sys.argv
|
DRY = "--apply" not in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
BASE = "http://10.40.20.30:8080/api"
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "")
|
||||||
APPLY = "--apply" in sys.argv
|
APPLY = "--apply" in sys.argv
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import urllib.parse
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
COMMIT = "--commit" in sys.argv
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import urllib.parse
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
COMMIT = "--commit" in sys.argv
|
COMMIT = "--commit" in sys.argv
|
||||||
@@ -134,10 +134,10 @@ VMS = {
|
|||||||
("docker", "10.40.20.126/24", "Docker-Host nested (LXC 400)"),
|
("docker", "10.40.20.126/24", "Docker-Host nested (LXC 400)"),
|
||||||
],
|
],
|
||||||
"nas-bf": [
|
"nas-bf": [
|
||||||
("nautobot", "10.40.20.30/24", "Nautobot Web (DS923 ep14)"),
|
("nautobot", "10.40.20.154/24", "Nautobot Web (DS923 ep14)"),
|
||||||
("nautobot-worker", "10.40.20.31/24", "Nautobot Golden-Config Worker"),
|
("nautobot-worker", "10.40.20.155/24", "Nautobot Golden-Config Worker"),
|
||||||
("nautobot-scheduler", "10.40.20.32/24", "Nautobot Scheduler"),
|
("nautobot-scheduler", "10.40.20.156/24", "Nautobot Scheduler"),
|
||||||
("nautobot-redis", "10.40.20.34/24", "Nautobot Redis"),
|
("nautobot-redis", "10.40.20.158/24", "Nautobot Redis"),
|
||||||
("gitea", "10.40.20.157/24", "Gitea (git-SSoT)"),
|
("gitea", "10.40.20.157/24", "Gitea (git-SSoT)"),
|
||||||
("vaultwarden", "10.40.20.127/24", "Vaultwarden (DB=mariadb LXC302)"),
|
("vaultwarden", "10.40.20.127/24", "Vaultwarden (DB=mariadb LXC302)"),
|
||||||
("shelly-mcp", "10.40.20.180/24", "Shelly-MCP"),
|
("shelly-mcp", "10.40.20.180/24", "Shelly-MCP"),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import urllib.parse
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
CF_ACCOUNT_ID = "393c051f3accf51881d6c98fc5b66c36"
|
CF_ACCOUNT_ID = "393c051f3accf51881d6c98fc5b66c36"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import urllib.parse
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
COMMIT = "--commit" in sys.argv
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import urllib.parse
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.154:8080")
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
COMMIT = "--commit" in sys.argv
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|||||||
131
nautobot/tp45-ip-move.py
Normal file
131
nautobot/tp45-ip-move.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
r"""TP45 Schicht A — primaere IP eines Nautobot-Gastes umziehen.
|
||||||
|
|
||||||
|
WARUM ES DIESES SKRIPT GIBT
|
||||||
|
---------------------------
|
||||||
|
Nautobot verbietet das Aendern einer IP-Adresse nach dem Anlegen
|
||||||
|
(`{"__all__": ["Host address cannot be changed once created"]}`). Ein Umzug
|
||||||
|
ist deshalb kein PATCH, sondern ein Dreischritt je Gast:
|
||||||
|
|
||||||
|
1. neues ipam.IPAddress am Ziel anlegen (Status/Namespace vom Altobjekt)
|
||||||
|
2. altes Objekt an die VM-Schnittstelle haengen und primary_ip4 umhaengen
|
||||||
|
3. altes Objekt loeschen
|
||||||
|
|
||||||
|
Von Hand ist das pro Gast drei API-Aufrufe in genau dieser Reihenfolge — bei
|
||||||
|
~15 Gaesten in Schicht A eine sichere Fehlerquelle. Reihenfolge ist zwingend:
|
||||||
|
wer das Altobjekt vor dem Umhaengen loescht, reisst die primary_ip4-Referenz.
|
||||||
|
|
||||||
|
⚠️ Die IP allein ist NICHT der ganze Umzug. Ein Gast, den der Core ueber eine
|
||||||
|
Firewall-Adressliste erreicht (z.B. `nautobot`), braucht die neue IP ZUERST in
|
||||||
|
dieser Liste — sonst steht der Zugriff weg, bevor die Migration durch ist.
|
||||||
|
|
||||||
|
python3 nautobot/tp45-ip-move.py # Dry-Run (Default)
|
||||||
|
python3 nautobot/tp45-ip-move.py --apply
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
|
BASE = "http://10.40.20.154:8080/api"
|
||||||
|
APPLY = "--apply" in sys.argv
|
||||||
|
|
||||||
|
# (VM-Name, alte IP, neue IP) — Oktett-Klassen nach TP32, s. docs/teilprojekte.md ### TP45
|
||||||
|
# Charge 1 (2026-09-21, angewendet). Die Altobjekte sind weg — ein erneuter Lauf
|
||||||
|
# bricht darum sauber ab ("0 Treffer"), er zieht nichts doppelt um.
|
||||||
|
MOVES = [
|
||||||
|
("nautobot", "10.40.20.30/24", "10.40.20.154/24"),
|
||||||
|
("nautobot-worker", "10.40.20.31/24", "10.40.20.155/24"),
|
||||||
|
("nautobot-scheduler", "10.40.20.32/24", "10.40.20.156/24"),
|
||||||
|
("nautobot-redis", "10.40.20.34/24", "10.40.20.158/24"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
raise SystemExit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML}")
|
||||||
|
|
||||||
|
|
||||||
|
TOK = load_token()
|
||||||
|
|
||||||
|
|
||||||
|
def api(path, method="GET", payload=None):
|
||||||
|
req = urllib.request.Request(
|
||||||
|
BASE + path, method=method,
|
||||||
|
data=json.dumps(payload).encode() if payload is not None else None,
|
||||||
|
headers={"Authorization": f"Token {TOK}", "Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return json.load(r) if r.status != 204 else {}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
raise SystemExit(f"{method} {path} -> {e.code}: {e.read().decode()[:300]}")
|
||||||
|
|
||||||
|
|
||||||
|
def only(path, expect=1):
|
||||||
|
res = api(path)["results"]
|
||||||
|
if len(res) != expect:
|
||||||
|
raise SystemExit(f"{path}: {len(res)} Treffer, erwartet {expect}")
|
||||||
|
return res[0] if expect == 1 else res
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP45 IP-Move ({'APPLY' if APPLY else 'DRY-RUN'}) ==")
|
||||||
|
for vm_name, old_ip, new_ip in MOVES:
|
||||||
|
print(f"\n-- {vm_name}: {old_ip} -> {new_ip} --")
|
||||||
|
old = only(f"/ipam/ip-addresses/?address={old_ip}")
|
||||||
|
vm = only(f"/virtualization/virtual-machines/?name={vm_name}")
|
||||||
|
|
||||||
|
ref = vm.get("primary_ip4")
|
||||||
|
ref = ref.get("id") if isinstance(ref, dict) else ref
|
||||||
|
if ref != old["id"]:
|
||||||
|
raise SystemExit(f" primary_ip4 von {vm_name} zeigt nicht auf {old_ip} "
|
||||||
|
f"(ist {ref}) — Abbruch, nichts geschrieben")
|
||||||
|
|
||||||
|
if api(f"/ipam/ip-addresses/?address={new_ip}")["count"]:
|
||||||
|
raise SystemExit(f" {new_ip} ist in Nautobot schon belegt — Abbruch")
|
||||||
|
print(f" Altobjekt={old['id'][:8]} primary_ip4 bestaetigt Ziel frei")
|
||||||
|
|
||||||
|
if not APPLY:
|
||||||
|
print(" [DRY-RUN] wuerde: anlegen -> eth0 verknuepfen -> primary_ip4 "
|
||||||
|
"umhaengen -> Altobjekt loeschen")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# namespace ist am Altobjekt nicht serialisiert und beim Anlegen Pflicht
|
||||||
|
ns = old.get("namespace")
|
||||||
|
ns_id = (ns or {}).get("id") if isinstance(ns, dict) else ns
|
||||||
|
if not ns_id:
|
||||||
|
ns_id = only("/ipam/namespaces/?name=Global")["id"]
|
||||||
|
st = old.get("status")
|
||||||
|
new = api("/ipam/ip-addresses/", "POST",
|
||||||
|
{"address": new_ip,
|
||||||
|
"status": st["id"] if isinstance(st, dict) else st,
|
||||||
|
"namespace": ns_id, "dns_name": ""})
|
||||||
|
print(f" + angelegt {new['id'][:8]} @ {new['address']}")
|
||||||
|
|
||||||
|
vmif = api(f"/virtualization/interfaces/?virtual_machine={vm['id']}&name=eth0")["results"]
|
||||||
|
if vmif:
|
||||||
|
api("/ipam/ip-address-to-interface/", "POST",
|
||||||
|
{"ip_address": new["id"], "vm_interface": vmif[0]["id"]})
|
||||||
|
print(f" + verknuepft mit {vm_name}/eth0 ({vmif[0]['id'][:8]})")
|
||||||
|
else:
|
||||||
|
print(" ~ keine eth0-Schnittstelle — Verknuepfung uebersprungen")
|
||||||
|
|
||||||
|
api(f"/virtualization/virtual-machines/{vm['id']}/", "PATCH", {"primary_ip4": new["id"]})
|
||||||
|
print(f" + primary_ip4 umgehaengt")
|
||||||
|
|
||||||
|
api(f"/ipam/ip-addresses/{old['id']}/", "DELETE")
|
||||||
|
print(f" - Altobjekt {old_ip} geloescht")
|
||||||
|
|
||||||
|
print("\nFertig." if APPLY else "\nDRY-RUN — nichts geschrieben. Mit --apply ausfuehren.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -7,7 +7,7 @@ import pathlib
|
|||||||
import re
|
import re
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
NAUTOBOT_URL = "http://10.40.20.30:8080"
|
NAUTOBOT_URL = "http://10.40.20.154:8080"
|
||||||
TOKEN_PATH = pathlib.Path(__file__).parent.parent / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
TOKEN_PATH = pathlib.Path(__file__).parent.parent / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
OUT = pathlib.Path(__file__).with_name("tp15-core-cutover-intended.rsc")
|
OUT = pathlib.Path(__file__).with_name("tp15-core-cutover-intended.rsc")
|
||||||
LEASES_RSC = pathlib.Path(__file__).with_name("ap1-leases-to-core.rsc")
|
LEASES_RSC = pathlib.Path(__file__).with_name("ap1-leases-to-core.rsc")
|
||||||
|
|||||||
Reference in New Issue
Block a user