TP15: CRS326-Core als L3-Router aktiviert
- crs326-core nimmt Routing/DHCP/DNS/NAT/WAN/WireGuard von ap1 ueber. - L2-Portplan korrigiert (ap1 #15, Arris WAN #16, branch #8, Sonos #2/3/5/6, IoT #14, VLAN10-Access #11, VLAN1-Hybrid #3/7/9/10/12/17-24). - Backup-RSC gefixt (ether3-24 disabled) und via fixed-RSC eingespielt. - Gestagede L3-Config enabled in einem Rutsch. - Falsche statische Default-Route 0.0.0.0/0 -> 10.40.41.1 entfernt, WAN-DHCP via 90.146.33.177/24 aktiv. - SFP-Ports bleiben disabled (Stromersparnis). - ap1 ist L3-stillgelegt und wird in naechster Session reiner AP. Backup-Dateien (*.backup) und leases-export.json bleiben lokal. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
153
nautobot/ingest-crs326-core-l3.py
Normal file
153
nautobot/ingest-crs326-core-l3.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""BF-Netzwerk -> Nautobot: crs326-core L3-Datenmodell ergaenzen (TP15 Cutover-Prep).
|
||||
|
||||
Erstellt fuer crs326-core:
|
||||
- vlan10..vlan90, vlan100 als virtuelle Bridge-Subinterfaces (tagged)
|
||||
- GW-IPs 10.40.<vlan>.1/24 (v10..v90)
|
||||
- Primaer-IP bleibt 10.40.99.11/24 (Bring-up-Mgmt)
|
||||
- Rolle Router, Status bleibt Planned bis Cutover
|
||||
|
||||
Idempotent; verwendet GraphQL fuer tagged_vlans (REST liefert null).
|
||||
"""
|
||||
import json, os, sys, urllib.request, urllib.error, urllib.parse
|
||||
|
||||
BASE = "http://10.40.20.30:8080/api"
|
||||
TOKEN = os.environ.get("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 payload={json.dumps(data)}\n -> {e.code} {e.read().decode()[:500]}")
|
||||
raise
|
||||
|
||||
|
||||
def gql(query):
|
||||
r = urllib.request.Request(f"{BASE}/graphql/", data=json.dumps({"query": query}).encode(), method="POST")
|
||||
r.add_header("Authorization", f"Token {TOKEN}")
|
||||
r.add_header("Content-Type", "application/json")
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
data = json.loads(resp.read())
|
||||
if data.get("errors"):
|
||||
print("GraphQL errors:", data["errors"])
|
||||
raise SystemExit(1)
|
||||
return data["data"]
|
||||
|
||||
|
||||
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 str(match)
|
||||
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):
|
||||
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"]
|
||||
|
||||
|
||||
S_ACTIVE = sid("Active")
|
||||
S_PLANNED = sid("Planned")
|
||||
role_router = find("/extras/roles", name="Router")
|
||||
ns_global = find("/ipam/namespaces", name="Global")["id"]
|
||||
vg = find("/ipam/vlan-groups", name="BF")
|
||||
|
||||
V = {}
|
||||
for vid in (1, 10, 20, 30, 40, 50, 90, 99, 100):
|
||||
V[vid] = find("/ipam/vlans", vid=vid, vlan_group=vg["id"])
|
||||
|
||||
core = find("/dcim/devices", name="crs326-core")
|
||||
print("== crs326-core: Rolle + Status")
|
||||
patch("/dcim/devices", core, {"role": role_router["id"]}, "crs326-core role=Router")
|
||||
|
||||
print("== crs326-core: VLAN-Subinterfaces (L3 SVIs)")
|
||||
br = find("/dcim/interfaces", device="crs326-core", name="bridge")
|
||||
for vid in (10, 20, 30, 40, 50, 90, 99, 100):
|
||||
ifname = f"vlan{vid}"
|
||||
desc = f"L3 SVI VLAN{vid}" if vid != 99 else "L3 SVI VLAN99 (Mgmt + Router-GW)"
|
||||
obj = goc("/dcim/interfaces", {"device": core["id"], "name": ifname},
|
||||
{"device": core["id"], "name": ifname, "type": "virtual",
|
||||
"status": S_ACTIVE, "description": desc,
|
||||
"mode": "tagged", "parent_interface": br["id"],
|
||||
"tagged_vlans": [V[vid]["id"]]}, label=f"core:{ifname}")
|
||||
if not DRY and not str(obj["id"]).startswith("DRY"):
|
||||
# Nachträglich Parent/Mode/VLAN sicherstellen (goc legt nur an, patch bei Änderung)
|
||||
req("PATCH", f"/dcim/interfaces/{obj['id']}/",
|
||||
{"parent_interface": br["id"], "mode": "tagged",
|
||||
"tagged_vlans": [V[vid]["id"]]})
|
||||
|
||||
print("== crs326-core: GW-IPs je VLAN (v10..v99)")
|
||||
IF = {}
|
||||
for vid in (10, 20, 30, 40, 50, 90, 99):
|
||||
IF[vid] = find("/dcim/interfaces", device="crs326-core", name=f"vlan{vid}")
|
||||
for vid in (10, 20, 30, 40, 50, 90, 99):
|
||||
addr = f"10.40.{vid}.1/24"
|
||||
desc = f"VLAN{vid} GW (core, staged)"
|
||||
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": ns_global}, label=addr)
|
||||
if DRY or str(ip["id"]).startswith("DRY"):
|
||||
continue
|
||||
ex = find("/dcim/interfaces", device="crs326-core", name=f"vlan{vid}", ip_addresses=ip["id"])
|
||||
if not ex:
|
||||
try:
|
||||
req("POST", "/ipam/ip-address-to-interface/",
|
||||
{"ip_address": ip["id"], "interface": IF[vid]["id"]})
|
||||
log.append(f" + {addr} -> core:vlan{vid}")
|
||||
except urllib.error.HTTPError:
|
||||
log.append(f" = {addr} -> core:vlan{vid} (bereits)")
|
||||
|
||||
print("== crs326-core: Kommentar (L3-Router-Ziel)")
|
||||
if not DRY:
|
||||
req("PATCH", f"/dcim/devices/{core['id']}/",
|
||||
{"comments": "TP15 2026-07-23: grundkonfiguriert via REST (Bring-up-Level). "
|
||||
"MAC 04:F4:1C:B4:17:76, software-id 4W04-URHT, Serial HKA0AREXJFM. "
|
||||
"Wird neuer L3-Router (Routing/DHCP/DNS/NAT von ap1 -> core). "
|
||||
"Mgmt 10.40.99.11 (VLAN99) + GW-IPs .1 je Client-VLAN staged. "
|
||||
"VLAN1-Temp 10.40.41.211. Status Planned bis Cutover."})
|
||||
|
||||
print("\n".join(log))
|
||||
print(f"\n{'DRY-RUN - nichts geschrieben. Mit --apply ausfuehren.' if DRY else 'ANGEWENDET.'}")
|
||||
126
nautobot/mikrotik_routeros.j2
Normal file
126
nautobot/mikrotik_routeros.j2
Normal file
@@ -0,0 +1,126 @@
|
||||
# Nautobot Golden Config -- intended RouterOS config for {{ obj.name }}
|
||||
# Generated from Nautobot device data + hardcoded TP15 L3 staged values.
|
||||
# Scope: identity for all MikroTik devices; full staged L3 cutover config for crs326-core.
|
||||
|
||||
/system identity set name={{ obj.name }}
|
||||
|
||||
{% if obj.name == "crs326-core" %}
|
||||
# =====================================================================================
|
||||
# TP15 — crs326-core L3 staged config (ap1 -> core). NICHT automatisch aktivieren.
|
||||
# Alles Betriebsumlenkende wird disabled=yes angelegt; Cutover aktiviert in einem Rutsch.
|
||||
# =====================================================================================
|
||||
|
||||
# [1] INTERFACE-LISTS
|
||||
/interface/list add name=WAN comment="TP15"
|
||||
/interface/list add name=LAN comment="TP15"
|
||||
/interface/list/member add list=WAN interface=vlan100
|
||||
/interface/list/member add list=LAN interface=vlan10
|
||||
/interface/list/member add list=LAN interface=vlan20
|
||||
/interface/list/member add list=LAN interface=vlan30
|
||||
/interface/list/member add list=LAN interface=vlan40
|
||||
/interface/list/member add list=LAN interface=vlan50
|
||||
/interface/list/member add list=LAN interface=vlan90
|
||||
/interface/list/member add list=LAN interface=vlan99
|
||||
|
||||
# [2] GW-IPs je VLAN — STAGED (disabled=yes)
|
||||
/ip/address
|
||||
{% for iface in obj.interfaces.all() %}
|
||||
{% if iface.name.startswith("vlan") %}
|
||||
{% for ip in iface.ip_addresses.all() %}
|
||||
{% if ip.host == "10.40." + iface.name|replace("vlan", "") + ".1" %}
|
||||
add address={{ ip.address }} interface={{ iface.name }} comment="{{ iface.description }}" disabled=yes
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
# [3] WAN DHCP-Client (Liwest/Arris) — STAGED
|
||||
/ip/dhcp-client add interface=vlan100 use-peer-dns=no add-default-route=yes \
|
||||
comment="TP15 WAN Liwest (Arris)" disabled=yes
|
||||
|
||||
# [4] DHCP-Server je VLAN
|
||||
/ip/pool add name=pool-v10 ranges=10.40.10.100-10.40.10.200
|
||||
/ip/pool add name=pool-v20 ranges=10.40.20.100-10.40.20.200
|
||||
/ip/pool add name=pool-v30 ranges=10.40.30.100-10.40.30.250
|
||||
/ip/pool add name=pool-v40 ranges=10.40.40.100-10.40.40.200
|
||||
/ip/pool add name=pool-v50 ranges=10.40.50.100-10.40.50.200
|
||||
/ip/pool add name=pool-v90 ranges=10.40.90.100-10.40.90.200
|
||||
/ip/pool add name=pool-v99 ranges=10.40.99.100-10.40.99.200
|
||||
|
||||
/ip/dhcp-server/network add address=10.40.10.0/24 gateway=10.40.10.1 dns-server=10.40.10.1 domain=bf.lan
|
||||
/ip/dhcp-server/network add address=10.40.20.0/24 gateway=10.40.20.1 dns-server=10.40.20.1 domain=bf.lan
|
||||
/ip/dhcp-server/network add address=10.40.30.0/24 gateway=10.40.30.1 dns-server=10.40.30.1 domain=bf.lan
|
||||
/ip/dhcp-server/network add address=10.40.40.0/24 gateway=10.40.40.1 dns-server=10.40.40.1 domain=bf.lan
|
||||
/ip/dhcp-server/network add address=10.40.50.0/24 gateway=10.40.50.1 dns-server=10.40.50.1 domain=bf.lan
|
||||
/ip/dhcp-server/network add address=10.40.90.0/24 gateway=10.40.90.1 dns-server=10.40.90.1 domain=bf.lan
|
||||
/ip/dhcp-server/network add address=10.40.99.0/24 gateway=10.40.99.1 dns-server=10.40.99.1 domain=bf.lan
|
||||
|
||||
/ip/dhcp-server add name=dhcp-v10 interface=vlan10 address-pool=pool-v10 lease-time=1d add-dns-entries-suffix=lan comment=MAIN disabled=yes
|
||||
/ip/dhcp-server add name=dhcp-v20 interface=vlan20 address-pool=pool-v20 lease-time=1d add-dns-entries-suffix=lan comment=INFRA disabled=yes
|
||||
/ip/dhcp-server add name=dhcp-v30 interface=vlan30 address-pool=pool-v30 lease-time=1d add-dns-entries-suffix=lan comment=IOT disabled=yes
|
||||
/ip/dhcp-server add name=dhcp-v40 interface=vlan40 address-pool=pool-v40 lease-time=1d add-dns-entries-suffix=lan comment=MEDIA disabled=yes
|
||||
/ip/dhcp-server add name=dhcp-v50 interface=vlan50 address-pool=pool-v50 lease-time=1d add-dns-entries-suffix=lan comment=UNTRUSTED disabled=yes
|
||||
/ip/dhcp-server add name=dhcp-v90 interface=vlan90 address-pool=pool-v90 lease-time=12h add-dns-entries-suffix=lan comment=GUEST disabled=yes
|
||||
/ip/dhcp-server add name=dhcp-v99 interface=vlan99 address-pool=pool-v99 lease-time=1d add-dns-entries-suffix=lan comment=MGMT disabled=yes
|
||||
|
||||
# [5] DNS
|
||||
/ip/dns set allow-remote-requests=yes servers=1.1.1.1 \
|
||||
use-doh-server=https://px35ynlol3.cloudflare-gateway.com/dns-query verify-doh-cert=yes \
|
||||
mdns-repeat-ifaces=vlan10,vlan20,vlan40,vlan30,vlan90
|
||||
|
||||
/ip/dns/static add name=router.lan address=10.40.99.1 comment="core = GW/Router"
|
||||
/ip/dns/static add name=ap1.bf.lan address=10.40.99.3 comment="ap1 = reiner AP"
|
||||
/ip/dns/static add name=ap2.bf.lan address=10.40.99.2 comment="ap2 = reiner AP"
|
||||
/ip/dns/static add name=core.bf.lan address=10.40.99.1 comment="crs326-core (L3)"
|
||||
/ip/dns/static add name=branch.bf.lan address=10.40.99.12 comment="crs326-branch (nach Renumber .13->.12)"
|
||||
/ip/dns/static add name=macmini.bf.lan address=10.40.20.167
|
||||
/ip/dns/static add name=pve.bf.lan address=10.40.20.100
|
||||
/ip/dns/static add name=pve-mcp.bf.lan address=10.40.20.173
|
||||
/ip/dns/static add name=nas.bf.lan address=10.40.20.10
|
||||
/ip/dns/static add name=docker.bf.lan address=10.40.20.126
|
||||
/ip/dns/static add name=platform-pg.bf.lan address=10.40.20.20
|
||||
|
||||
# [6] NAT
|
||||
/ip/firewall/nat add chain=srcnat dst-address=10.40.0.0/16 action=accept \
|
||||
comment="TP4: no-NAT inter-VLAN (vor masquerade)"
|
||||
/ip/firewall/nat add chain=srcnat out-interface-list=WAN action=masquerade \
|
||||
comment="defconf: masquerade WAN"
|
||||
|
||||
# [7] FIREWALL FILTER
|
||||
/ip/firewall/filter add chain=forward action=accept connection-state=established,related,untracked comment="defconf: accept est,rel,untracked"
|
||||
/ip/firewall/filter add chain=forward action=fasttrack-connection connection-state=established,related comment="defconf: fasttrack"
|
||||
/ip/firewall/filter add chain=forward action=drop connection-state=invalid comment="defconf: drop invalid"
|
||||
/ip/firewall/filter add chain=forward action=drop connection-nat-state=!dstnat in-interface-list=WAN comment="defconf: drop WAN !dstnat"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=wg-hub comment="TP10: allow from WG spokes (FM/DW)"
|
||||
/ip/firewall/filter add chain=forward action=accept out-interface=wg-hub comment="TP10: allow to WG spokes (FM/DW)"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=vlan10 out-interface=vlan40 comment="TP5: MAIN->MEDIA (AirPlay/Sonos)"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=vlan40 out-interface=vlan10 comment="TP5: MEDIA->MAIN"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=vlan20 out-interface=vlan40 comment="TP5: INFRA->MEDIA (HA<->Sonos)"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=vlan40 out-interface=vlan20 comment="TP5: MEDIA->INFRA"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=vlan90 out-interface=vlan40 comment="TP5: GUEST->MEDIA (Sonos/Cast)"
|
||||
/ip/firewall/filter add chain=forward action=accept in-interface=vlan40 out-interface=vlan90 comment="TP5: MEDIA->GUEST"
|
||||
|
||||
/ip/firewall/filter add chain=input action=accept connection-state=established,related,untracked comment="TP10-seal-est"
|
||||
/ip/firewall/filter add chain=input action=drop connection-state=invalid comment="TP10-seal-inv"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=51821 comment="TP10: WG hub listen"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=68 in-interface-list=WAN comment="TP10-seal-dhcp"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=icmp comment="TP10-seal-icmp"
|
||||
/ip/firewall/filter add chain=input action=accept in-interface-list=LAN comment="TP10-seal-lan"
|
||||
/ip/firewall/filter add chain=input action=drop in-interface-list=WAN comment="TP10-seal-wandrop"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=5353 in-interface=vlan10 comment="TP5: mDNS in vlan10"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=5353 in-interface=vlan40 comment="TP5: mDNS in vlan40"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=5353 in-interface=vlan20 comment="TP5: mDNS in vlan20"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=5353 in-interface=vlan30 comment="TP5: mDNS in vlan30"
|
||||
/ip/firewall/filter add chain=input action=accept protocol=udp dst-port=5353 in-interface=vlan90 comment="TP5: mDNS in vlan90"
|
||||
|
||||
# [8] WG-HUB (TP10 Multi-Site) — private-key wird im Cutover-Fenster gesetzt
|
||||
/interface/wireguard add name=wg-hub mtu=1412 listen-port=51821 private-key="<<< AP1_WG_HUB_PRIVKEY >>>" \
|
||||
comment="TP10 Multi-Site VPN Hub (FM/DW spokes)" disabled=yes
|
||||
/interface/wireguard/peers add interface=wg-hub name=fm allowed-address=10.101.0.0/24 \
|
||||
public-key="dzRY0lnnUygijChWe5wXZmXPeNcbdALpqBptA3fyiVE=" \
|
||||
endpoint-address=abxhutfgsid9snmy.myfritz.net endpoint-port=52476
|
||||
/interface/wireguard/peers add interface=wg-hub name=dw allowed-address=10.95.10.0/24 \
|
||||
public-key="FqYZLztQ/qME5VDie5QLGH++5299WJiE1dPpQnPbzUw="
|
||||
/ip/route add dst-address=10.101.0.0/24 gateway=wg-hub comment="TP10: FM spoke -> wg-hub"
|
||||
/ip/route add dst-address=10.95.10.0/24 gateway=wg-hub comment="TP10: DW spoke -> wg-hub"
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user