Files
bf-infra/nautobot/ingest-2026-07-22-restdaten.py
Christoph Emsenhuber b52d6aa49e 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>
2026-09-21 13:14:35 +02:00

134 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Restdaten-Ingest 2026-07-22: cudy1/cudy2, sw1-Ports, crs326-core Zielport."""
import json, os, sys, urllib.parse, urllib.request, urllib.error
BASE = "http://10.40.20.154:8080/api"
TOKEN = os.environ["NAUTOBOT_TOKEN"]
DRY = "--apply" not in sys.argv
log = []
def req(method, path, data=None):
url = path if path.startswith("http") else f"{BASE}{path}"
body = json.dumps(data).encode() if data is not None else None
r = urllib.request.Request(url, data=body, method=method)
r.add_header("Authorization", f"Token {TOKEN}")
r.add_header("Content-Type", "application/json")
r.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(r, timeout=30) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
print(f"!! {method} {url}\n {json.dumps(data)}\n -> {e.code} {e.read().decode()[:400]}")
raise
def find(path, **params):
q = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
res = req("GET", f"{path}?{q}&limit=2")
return res["results"][0] if res.get("count") else None
def goc(path, match, payload, label=None):
name = label or payload.get("name") or payload.get("model")
obj = find(path, **match)
if obj:
log.append(f" = {name}")
return obj
if DRY:
log.append(f" + {name} (dry)")
return {"id": f"DRY-{name}"}
log.append(f" + {name}")
return req("POST", path + "/", payload)
def patch(path, obj, payload, label):
delta = {}
for k, v in payload.items():
cur = obj.get(k)
cur = cur.get("id") if isinstance(cur, dict) else cur
if cur != v:
delta[k] = v
if not delta:
log.append(f" = {label} (unveraendert)")
return obj
if DRY:
log.append(f" ~ {label}: {list(delta)} (dry)")
return obj
log.append(f" ~ {label}: {list(delta)}")
return req("PATCH", f"{path}/{obj['id']}/", delta)
sid = lambda n: find("/extras/statuses", name=n)["id"]
S_ACTIVE, S_DECOM = sid("Active"), sid("Decommissioning")
FW = "Cudy WR3600E V1.0, Firmware 2.3.15-20250905-191956."
print("== 1. Cudy: echter DeviceType statt Platzhalter")
cudy_mfr = find("/dcim/manufacturers", name="Cudy")
dt_new = goc("/dcim/device-types", {"model": "WR3600E V1.0"}, {
"manufacturer": cudy_mfr["id"], "model": "WR3600E V1.0", "u_height": 0,
"is_full_depth": False, "comments": "Cudy WR3600E V1.0 — Uebergangs-WLAN waehrend der Umstellung."})
LOC = {n: find("/dcim/locations", name=n) for n in ("Technik UG", "Technik OG")}
role_ap = find("/extras/roles", name="Wireless AP")
print("== 2. cudy1 / cudy2")
c1 = find("/dcim/devices", name="cudy1")
patch("/dcim/devices", c1, {
"device_type": dt_new["id"], "location": LOC["Technik UG"]["id"], "status": S_ACTIVE}, "cudy1")
if not DRY:
req("PATCH", f"/dcim/devices/{c1['id']}/", {"comments":
f"{FW} Standalone-WLAN 'elektrosmog.c' auf 10.40.10.246, haengt an sw1 Port 11. "
"Uebergangssender, kein Abbauziel mehr (TP19). WICHTIG: Christophs Notebook haengt "
"per WLAN hierueber — Rettungspfad beim Branch-Tausch."})
goc("/dcim/devices", {"name": "cudy2"}, {
"name": "cudy2", "device_type": dt_new["id"], "role": role_ap["id"],
"location": LOC["Technik OG"]["id"], "status": S_DECOM,
"comments": f"{FW} Ausser Betrieb, wird nicht reaktiviert (TP19, ARP-Sweep bestaetigt "
"keine Praesenz mehr). Frueher 10.40.41.247."})
print("== 3. Platzhalter-DeviceType entfernen")
old = find("/dcim/device-types", model="Cudy AP (Modell TBD)")
if old and not DRY:
if req("GET", f"/dcim/devices/?device_type={old['id']}&limit=1")["count"] == 0:
req("DELETE", f"/dcim/device-types/{old['id']}/")
log.append(" - Cudy AP (Modell TBD) geloescht")
else:
log.append(" ! Platzhalter noch in Benutzung — nicht geloescht")
print("== 4. sw1-Ports (nur Gesichertes + markiert Unverifiziertes)")
sw1 = find("/dcim/devices", name="sw1")
V = {v: find("/ipam/vlans", vid=v) for v in (1, 10, 20, 30, 40, 50, 90, 99, 100)}
PORTS = [
("gi15", "ap1-Trunk (bestaetigt User 2026-07-22). Ziel: wandert auf crs326-core ether4.",
"tagged", 1, [10, 20, 30, 40, 50, 90, 99, 100]),
("gi11", "cudy1 (bestaetigt User 2026-07-22). Christophs WLAN-Pfad haengt hier dran.",
None, None, None),
("gi16", "Arris-Kabelmodem, Access VLAN100 (aus TP4-Doku, UNVERIFIZIERT am Geraet).",
"access", 100, None),
("gi8", "Trunk zu sw2 (aus TP1/TP2-Doku, UNVERIFIZIERT am Geraet).",
"tagged", 1, [10, 20, 30, 40, 50, 90, 99, 100]),
]
for nm, desc, mode, untag, tags in PORTS:
i = goc("/dcim/interfaces", {"device": "sw1", "name": nm}, {
"device": sw1["id"], "name": nm, "type": "1000base-t",
"status": S_ACTIVE, "description": desc}, label=f"sw1:{nm}")
if not DRY and mode:
p = {"mode": mode, "untagged_vlan": V[untag]["id"]}
if tags:
p["tagged_vlans"] = [V[t]["id"] for t in tags]
req("PATCH", f"/dcim/interfaces/{i['id']}/", p)
print("== 5. crs326-core: Zielport fuer ap1")
core = find("/dcim/devices", name="crs326-core")
goc("/dcim/interfaces", {"device": "crs326-core", "name": "ether4"}, {
"device": core["id"], "name": "ether4", "type": "1000base-t", "status": S_ACTIVE,
"description": "Ziel-Port fuer ap1 (heute sw1 #15). User-Vorgabe 2026-07-22. "
"ap1-Uplink wandert dabei von ether2 auf ether1 (PoE-Port)."},
label="crs326-core:ether4")
print("\n".join(log))
print("\n" + ("DRY-RUN — nichts geschrieben." if DRY else "ANGEWENDET."))