#!/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.30: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."))