obmp-docker/exabgp/bgpls_config.py

261 lines
9.3 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
BGP Link-State (BGP-LS) Configuration Script
=============================================
Enables BGP-LS on all spoke routers so every router appears as a
vantage point in OpenBMP's link-state tables.
Current state (discovered via NETCONF audit):
CORE-01 (10.100.0.100): IS-IS distribute , lsls toward all
CORE-02 (10.100.0.200): IS-IS distribute , lsls toward all
R9K-01: IS-IS distribute , lslsCORE-01 , lslsCORE-02
R9K-02: IS-IS distribute , lslsCORE-01 , lslsCORE-02
R9K-03: IS-IS distribute , lslsCORE-01 , lslsCORE-02
R9K-04: IS-IS distribute , lslsCORE-01 , lslsCORE-02
R9K-05: IS-IS distribute , lslsCORE-01 , lslsCORE-02
R9K-06: IS-IS distribute , lslsCORE-01 , lslsCORE-02
R9K-07: IS-IS distribute , lslsCORE-01 , lslsCORE-02
What this script applies per router:
- IS-IS instance 1: <distribute/> if missing (redistributes IS-IS into BGP-LS)
- BGP neighbor 10.10.255.0: lsls AF if missing (activate toward CORE-01)
- BGP neighbor 10.10.255.20: lsls AF if missing (activate toward CORE-02)
IS-IS instance name: 1
BGP AS: 65020
"""
from ncclient import manager
import xml.etree.ElementTree as ET
import sys
BGP_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-bgp-cfg'
ISIS_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-clns-isis-cfg'
ROUTERS = [
# (mgmt_ip, label, loopback, need_isis_dist, need_lsls_core1, need_lsls_core2)
# R9K-01 through R9K-04 already done in first pass
('10.100.0.1', 'R9K-01', '10.10.255.1', False, False, False), # already OK
('10.100.0.2', 'R9K-02', '10.10.255.2', False, False, False), # already OK
('10.100.0.3', 'R9K-03', '10.10.255.3', False, False, False), # already OK
('10.100.0.4', 'R9K-04', '10.10.255.4', False, False, False), # already OK
# R9K-05/06/07: need global AF init + IS-IS distribute + both CORE peers
('10.100.0.5', 'R9K-05', '10.10.255.5', True, True, True),
('10.100.0.6', 'R9K-06', '10.10.255.6', True, True, True),
('10.100.0.7', 'R9K-07', '10.10.255.7', True, True, True),
]
ISIS_DISTRIBUTE_XML = """
<config>
<isis xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-clns-isis-cfg">
<instances>
<instance>
<instance-name>1</instance-name>
<distribute/>
</instance>
</instances>
</isis>
</config>
"""
BGP_GLOBAL_LSLS_XML = """
<config>
<bgp xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-bgp-cfg">
<instance>
<instance-name>default</instance-name>
<instance-as>
<as>0</as>
<four-byte-as>
<as>65020</as>
<bgp-running/>
<default-vrf>
<global>
<global-afs>
<global-af>
<af-name>lsls</af-name>
<enable/>
</global-af>
</global-afs>
</global>
</default-vrf>
</four-byte-as>
</instance-as>
</instance>
</bgp>
</config>
"""
def bgp_lsls_xml(neighbor_addr):
"""Return edit-config XML to add lsls AF toward a single neighbor."""
return f"""
<config>
<bgp xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-bgp-cfg">
<instance>
<instance-name>default</instance-name>
<instance-as>
<as>0</as>
<four-byte-as>
<as>65020</as>
<bgp-running/>
<default-vrf>
<bgp-entity>
<neighbors>
<neighbor>
<neighbor-address>{neighbor_addr}</neighbor-address>
<neighbor-afs>
<neighbor-af>
<af-name>lsls</af-name>
<activate/>
</neighbor-af>
</neighbor-afs>
</neighbor>
</neighbors>
</bgp-entity>
</default-vrf>
</four-byte-as>
</instance-as>
</instance>
</bgp>
</config>
"""
def configure_router(mgmt_ip, label, need_isis, need_core1, need_core2):
print(f"\n{''*60}")
print(f" Configuring {label} ({mgmt_ip})")
print(f"{''*60}")
print(f" Applying: isis-distribute={'YES' if need_isis else 'skip'} "
f"lsls→CORE-01={'YES' if need_core1 else 'skip'} "
f"lsls→CORE-02={'YES' if need_core2 else 'skip'}")
if not (need_isis or need_core1 or need_core2):
print(" Nothing to do.")
return True
try:
with manager.connect(
host=mgmt_ip,
port=830,
username='webui',
password='cisco',
hostkey_verify=False,
device_params={'name': 'iosxr'},
timeout=20,
) as m:
if need_isis:
print(" → Applying IS-IS distribute...")
m.edit_config(target='candidate', config=ISIS_DISTRIBUTE_XML)
# If both CORE neighbors need lsls, the AF has never been initialized —
# push the global AF first so IOS-XR accepts per-neighbor activation.
if need_core1 and need_core2:
print(" → Initializing BGP link-state global AF...")
m.edit_config(target='candidate', config=BGP_GLOBAL_LSLS_XML)
if need_core1:
print(" → Activating lsls toward CORE-01 (10.10.255.0)...")
m.edit_config(target='candidate', config=bgp_lsls_xml('10.10.255.0'))
if need_core2:
print(" → Activating lsls toward CORE-02 (10.10.255.20)...")
m.edit_config(target='candidate', config=bgp_lsls_xml('10.10.255.20'))
print(" → Committing...")
m.commit()
print(f"{label} done.")
return True
except Exception as e:
print(f" ✗ ERROR on {label}: {e}")
return False
def verify_router(mgmt_ip, label):
"""Re-read config and print summary after applying."""
try:
with manager.connect(
host=mgmt_ip, port=830, username='webui', password='cisco',
hostkey_verify=False, device_params={'name': 'iosxr'}, timeout=10
) as m:
# ISIS distribute
filt_isis = """<filter>
<isis xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-clns-isis-cfg">
<instances><instance><instance-name/><distribute/></instance></instances>
</isis>
</filter>"""
r_isis = m.get_config(source='running', filter=filt_isis)
has_dist = '<distribute' in str(r_isis)
# BGP lsls
filt_bgp = """<filter>
<bgp xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-bgp-cfg">
<instance><instance-name>default</instance-name>
<instance-as><as>0</as><four-byte-as><as>65020</as><bgp-running/>
<default-vrf><bgp-entity><neighbors/></bgp-entity></default-vrf>
</four-byte-as></instance-as>
</instance>
</bgp>
</filter>"""
r_bgp = m.get_config(source='running', filter=filt_bgp)
tree = ET.fromstring(str(r_bgp))
lsls = {}
for nbr in tree.iter(f'{{{BGP_NS}}}neighbor'):
addr = nbr.findtext(f'{{{BGP_NS}}}neighbor-address')
afs = [af.findtext(f'{{{BGP_NS}}}af-name') for af in nbr.iter(f'{{{BGP_NS}}}neighbor-af')]
lsls[addr] = 'lsls' in afs
c1 = '' if lsls.get('10.10.255.0') else ''
c2 = '' if lsls.get('10.10.255.20') else ''
d = '' if has_dist else ''
status = 'OK' if (has_dist and lsls.get('10.10.255.0') and lsls.get('10.10.255.20')) else 'INCOMPLETE'
print(f" {label:8s} ISIS-dist={d} lsls→C1={c1} lsls→C2={c2} [{status}]")
except Exception as e:
print(f" {label:8s} verify error: {e}")
def main():
print("BGP-LS Configuration Script")
print("============================")
print("Targets: all 7 spoke routers")
print()
results = []
for mgmt, label, loopback, need_isis, need_c1, need_c2 in ROUTERS:
ok = configure_router(mgmt, label, need_isis, need_c1, need_c2)
results.append((mgmt, label, ok))
# Summary
print(f"\n{'='*60}")
print("Post-apply verification")
print('='*60)
print(f" {'Router':8s} {'ISIS-dist':9s} {'lsls→C1':7s} {'lsls→C2':7s} Status")
for mgmt, label, ok in results:
if ok:
verify_router(mgmt, label)
else:
print(f" {label:8s} skipped (apply failed)")
failed = [label for _, label, ok in results if not ok]
print()
if failed:
print(f"FAILED: {', '.join(failed)}")
sys.exit(1)
else:
print("All routers configured successfully.")
print()
print("Next: wait ~30s for BGP sessions to exchange BGP-LS NLRIs, then:")
print(" docker exec obmp-psql psql -U openbmp -d openbmp -c \\")
print(" \"SELECT bp.peer_addr, count(DISTINCT ln.hash_id) as nodes,")
print(" count(DISTINCT ll.hash_id) as links")
print(" FROM bgp_peers bp")
print(" LEFT JOIN ls_nodes ln ON ln.peer_hash_id = bp.hash_id")
print(" LEFT JOIN ls_links ll ON ll.peer_hash_id = bp.hash_id")
print(" GROUP BY bp.peer_addr HAVING count(DISTINCT ln.hash_id)>0")
print(" ORDER BY bp.peer_addr;\"")
if __name__ == '__main__':
main()