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>
344 lines
16 KiB
Python
344 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""BF-Netzwerk -> Nautobot Basisdaten-Ingest (idempotent).
|
|
|
|
Bildet den IST-Zustand ab (Stand 2026-07-22), nicht den Zielzustand.
|
|
Quelle: ap1 live via MikroMCP + docs/teilprojekte.md.
|
|
"""
|
|
import json, sys, urllib.request, urllib.error
|
|
|
|
BASE = "http://10.40.20.154:8080/api"
|
|
import os
|
|
TOKEN = os.environ.get("NAUTOBOT_TOKEN", "") # aus docs/nas-vlan20/stacks/210-nautobot.yml (gitignored)
|
|
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 payload={json.dumps(data)}\n -> {e.code} {e.read().decode()[:500]}")
|
|
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):
|
|
"""get-or-create; gibt das Objekt zurueck."""
|
|
name = label or payload.get("name") or payload.get("model") or str(match)
|
|
# Im Dry-Run haengen Folgeobjekte an Platzhalter-IDs -> Lookup nicht moeglich.
|
|
if any(str(v).startswith("DRY-") for v in match.values()):
|
|
log.append(f" + {name} (dry-run, Abhaengigkeit noch nicht angelegt)")
|
|
return {"id": f"DRY-{name}"}
|
|
obj = find(path, **match)
|
|
if obj:
|
|
log.append(f" = {name}")
|
|
return obj
|
|
if DRY:
|
|
log.append(f" + {name} (dry-run)")
|
|
return {"id": f"DRY-{name}"}
|
|
obj = req("POST", path + "/", payload)
|
|
log.append(f" + {name}")
|
|
return obj
|
|
|
|
|
|
def patch(path, obj, payload, label):
|
|
"""Nur die Felder patchen, die sich real unterscheiden."""
|
|
delta = {}
|
|
for k, v in payload.items():
|
|
cur = obj.get(k)
|
|
cur_id = cur.get("id") if isinstance(cur, dict) else cur
|
|
if cur_id != v:
|
|
delta[k] = v
|
|
if not delta:
|
|
log.append(f" = {label} (unveraendert)")
|
|
return obj
|
|
if DRY:
|
|
log.append(f" ~ {label}: {list(delta)} (dry-run)")
|
|
return obj
|
|
out = req("PATCH", f"{path}/{obj['id']}/", delta)
|
|
log.append(f" ~ {label}: {list(delta)}")
|
|
return out
|
|
|
|
|
|
def sid(name):
|
|
return find("/extras/statuses", name=name)["id"]
|
|
|
|
|
|
# ---------------------------------------------------------------- Statuses
|
|
S_ACTIVE, S_OFFLINE, S_DEPR, S_PLANNED = (sid(n) for n in ("Active", "Offline", "Deprecated", "Planned"))
|
|
|
|
print("== 1. Location-Types + Locations")
|
|
site_lt = find("/dcim/location-types", name="Site")
|
|
bereich = goc("/dcim/location-types", {"name": "Bereich"}, {
|
|
"name": "Bereich", "parent": site_lt["id"], "nestable": True,
|
|
"content_types": ["dcim.device"],
|
|
"description": "Raum/Etage innerhalb einer Site",
|
|
})
|
|
bf = find("/dcim/locations", name="BF")
|
|
LOC = {}
|
|
for n, d in [("Technik UG", "Technikraum Untergeschoss - Core/Branch-Switch, ap2"),
|
|
("Wohnzimmer Sideboard", "Sideboard Wohnzimmer - ap1 (Router + AP)")]:
|
|
LOC[n] = goc("/dcim/locations", {"name": n}, {
|
|
"name": n, "location_type": bereich["id"], "parent": bf["id"],
|
|
"status": S_ACTIVE, "description": d})
|
|
|
|
print("== 2. Rollen / Hersteller / Device-Types")
|
|
role_router = find("/extras/roles", name="Router")
|
|
role_switch = find("/extras/roles", name="Switch")
|
|
role_ap = goc("/extras/roles", {"name": "Wireless AP"}, {
|
|
"name": "Wireless AP", "color": "2196f3", "content_types": ["dcim.device"]})
|
|
|
|
MFR = {}
|
|
for m in ("ALLNET", "TP-Link", "Cudy"):
|
|
MFR[m] = goc("/dcim/manufacturers", {"name": m}, {"name": m})
|
|
mt = find("/dcim/manufacturers", name="MikroTik")
|
|
|
|
DT = {}
|
|
for mfr, model, note in [
|
|
("ALLNET", "ALLNET Managed Switch (Modell TBD)", "sw1 Core-Switch, FW 2.2.1, 16x GbE - exaktes Modell noch nachzutragen"),
|
|
("TP-Link", "ES208G", "sw2 Omada Easy Managed, 8 Ports, standalone"),
|
|
("Cudy", "Cudy AP (Modell TBD)", "cudy1 - Standalone-WLAN waehrend der Umstellungsphase"),
|
|
]:
|
|
DT[model] = goc("/dcim/device-types", {"model": model}, {
|
|
"manufacturer": MFR[mfr]["id"], "model": model, "u_height": 1,
|
|
"comments": note}, label=model)
|
|
|
|
dt_hap = find("/dcim/device-types", model="hAP ax2")
|
|
dt_crs = find("/dcim/device-types", model="CRS326-24G-2S+RM")
|
|
plat_ros = find("/dcim/platforms", name="RouterOS")
|
|
|
|
print("== 3. Bestands-Devices korrigieren")
|
|
D = {}
|
|
for name, payload in [
|
|
("ap1", {"location": LOC["Wohnzimmer Sideboard"]["id"], "role": role_router["id"],
|
|
"platform": plat_ros["id"], "status": S_ACTIVE}),
|
|
("ap2", {"location": LOC["Technik UG"]["id"], "role": role_ap["id"],
|
|
"platform": plat_ros["id"], "status": S_OFFLINE}),
|
|
("crs326-core", {"location": LOC["Technik UG"]["id"], "platform": plat_ros["id"],
|
|
"role": role_switch["id"], "status": S_PLANNED}),
|
|
("crs326-branch", {"location": LOC["Technik UG"]["id"], "platform": plat_ros["id"],
|
|
"role": role_switch["id"], "status": S_PLANNED}),
|
|
]:
|
|
dev = find("/dcim/devices", name=name)
|
|
D[name] = patch("/dcim/devices", dev, payload, name)
|
|
|
|
# Werks-Identitaeten der CRS326 als Kommentar festhalten (per MAC unterscheidbar)
|
|
if not DRY:
|
|
for name, note in [
|
|
("crs326-core", "Werkszustand. RouterOS 7.19.5. MAC 04:F4:1C:B4:17:76, software-id 4W04-URHT, Werks-IP 192.168.88.1."),
|
|
("crs326-branch", "Werkszustand. RouterOS 7.19.5. MAC 04:F4:1C:B4:15:58, software-id RGZJ-UK0T, Werks-IP 192.168.88.1. ap2 haengt an Port 24."),
|
|
]:
|
|
req("PATCH", f"/dcim/devices/{D[name]['id']}/", {"comments": note})
|
|
|
|
print("== 4. Uebergangs-Devices (ohne Platform -> nie Golden Config)")
|
|
for name, dt, role, loc, note in [
|
|
("sw1", "ALLNET Managed Switch (Modell TBD)", role_switch, bf,
|
|
"ALLNET Core-Switch, Mgmt 10.40.41.11 (VLAN1). Nur Telnet-CLI, kein RouterOS. Wird von crs326-core abgeloest (TP15)."),
|
|
("sw2", "ES208G", role_switch, bf,
|
|
"Omada ES208G, Mgmt 10.40.99.12 (statisch). Standalone ohne Controller (TP2.1). Interim bis CRS326-Ausbau."),
|
|
("cudy1", "Cudy AP (Modell TBD)", role_ap, bf,
|
|
"Standalone-WLAN 'elektrosmog.c' auf 10.40.10.246. Uebergangssender, kein Abbauziel mehr (TP19)."),
|
|
]:
|
|
goc("/dcim/devices", {"name": name}, {
|
|
"name": name, "device_type": DT[dt]["id"], "role": role["id"],
|
|
"location": loc["id"], "status": S_ACTIVE, "comments": note})
|
|
|
|
print("== 5. VLAN-Group + VLANs")
|
|
vg = goc("/ipam/vlan-groups", {"name": "BF"}, {
|
|
"name": "BF", "location": bf["id"], "description": "Kanonisches VLAN-Modell Standort BF"})
|
|
|
|
VLANS = [
|
|
(1, "LEGACY", S_DEPR, "Alt-/Default-VLAN. Abbau laeuft (TP19/TP19.1). Traegt noch sw1-Mgmt + ap1-Bridge-IPs."),
|
|
(10, "MAIN", S_ACTIVE, "Trusted: Handy/Laptop/Apple Watch"),
|
|
(20, "INFRA", S_ACTIVE, "NAS/VMs/Docker/HA"),
|
|
(30, "IOT", S_ACTIVE, "Smart-Home (HA-managed), Shellys"),
|
|
(40, "MEDIA", S_ACTIVE, "Sonos, AppleTV, TV, Cast-Ziele"),
|
|
(50, "UNTRUSTED", S_ACTIVE, "Spielekonsole, eBook-Reader, Low-Trust"),
|
|
(90, "GUEST", S_ACTIVE, "nur Internet"),
|
|
(99, "MGMT", S_ACTIVE, "Switch-/AP-/Hypervisor-Mgmt"),
|
|
(100, "WAN", S_ACTIVE, "Transfernetz zum Arris-Kabelmodem (Liwest, Bridge-Mode)"),
|
|
]
|
|
V = {}
|
|
for vid, nm, st, desc in VLANS:
|
|
V[vid] = goc("/ipam/vlans", {"vid": vid, "vlan_group": vg["id"]}, {
|
|
"vid": vid, "name": nm, "vlan_group": vg["id"], "status": st,
|
|
"location": bf["id"], "description": desc}, label=f"VLAN{vid} {nm}")
|
|
|
|
print("== 6. Prefixes")
|
|
PFX = [
|
|
("10.40.41.0/24", 1, S_DEPR, "VLAN1 LEGACY - Abbau laeuft (TP19)"),
|
|
("10.40.10.0/24", 10, S_ACTIVE, "VLAN10 MAIN"),
|
|
("10.40.20.0/24", 20, S_ACTIVE, "VLAN20 INFRA"),
|
|
("10.40.30.0/24", 30, S_ACTIVE, "VLAN30 IOT"),
|
|
("10.40.40.0/24", 40, S_ACTIVE, "VLAN40 MEDIA"),
|
|
("10.40.50.0/24", 50, S_ACTIVE, "VLAN50 UNTRUSTED"),
|
|
("10.40.90.0/24", 90, S_ACTIVE, "VLAN90 GUEST"),
|
|
("10.40.99.0/24", 99, S_ACTIVE, "VLAN99 MGMT"),
|
|
]
|
|
for pfx, vid, st, desc in PFX:
|
|
ex = find("/ipam/prefixes", prefix=pfx)
|
|
payload = {"vlan": V[vid]["id"], "status": st, "description": desc, "location": bf["id"]}
|
|
if ex:
|
|
patch("/ipam/prefixes", ex, payload, pfx)
|
|
else:
|
|
goc("/ipam/prefixes", {"prefix": pfx}, dict(prefix=pfx, **payload), label=pfx)
|
|
|
|
print("== 7. ap1-Interfaces (Ist, live von ap1)")
|
|
ETH = "1000base-t"
|
|
IFACES = [
|
|
# name, type, mac, description
|
|
("ether1", ETH, "D0:EA:11:4D:40:E0", "ungenutzt (WAN lief hier vor dem Edge-Cutover)"),
|
|
("ether2", ETH, "D0:EA:11:4D:40:E1", "Trunk zu sw1 GE1 - traegt WAN vlan100 + alle Client-VLANs"),
|
|
("ether3", ETH, "D0:EA:11:4D:40:E2", ""),
|
|
("ether4", ETH, "D0:EA:11:4D:40:E3", ""),
|
|
("ether5", ETH, "D0:EA:11:4D:40:E4", ""),
|
|
("bridge", "bridge", "D0:EA:11:4D:40:E1", "vlan-filtering=yes; VLAN1 untagged, 10-100 tagged"),
|
|
("lo", "virtual", None, "loopback"),
|
|
("wg-hub", "virtual", None, "TP10 Multi-Site VPN Hub (FM/DW spokes), UDP 51821, MTU 1412"),
|
|
]
|
|
for vid in (10, 20, 30, 40, 50, 90, 99, 100):
|
|
IFACES.append((f"vlan{vid}", "virtual", None, ""))
|
|
WIFI = [
|
|
("wifi1", None, "5 GHz Master, cfg-main-5g, ch36/5180, SSID dirtyvader-main -> VLAN10"),
|
|
("wifi2", None, "2.4 GHz Master, cfg-main-2g, ch1/2412, SSID dirtyvader-main -> KEIN datapath/VLAN (egress untagged VLAN1!)"),
|
|
("w2-guest", "wifi2", "cfg-guest -> VLAN90"),
|
|
("w2-iot", "wifi2", "cfg-iot -> VLAN30 (multicast-enhance)"),
|
|
("w2-media", "wifi2", "cfg-media -> VLAN40"),
|
|
("w2-untrusted", "wifi2", "cfg-untrusted -> VLAN50"),
|
|
("w5-guest", "wifi1", "cfg-guest -> VLAN90"),
|
|
("w5-media", "wifi1", "cfg-media -> VLAN40"),
|
|
("w5-untrusted", "wifi1", "cfg-untrusted -> VLAN50"),
|
|
]
|
|
|
|
ap1 = D["ap1"]
|
|
IF = {}
|
|
for nm, typ, mac, desc in IFACES:
|
|
p = {"device": ap1["id"], "name": nm, "type": typ, "status": S_ACTIVE, "description": desc}
|
|
if mac:
|
|
p["mac_address"] = mac
|
|
IF[nm] = goc("/dcim/interfaces", {"device": "ap1", "name": nm}, p, label=f"ap1:{nm}")
|
|
|
|
for nm, master, desc in WIFI:
|
|
full = f"master-interface={master}; {desc}" if master else desc
|
|
p = {"device": ap1["id"], "name": nm, "type": "ieee802.11ax",
|
|
"status": S_ACTIVE, "description": full}
|
|
IF[nm] = goc("/dcim/interfaces", {"device": "ap1", "name": nm}, p, label=f"ap1:{nm}")
|
|
|
|
# VLAN-Subinterfaces an die Bridge haengen + tagged-VLAN am Trunk
|
|
if not DRY:
|
|
for vid in (10, 20, 30, 40, 50, 90, 99, 100):
|
|
req("PATCH", f"/dcim/interfaces/{IF[f'vlan{vid}']['id']}/", {
|
|
"parent_interface": IF["bridge"]["id"], "mode": "tagged",
|
|
"tagged_vlans": [V[vid]["id"]]})
|
|
# WLAN-VAPs: Nautobot erlaubt parent_interface nur fuer virtuelle Interfaces.
|
|
# Master-Beziehung steht daher in der description; hier nur die VLAN-Zuordnung.
|
|
for nm, vid in [("w2-guest", 90), ("w2-iot", 30), ("w2-media", 40), ("w2-untrusted", 50),
|
|
("w5-guest", 90), ("w5-media", 40), ("w5-untrusted", 50), ("wifi1", 10)]:
|
|
req("PATCH", f"/dcim/interfaces/{IF[nm]['id']}/", {
|
|
"bridge": IF["bridge"]["id"], "mode": "access", "untagged_vlan": V[vid]["id"]})
|
|
req("PATCH", f"/dcim/interfaces/{IF['ether2']['id']}/", {
|
|
"mode": "tagged", "untagged_vlan": V[1]["id"],
|
|
"tagged_vlans": [V[v]["id"] for v in (10, 20, 30, 40, 50, 90, 99, 100)]})
|
|
|
|
print("== 8. ap1-IP-Adressen")
|
|
IPS = [
|
|
("10.40.41.1/24", "bridge", "defconf VLAN1 (Legacy)"),
|
|
("10.40.41.254/24", "bridge", "Secondary = alte FB-GW-IP, haelt statische .41-Hosts online"),
|
|
("10.40.10.1/24", "vlan10", "MAIN GW"),
|
|
("10.40.20.1/24", "vlan20", "INFRA GW"),
|
|
("10.40.30.1/24", "vlan30", "IOT GW"),
|
|
("10.40.40.1/24", "vlan40", "MEDIA GW"),
|
|
("10.40.50.1/24", "vlan50", "UNTRUSTED GW"),
|
|
("10.40.90.1/24", "vlan90", "GUEST GW"),
|
|
("10.40.99.1/24", "vlan99", "MGMT GW"),
|
|
]
|
|
for addr, ifname, desc in IPS:
|
|
ip = find("/ipam/ip-addresses", address=addr)
|
|
if not ip:
|
|
ip = goc("/ipam/ip-addresses", {"address": addr},
|
|
{"address": addr, "status": S_ACTIVE, "description": desc,
|
|
"namespace": find("/ipam/namespaces", name="Global")["id"]}, label=addr)
|
|
if not DRY and not find("/dcim/interfaces", device="ap1", name=ifname, ip_addresses=ip["id"]):
|
|
try:
|
|
req("POST", "/ipam/ip-address-to-interface/", {
|
|
"ip_address": ip["id"], "interface": IF[ifname]["id"]})
|
|
log.append(f" + {addr} -> ap1:{ifname}")
|
|
except urllib.error.HTTPError:
|
|
log.append(f" = {addr} -> ap1:{ifname} (bereits verknuepft)")
|
|
|
|
print("== 9. WLAN: Controller + Gruppe + Netze")
|
|
ctrl = goc("/dcim/controllers", {"name": "bf-wlan"}, {
|
|
"name": "bf-wlan", "status": S_ACTIVE, "location": bf["id"],
|
|
"description": "Logischer WLAN-Controller = Nautobot Golden Config. KEIN CAPsMAN "
|
|
"(Entscheid 2026-07-22): beide APs bekommen eigenstaendige, identisch "
|
|
"generierte RouterOS-Configs."})
|
|
grp = goc("/dcim/controller-managed-device-groups", {"name": "BF-WLAN-APs"}, {
|
|
"name": "BF-WLAN-APs", "controller": ctrl["id"],
|
|
"description": "ap1 + ap2 - standalone provisioniert, kein CAPsMAN"})
|
|
if not DRY:
|
|
for n in ("ap1", "ap2"):
|
|
req("PATCH", f"/dcim/devices/{D[n]['id']}/", {"controller_managed_device_group": grp["id"]})
|
|
|
|
# ssid, vlan, auth, ros-detail
|
|
NETS = [
|
|
("dirtyvader-main", 10, "WPA3 Personal",
|
|
"RouterOS wpa3-psk, FT+FT-over-DS=yes. 5 GHz (wifi1) taggt VLAN10; "
|
|
"2.4 GHz (wifi2/cfg-main-2g) hat KEINEN datapath -> egress untagged VLAN1. Bekannter Defekt."),
|
|
("dirtyvader-infra", 20, "WPA2 Personal",
|
|
"RouterOS wpa2-psk+wpa3-psk, ft=no. Konfiguration cfg-infra existiert auf ap1, "
|
|
"wird aber von KEINEM Interface benutzt -> sendet dort nicht (TP3.3)."),
|
|
("dirtyvader-iot", 30, "WPA2 Personal",
|
|
"RouterOS wpa2-psk+wpa3-psk, ft=no, multicast-enhance. Nur 2.4 GHz (w2-iot)."),
|
|
("dirtyvader-media", 40, "WPA2 Personal",
|
|
"RouterOS wpa2-psk+wpa3-psk, ft=no (bewusst, wegen Sonos), multicast-enhance. 2.4 + 5 GHz."),
|
|
("dirtyvader-untrusted", 50, "WPA2 Personal",
|
|
"RouterOS wpa2-psk+wpa3-psk, ft=no. 2.4 + 5 GHz."),
|
|
("dirtyvader-guest", 90, "WPA2 Personal",
|
|
"RouterOS wpa2-psk+wpa3-psk, FT+FT-over-DS=yes. 2.4 + 5 GHz."),
|
|
]
|
|
for ssid, vid, auth, desc in NETS:
|
|
wn = goc("/wireless/wireless-networks", {"name": ssid}, {
|
|
"name": ssid, "ssid": ssid, "mode": "Standalone (Autonomous)",
|
|
"authentication": auth, "enabled": True, "hidden": False,
|
|
"description": desc}, label=ssid)
|
|
if not DRY:
|
|
ex = find("/wireless/controller-managed-device-group-wireless-network-assignments",
|
|
wireless_network=wn["id"], controller_managed_device_group=grp["id"])
|
|
if not ex:
|
|
req("POST", "/wireless/controller-managed-device-group-wireless-network-assignments/", {
|
|
"controller_managed_device_group": grp["id"], "wireless_network": wn["id"],
|
|
"vlan": V[vid]["id"]})
|
|
log.append(f" + {ssid} -> VLAN{vid}")
|
|
|
|
print("== 10. Radio-Profile (Ist ap1)")
|
|
for nm, freq, chans, width, desc in [
|
|
("BF 2.4GHz ap1", "2.4GHz", [1], [20], "ap1 wifi2: 2412 MHz, tx-power 20 dBm"),
|
|
("BF 5GHz ap1", "5GHz", [36], [20, 40, 80], "ap1 wifi1: 5180 MHz, skip-dfs-channels=10min-cac"),
|
|
]:
|
|
rp = goc("/wireless/radio-profiles", {"name": nm}, {
|
|
"name": nm, "frequency": freq, "regulatory_domain": "AT",
|
|
"allowed_channel_list": chans, "channel_width": width, "description": desc})
|
|
if not DRY:
|
|
ex = find("/wireless/controller-managed-device-group-radio-profile-assignments",
|
|
radio_profile=rp["id"], controller_managed_device_group=grp["id"])
|
|
if not ex:
|
|
req("POST", "/wireless/controller-managed-device-group-radio-profile-assignments/", {
|
|
"controller_managed_device_group": grp["id"], "radio_profile": rp["id"]})
|
|
|
|
print("\n".join(log))
|
|
print(f"\n{'DRY-RUN — nichts geschrieben. Mit --apply ausfuehren.' if DRY else 'ANGEWENDET.'}")
|