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