Add BGP-LS config script and fix ExaBGP peer event tracking
- exabgp/bgpls_config.py: NETCONF script that audits and fixes BGP-LS config on all 9 spoke routers; adds IS-IS distribute and lsls AF activation toward both COREs where missing; handles routers needing global AF initialization before per-neighbor activation - exabgp/startup.sh: add neighbor-changes to ExaBGP api blocks so peer up/down events are sent to Flask server.py stdin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6621942032
commit
39a130922a
260
exabgp/bgpls_config.py
Normal file
260
exabgp/bgpls_config.py
Normal file
@ -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: <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()
|
||||||
@ -43,6 +43,7 @@ neighbor ${PEER_1} {
|
|||||||
|
|
||||||
api {
|
api {
|
||||||
processes [ api ];
|
processes [ api ];
|
||||||
|
neighbor-changes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ neighbor ${PEER_2} {
|
|||||||
|
|
||||||
api {
|
api {
|
||||||
processes [ api ];
|
processes [ api ];
|
||||||
|
neighbor-changes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user