diff --git a/exabgp/bgpls_config.py b/exabgp/bgpls_config.py new file mode 100644 index 0000000..31d2bf2 --- /dev/null +++ b/exabgp/bgpls_config.py @@ -0,0 +1,260 @@ +#!/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() diff --git a/exabgp/startup.sh b/exabgp/startup.sh index 0dba2e6..edb3c76 100644 --- a/exabgp/startup.sh +++ b/exabgp/startup.sh @@ -43,6 +43,7 @@ neighbor ${PEER_1} { api { processes [ api ]; + neighbor-changes; } } @@ -60,6 +61,7 @@ neighbor ${PEER_2} { api { processes [ api ]; + neighbor-changes; } } EOF