198 lines
7.5 KiB
Python
198 lines
7.5 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Peer the CML core routers with the GoBGP full-table feed (roadmap E1).
|
||
|
|
|
||
|
|
GoBGP (AS65001, 10.40.40.250) holds the full real Internet table pulled from
|
||
|
|
the Bromirski route server. This script configures CORE-01/CORE-02 (AS65020)
|
||
|
|
to peer eBGP with GoBGP and accept that table. As route reflectors the cores
|
||
|
|
then propagate it to every R9K client -- so all 9 lab routers carry and
|
||
|
|
BMP-export a full table. This is an intentional lab stress test of the
|
||
|
|
OpenBMP ingestion/storage path.
|
||
|
|
|
||
|
|
Applied per core (additive -- no existing session/policy is modified):
|
||
|
|
* route-policy GOBGP-FEED-PASS (a plain `pass` policy; eBGP needs one)
|
||
|
|
* neighbor 10.40.40.202 remote-as 65001, ebgp-multihop, mgmt-sourced,
|
||
|
|
IPv4-unicast only, with a maximum-prefix safety cap.
|
||
|
|
|
||
|
|
The matching GoBGP side is gobgp/gobgpd.conf (neighbors 10.100.0.100/.200);
|
||
|
|
restart GoBGP after applying: docker compose up -d gobgp
|
||
|
|
|
||
|
|
IOS-XR BMP config is not exposed via NETCONF on this release, so -- like
|
||
|
|
cml/proxmox_bmp_config.py -- this applies config over the SSH CLI.
|
||
|
|
|
||
|
|
Covers both labs: CML cores (AS65020) and PROX cores (AS65021).
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python3 cml/gobgp_peering_config.py # apply, all 4 cores
|
||
|
|
python3 cml/gobgp_peering_config.py cml # apply, CML cores only
|
||
|
|
python3 cml/gobgp_peering_config.py prox # apply, PROX cores only
|
||
|
|
python3 cml/gobgp_peering_config.py --remove # ROLLBACK, all cores
|
||
|
|
python3 cml/gobgp_peering_config.py --remove prox # ROLLBACK, PROX only
|
||
|
|
|
||
|
|
Rollback: `--remove` deletes the GoBGP neighbor and the GOBGP-FEED-PASS
|
||
|
|
policy from the cores. To stop the feed instantly without touching router
|
||
|
|
config, `docker compose stop gobgp` -- the eBGP sessions drop and the full
|
||
|
|
table is withdrawn fleet-wide within seconds. See gobgp/README.md.
|
||
|
|
"""
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
import paramiko
|
||
|
|
|
||
|
|
# GoBGP runs network_mode: host, so it sources BGP TCP from the host's real
|
||
|
|
# interface IP (10.40.40.202) -- NOT its router-id 10.40.40.250. The cores
|
||
|
|
# must peer with the host IP.
|
||
|
|
GOBGP_IP = "10.40.40.202"
|
||
|
|
GOBGP_AS = "65001"
|
||
|
|
|
||
|
|
# Additive config, built per core (asn = that core's local BGP AS:
|
||
|
|
# CML lab = 65020, PROX lab = 65021). Flat formal-form lines applied at the
|
||
|
|
# (config)# prompt.
|
||
|
|
# IPv4-unicast only: the cores have no global IPv6 address, so an ipv6-unicast
|
||
|
|
# AF on this IPv4-transport session holds the whole neighbor Idle. The IPv6
|
||
|
|
# full-table feed is a separate phase (needs a v6-transport session or v6
|
||
|
|
# addressing on the cores).
|
||
|
|
def apply_lines(asn):
|
||
|
|
n = f"router bgp {asn} neighbor {GOBGP_IP}"
|
||
|
|
return [
|
||
|
|
"route-policy GOBGP-FEED-PASS",
|
||
|
|
" pass",
|
||
|
|
"end-policy",
|
||
|
|
f"{n} remote-as {GOBGP_AS}",
|
||
|
|
f"{n} description GoBGP full-table feed (lab stress test)",
|
||
|
|
f"{n} ebgp-multihop 64",
|
||
|
|
f"{n} update-source MgmtEth0/RP0/CPU0/0",
|
||
|
|
f"{n} address-family ipv4 unicast route-policy GOBGP-FEED-PASS in",
|
||
|
|
f"{n} address-family ipv4 unicast route-policy GOBGP-FEED-PASS out",
|
||
|
|
f"{n} address-family ipv4 unicast maximum-prefix 1500000 90",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
# Rollback -- remove the neighbor (and its sub-config) then the policy.
|
||
|
|
def remove_lines(asn):
|
||
|
|
return [
|
||
|
|
f"no router bgp {asn} neighbor {GOBGP_IP}",
|
||
|
|
"no route-policy GOBGP-FEED-PASS",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
# (name, mgmt_ip, user, password, local_asn) -- both labs.
|
||
|
|
CORES = [
|
||
|
|
("CML-CORE-01", "10.100.0.100", "webui", "cisco", "65020"),
|
||
|
|
("CML-CORE-02", "10.100.0.200", "webui", "cisco", "65020"),
|
||
|
|
("PROX-CORE-01", "10.100.1.100", "admin", "cisco", "65021"),
|
||
|
|
("PROX-CORE-02", "10.100.1.200", "admin", "cisco", "65021"),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def _drain(shell, settle=0.4, limit=20.0, until=None):
|
||
|
|
out, start = "", time.time()
|
||
|
|
while time.time() - start < limit:
|
||
|
|
time.sleep(settle)
|
||
|
|
if shell.recv_ready():
|
||
|
|
out += shell.recv(65535).decode(errors="replace")
|
||
|
|
if until and until in out:
|
||
|
|
break
|
||
|
|
elif until is None:
|
||
|
|
break
|
||
|
|
elif until in out:
|
||
|
|
break
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def configure_core(name, ip, user, pwd, asn, mode):
|
||
|
|
verb = "applying" if mode == "apply" else "removing"
|
||
|
|
lines = apply_lines(asn) if mode == "apply" else remove_lines(asn)
|
||
|
|
print(f"\n=== {name} ({ip}) AS{asn} -- {verb} GoBGP peering ===")
|
||
|
|
try:
|
||
|
|
ssh = paramiko.SSHClient()
|
||
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||
|
|
ssh.connect(ip, username=user, password=pwd, timeout=15,
|
||
|
|
look_for_keys=False, allow_agent=False)
|
||
|
|
shell = ssh.invoke_shell(width=220, height=2000)
|
||
|
|
time.sleep(2)
|
||
|
|
shell.recv(65535)
|
||
|
|
CFG = "(config)#"
|
||
|
|
|
||
|
|
shell.send("terminal length 0\n")
|
||
|
|
_drain(shell, 0.4, 5)
|
||
|
|
shell.send("configure terminal\n")
|
||
|
|
out = _drain(shell, 0.4, 15, until=CFG)
|
||
|
|
if CFG not in out:
|
||
|
|
print(f" FAIL: could not enter config mode\n {out[-200:]}")
|
||
|
|
ssh.close()
|
||
|
|
return False
|
||
|
|
|
||
|
|
for line in lines:
|
||
|
|
shell.send(line + "\n")
|
||
|
|
time.sleep(0.4)
|
||
|
|
_drain(shell, 0.3, 8, until=CFG)
|
||
|
|
|
||
|
|
shell.send("show configuration\n")
|
||
|
|
cand = _drain(shell, 0.3, 10, until=CFG)
|
||
|
|
if GOBGP_IP not in cand and "GOBGP-FEED-PASS" not in cand:
|
||
|
|
print(f" OK: no changes ({mode} already in effect)")
|
||
|
|
shell.send("abort\n")
|
||
|
|
_drain(shell, 0.5, 5)
|
||
|
|
ssh.close()
|
||
|
|
return True
|
||
|
|
|
||
|
|
shell.send("commit\n")
|
||
|
|
result = _drain(shell, 0.3, 25, until=CFG)
|
||
|
|
if "fail" in result.lower() or "error" in result.lower():
|
||
|
|
print(f" FAIL: commit error\n {result[-300:]}")
|
||
|
|
shell.send("abort\n")
|
||
|
|
_drain(shell, 0.5, 5)
|
||
|
|
ssh.close()
|
||
|
|
return False
|
||
|
|
|
||
|
|
shell.send("end\n")
|
||
|
|
_drain(shell, 1.0, 8)
|
||
|
|
if mode == "apply":
|
||
|
|
shell.send(f"show bgp ipv4 unicast neighbors {GOBGP_IP} | include BGP state\n")
|
||
|
|
verify = _drain(shell, 1.0, 12)
|
||
|
|
state = next((l.strip() for l in verify.splitlines()
|
||
|
|
if "BGP state" in l), "(state not yet reported)")
|
||
|
|
print(f" committed. {state}")
|
||
|
|
else:
|
||
|
|
shell.send(f"show running-config router bgp | include {GOBGP_IP}\n")
|
||
|
|
verify = _drain(shell, 1.0, 12)
|
||
|
|
gone = GOBGP_IP not in verify.replace(f"include {GOBGP_IP}", "")
|
||
|
|
print(f" committed. neighbor removed: {gone}")
|
||
|
|
ssh.close()
|
||
|
|
return True
|
||
|
|
except Exception as e:
|
||
|
|
print(f" FAIL: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
args = [a for a in sys.argv[1:]]
|
||
|
|
mode = "apply"
|
||
|
|
if "--remove" in args:
|
||
|
|
mode = "remove"
|
||
|
|
args.remove("--remove")
|
||
|
|
target = args[0].lower() if args else None
|
||
|
|
|
||
|
|
if mode == "remove":
|
||
|
|
print("ROLLBACK: removing GoBGP peering from the core routers.")
|
||
|
|
results = {}
|
||
|
|
for name, ip, user, pwd, asn in CORES:
|
||
|
|
if target and target not in name.lower():
|
||
|
|
continue
|
||
|
|
results[name] = configure_core(name, ip, user, pwd, asn, mode)
|
||
|
|
|
||
|
|
print(f"\n{'='*48}\n SUMMARY ({mode})")
|
||
|
|
for name, ok in results.items():
|
||
|
|
print(f" {name:22s} {'OK' if ok else 'FAILED'}")
|
||
|
|
if mode == "apply":
|
||
|
|
print("\nNext: restart GoBGP to load the new neighbors:")
|
||
|
|
print(" docker compose up -d gobgp")
|
||
|
|
else:
|
||
|
|
print("\nGoBGP container config still lists the cores; that is inert")
|
||
|
|
print("with the neighbors removed. To fully revert, also restore the")
|
||
|
|
print("previous gobgp/gobgpd.conf and run: docker compose up -d gobgp")
|
||
|
|
sys.exit(0 if all(results.values()) else 1)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|