#!/usr/bin/env python3
"""
Route Diversity Configuration Script
=====================================
Adds loopbacks, static routes, route-policies, and BGP redistribution
to R9K-01 through R9K-07 to create locally-originated routes that
produce meaningful RR Loc-RIB diffs between CORE-01 and CORE-02.
IS-IS topology (natural asymmetry — no metric tuning needed):
CORE-01 → R9K-01, R9K-02, R9K-03, R9K-04, R9K-05
CORE-02 → R9K-05, R9K-06, R9K-07
R9K-04 ↔ R9K-06 (cross-link)
R9K-05 dual-homed to both COREs
Overlapping prefixes from CORE-01-side and CORE-02-side routers produce
next-hop diffs because each RR picks the client with lowest IGP cost.
Address plan:
Loopbacks: 10.110.{router_id}.{1,2}/32 (unique per router)
Overlap LBs: 10.110.{100-103}.1/32 (shared across router pairs)
Static routes: 10.111.{router_id}.0/24 (unique per router)
Overlap statics: 10.111.{100-103}.0/24 (shared across router pairs)
Usage:
python3 route_diversity_config.py # apply all config
python3 route_diversity_config.py --verify-only # just check current state
python3 route_diversity_config.py --rollback # remove all added config
"""
from ncclient import manager
import xml.etree.ElementTree as ET
import sys
import argparse
# YANG namespaces
IFMGR_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg'
IPV4IO_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-io-cfg'
STATIC_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-ip-static-cfg'
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'
RPL_NS = 'http://cisco.com/ns/yang/Cisco-IOS-XR-policy-repository-cfg'
# ──────────────────────────────────────────────────────────────────────
# Router definitions
# ──────────────────────────────────────────────────────────────────────
ROUTERS = {
'R9K-01': {
'mgmt': '10.100.0.1',
'loopbacks': [
('Loopback10', '10.110.1.1', '255.255.255.255'),
('Loopback11', '10.110.1.2', '255.255.255.255'),
('Loopback100', '10.110.100.1', '255.255.255.255'), # overlap with R9K-06
('Loopback103', '10.110.103.1', '255.255.255.255'), # overlap with R9K-04, R9K-07
],
'statics': [
('10.111.1.0', 24, 100), # unique, tag=100 → LP=200
('10.111.100.0', 24, 100), # overlap with R9K-06 (tag=200)
('10.111.103.0', 24, 100), # overlap with R9K-04, R9K-07 (same tag)
],
},
'R9K-02': {
'mgmt': '10.100.0.2',
'loopbacks': [
('Loopback10', '10.110.2.1', '255.255.255.255'),
('Loopback11', '10.110.2.2', '255.255.255.255'),
('Loopback101', '10.110.101.1', '255.255.255.255'), # overlap with R9K-07
],
'statics': [
('10.111.2.0', 24, 100),
('10.111.101.0', 24, 100), # overlap with R9K-07 (tag=300)
],
},
'R9K-03': {
'mgmt': '10.100.0.3',
'loopbacks': [
('Loopback10', '10.110.3.1', '255.255.255.255'),
('Loopback11', '10.110.3.2', '255.255.255.255'),
('Loopback102', '10.110.102.1', '255.255.255.255'), # overlap with R9K-05
],
'statics': [
('10.111.3.0', 24, 100),
('10.111.102.0', 24, 100), # overlap with R9K-04 (tag=200)
],
},
'R9K-04': {
'mgmt': '10.100.0.4',
'loopbacks': [
('Loopback10', '10.110.4.1', '255.255.255.255'),
('Loopback11', '10.110.4.2', '255.255.255.255'),
('Loopback103', '10.110.103.1', '255.255.255.255'), # overlap with R9K-01, R9K-07
],
'statics': [
('10.111.4.0', 24, 200), # tag=200 → LP=150
('10.111.102.0', 24, 200), # overlap with R9K-03 (tag=100)
('10.111.103.0', 24, 100), # overlap with R9K-01, R9K-07 (same tag)
],
},
'R9K-05': {
'mgmt': '10.100.0.5',
'loopbacks': [
('Loopback10', '10.110.5.1', '255.255.255.255'),
('Loopback11', '10.110.5.2', '255.255.255.255'),
('Loopback102', '10.110.102.1', '255.255.255.255'), # overlap with R9K-03
],
'statics': [
('10.111.5.0', 24, 100),
],
},
'R9K-06': {
'mgmt': '10.100.0.6',
'loopbacks': [
('Loopback10', '10.110.6.1', '255.255.255.255'),
('Loopback11', '10.110.6.2', '255.255.255.255'),
('Loopback100', '10.110.100.1', '255.255.255.255'), # overlap with R9K-01
],
'statics': [
('10.111.6.0', 24, 200), # tag=200 → LP=150
('10.111.100.0', 24, 200), # overlap with R9K-01 (tag=100)
],
},
'R9K-07': {
'mgmt': '10.100.0.7',
'loopbacks': [
('Loopback10', '10.110.7.1', '255.255.255.255'),
('Loopback11', '10.110.7.2', '255.255.255.255'),
('Loopback101', '10.110.101.1', '255.255.255.255'), # overlap with R9K-02
('Loopback103', '10.110.103.1', '255.255.255.255'), # overlap with R9K-01, R9K-04
],
'statics': [
('10.111.7.0', 24, 300), # tag=300 → LP=100
('10.111.101.0', 24, 300), # overlap with R9K-02 (tag=100)
('10.111.103.0', 24, 100), # overlap with R9K-01, R9K-04 (same tag)
],
},
}
# ──────────────────────────────────────────────────────────────────────
# Route-policy (RPL text blob)
# ──────────────────────────────────────────────────────────────────────
ROUTE_POLICY_NAME = 'REDIST-TO-BGP'
ROUTE_POLICY_BODY = """\
route-policy REDIST-TO-BGP
if tag is 100 then
set local-preference 200
set med 50
set community (65020:100) additive
pass
elseif tag is 200 then
set local-preference 150
set med 100
set community (65020:200) additive
pass
elseif tag is 300 then
set local-preference 100
set med 200
set community (65020:300) additive
pass
else
set local-preference 100
pass
endif
end-policy
"""
# ──────────────────────────────────────────────────────────────────────
# XML builders
# ──────────────────────────────────────────────────────────────────────
def loopback_xml(name, addr, mask):
"""Create a loopback interface with an IPv4 address."""
return f"""
act
{name}
{addr}
{mask}
"""
def static_route_xml(prefix, prefix_len, tag):
"""Create a static route to Null0 with a tag."""
return f"""
{prefix}
{prefix_len}
Null0
{tag}
"""
def route_policy_xml(name, body):
"""Create/replace a route-policy (RPL text blob)."""
return f"""
{name}
{body}
"""
def isis_passive_xml(intf_name):
"""Add a loopback to IS-IS instance 1 (passive by default for loopbacks)."""
return f"""
1
{intf_name}
ipv4
unicast
"""
def bgp_redistribute_xml():
"""Configure redistribute connected + static with REDIST-TO-BGP policy."""
return f"""
default
0
65020
ipv4-unicast
{ROUTE_POLICY_NAME}
{ROUTE_POLICY_NAME}
"""
# ──────────────────────────────────────────────────────────────────────
# Rollback XML builders (delete operations)
# ──────────────────────────────────────────────────────────────────────
NC_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
def delete_loopback_xml(name):
return f"""
act
{name}
"""
def delete_static_route_xml(prefix, prefix_len):
return f"""
{prefix}
{prefix_len}
"""
def delete_bgp_redistribute_xml():
return f"""
default
0
65020
ipv4-unicast
"""
def delete_isis_interface_xml(intf_name):
return f"""
1
{intf_name}
"""
def delete_route_policy_xml(name):
return f"""
{name}
"""
# ──────────────────────────────────────────────────────────────────────
# Configuration functions
# ──────────────────────────────────────────────────────────────────────
def nc_connect(mgmt_ip):
"""Open NETCONF session."""
return manager.connect(
host=mgmt_ip,
port=830,
username='webui',
password='cisco',
hostkey_verify=False,
device_params={'name': 'iosxr'},
timeout=30,
)
def configure_router(label, cfg):
"""Apply full route-diversity config to a single router."""
mgmt_ip = cfg['mgmt']
print(f"\n{'─'*60}")
print(f" Configuring {label} ({mgmt_ip})")
print(f"{'─'*60}")
lb_names = [lb[0] for lb in cfg['loopbacks']]
static_prefixes = [f"{s[0]}/{s[1]}" for s in cfg['statics']]
print(f" Loopbacks: {', '.join(lb_names)}")
print(f" Statics: {', '.join(static_prefixes)}")
try:
with nc_connect(mgmt_ip) as m:
# Phase 1: Route-policy (must exist before BGP references it)
print(f" → Creating route-policy {ROUTE_POLICY_NAME}...")
m.edit_config(target='candidate', config=route_policy_xml(ROUTE_POLICY_NAME, ROUTE_POLICY_BODY))
# Phase 2: Loopback interfaces
for name, addr, mask in cfg['loopbacks']:
print(f" → Creating {name} ({addr})...")
m.edit_config(target='candidate', config=loopback_xml(name, addr, mask))
# Phase 3: Static routes
for prefix, plen, tag in cfg['statics']:
print(f" → Static {prefix}/{plen} → Null0 tag={tag}...")
m.edit_config(target='candidate', config=static_route_xml(prefix, plen, tag))
# Phase 4: IS-IS passive on new loopbacks
for name, _, _ in cfg['loopbacks']:
print(f" → IS-IS passive: {name}...")
m.edit_config(target='candidate', config=isis_passive_xml(name))
# Phase 5: BGP redistribution
print(f" → BGP redistribute connected + static...")
m.edit_config(target='candidate', config=bgp_redistribute_xml())
# Phase 6: Commit
print(f" → Committing...")
m.commit()
print(f" ✓ {label} done.")
return True
except Exception as e:
print(f" ✗ ERROR on {label}: {e}")
return False
def rollback_router(label, cfg):
"""Remove all route-diversity config from a single router."""
mgmt_ip = cfg['mgmt']
print(f"\n{'─'*60}")
print(f" Rolling back {label} ({mgmt_ip})")
print(f"{'─'*60}")
try:
with nc_connect(mgmt_ip) as m:
# Remove BGP redistribution first (references the policy)
print(f" → Removing BGP redistribute...")
try:
m.edit_config(target='candidate', config=delete_bgp_redistribute_xml())
except Exception as e:
print(f" (skip — may not exist: {e})")
# Remove IS-IS interfaces
for name, _, _ in cfg['loopbacks']:
print(f" → Removing IS-IS interface {name}...")
try:
m.edit_config(target='candidate', config=delete_isis_interface_xml(name))
except Exception as e:
print(f" (skip: {e})")
# Remove static routes
for prefix, plen, _ in cfg['statics']:
print(f" → Removing static {prefix}/{plen}...")
try:
m.edit_config(target='candidate', config=delete_static_route_xml(prefix, plen))
except Exception as e:
print(f" (skip: {e})")
# Remove loopbacks
for name, _, _ in cfg['loopbacks']:
print(f" → Removing {name}...")
try:
m.edit_config(target='candidate', config=delete_loopback_xml(name))
except Exception as e:
print(f" (skip: {e})")
# Remove route-policy
print(f" → Removing route-policy {ROUTE_POLICY_NAME}...")
try:
m.edit_config(target='candidate', config=delete_route_policy_xml(ROUTE_POLICY_NAME))
except Exception as e:
print(f" (skip: {e})")
print(f" → Committing rollback...")
m.commit()
print(f" ✓ {label} rolled back.")
return True
except Exception as e:
print(f" ✗ ERROR rolling back {label}: {e}")
return False
def verify_router(label, cfg):
"""Check if route-diversity config is present on a router."""
mgmt_ip = cfg['mgmt']
try:
with nc_connect(mgmt_ip) as m:
# Check loopbacks
filt_intf = f"""
"""
r_intf = str(m.get_config(source='running', filter=filt_intf))
found_lbs = []
for name, _, _ in cfg['loopbacks']:
if name in r_intf:
found_lbs.append(name)
# Check route-policy
filt_rpl = f"""
"""
r_rpl = str(m.get_config(source='running', filter=filt_rpl))
has_policy = ROUTE_POLICY_NAME in r_rpl
# Check BGP redistribute
filt_bgp = f"""
default
"""
r_bgp = str(m.get_config(source='running', filter=filt_bgp))
has_redist_connected = 'connected-routes' in r_bgp and ROUTE_POLICY_NAME in r_bgp
has_redist_static = 'static-routes' in r_bgp and ROUTE_POLICY_NAME in r_bgp
total_lbs = len(cfg['loopbacks'])
lb_str = f"{len(found_lbs)}/{total_lbs}"
pol = '✓' if has_policy else '✗'
rc = '✓' if has_redist_connected else '✗'
rs = '✓' if has_redist_static else '✗'
ok = len(found_lbs) == total_lbs and has_policy and has_redist_connected and has_redist_static
status = 'OK' if ok else 'INCOMPLETE'
print(f" {label:8s} LBs={lb_str:5s} Policy={pol} Redist-C={rc} Redist-S={rs} [{status}]")
except Exception as e:
print(f" {label:8s} verify error: {e}")
# ──────────────────────────────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description='Route Diversity Configuration for RR Diff Analysis')
parser.add_argument('--verify-only', action='store_true', help='Only verify current state')
parser.add_argument('--rollback', action='store_true', help='Remove all added config')
args = parser.parse_args()
print("Route Diversity Configuration Script")
print("=" * 60)
print(f"Targets: {len(ROUTERS)} routers ({', '.join(ROUTERS.keys())})")
print()
if args.verify_only:
print("Verify-only mode")
print('-' * 60)
print(f" {'Router':8s} {'LBs':5s} {'Policy':6s} {'Redist-C':8s} {'Redist-S':8s} Status")
for label, cfg in ROUTERS.items():
verify_router(label, cfg)
return
if args.rollback:
print("ROLLBACK mode — removing all route-diversity config")
print('-' * 60)
results = []
for label, cfg in ROUTERS.items():
ok = rollback_router(label, cfg)
results.append((label, ok))
failed = [l for l, ok in results if not ok]
print()
if failed:
print(f"FAILED rollback: {', '.join(failed)}")
sys.exit(1)
else:
print("All routers rolled back successfully.")
return
# Apply mode
results = []
for label, cfg in ROUTERS.items():
ok = configure_router(label, cfg)
results.append((label, ok))
# Post-apply verification
print(f"\n{'='*60}")
print("Post-apply verification")
print('=' * 60)
print(f" {'Router':8s} {'LBs':5s} {'Policy':6s} {'Redist-C':8s} {'Redist-S':8s} Status")
for label, cfg in ROUTERS.items():
verify_router(label, cfg)
failed = [l for l, ok in results if not ok]
print()
if failed:
print(f"FAILED: {', '.join(failed)}")
sys.exit(1)
else:
total_lbs = sum(len(c['loopbacks']) for c in ROUTERS.values())
total_statics = sum(len(c['statics']) for c in ROUTERS.values())
print(f"All routers configured successfully.")
print(f" {total_lbs} loopbacks + {total_statics} static routes created")
print()
print("Wait ~60s for BGP convergence and BMP collection, then verify:")
print()
print(" # Check new prefixes in OpenBMP")
print(" docker exec -i obmp-psql psql -U openbmp -d openbmp -c \\")
print(" \"SELECT prefix::text, COUNT(*) FROM ip_rib")
print(" WHERE (prefix::text LIKE '10.110.%' OR prefix::text LIKE '10.111.%')")
print(" AND iswithdrawn = false GROUP BY prefix ORDER BY prefix;\"")
if __name__ == '__main__':
main()