#!/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 ✓, lsls→CORE-01 ✗, lsls→CORE-02 ✓ R9K-02: IS-IS distribute ✓, lsls→CORE-01 ✗, lsls→CORE-02 ✗ R9K-03: IS-IS distribute ✓, lsls→CORE-01 ✗, lsls→CORE-02 ✗ R9K-04: IS-IS distribute ✓, lsls→CORE-01 ✗, lsls→CORE-02 ✗ R9K-05: IS-IS distribute ✗, lsls→CORE-01 ✗, lsls→CORE-02 ✗ R9K-06: IS-IS distribute ✗, lsls→CORE-01 ✗, lsls→CORE-02 ✗ R9K-07: IS-IS distribute ✗, lsls→CORE-01 ✗, lsls→CORE-02 ✗ What this script applies per router: - IS-IS instance 1: 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 = """ 1 """ BGP_GLOBAL_LSLS_XML = """ default 0 65020 lsls """ def bgp_lsls_xml(neighbor_addr): """Return edit-config XML to add lsls AF toward a single neighbor.""" return f""" default 0 65020 {neighbor_addr} lsls """ 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 = """ """ r_isis = m.get_config(source='running', filter=filt_isis) has_dist = ' default 065020 """ 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()