#!/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()