Add RR Loc-RIB diff dashboard and route diversity config
Dashboard compares Adj-RIB-In tables between two Route Reflectors via BMP, showing missing prefixes, attribute diffs (next-hop, AS path), and per-client consistency. Route diversity script deploys 29 prefixes across R9K-01-07 via NETCONF to create verifiable next-hop differences between RRs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
45f4c9859d
commit
541f018bc5
658
exabgp/route_diversity_config.py
Normal file
658
exabgp/route_diversity_config.py
Normal file
@ -0,0 +1,658 @@
|
||||
#!/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"""
|
||||
<config>
|
||||
<interface-configurations xmlns="{IFMGR_NS}">
|
||||
<interface-configuration>
|
||||
<active>act</active>
|
||||
<interface-name>{name}</interface-name>
|
||||
<interface-virtual/>
|
||||
<ipv4-network xmlns="{IPV4IO_NS}">
|
||||
<addresses>
|
||||
<primary>
|
||||
<address>{addr}</address>
|
||||
<netmask>{mask}</netmask>
|
||||
</primary>
|
||||
</addresses>
|
||||
</ipv4-network>
|
||||
</interface-configuration>
|
||||
</interface-configurations>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def static_route_xml(prefix, prefix_len, tag):
|
||||
"""Create a static route to Null0 with a tag."""
|
||||
return f"""
|
||||
<config>
|
||||
<router-static xmlns="{STATIC_NS}">
|
||||
<default-vrf>
|
||||
<address-family>
|
||||
<vrfipv4>
|
||||
<vrf-unicast>
|
||||
<vrf-prefixes>
|
||||
<vrf-prefix>
|
||||
<prefix>{prefix}</prefix>
|
||||
<prefix-length>{prefix_len}</prefix-length>
|
||||
<vrf-route>
|
||||
<vrf-next-hop-table>
|
||||
<vrf-next-hop-interface-name>
|
||||
<interface-name>Null0</interface-name>
|
||||
<tag>{tag}</tag>
|
||||
</vrf-next-hop-interface-name>
|
||||
</vrf-next-hop-table>
|
||||
</vrf-route>
|
||||
</vrf-prefix>
|
||||
</vrf-prefixes>
|
||||
</vrf-unicast>
|
||||
</vrfipv4>
|
||||
</address-family>
|
||||
</default-vrf>
|
||||
</router-static>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def route_policy_xml(name, body):
|
||||
"""Create/replace a route-policy (RPL text blob)."""
|
||||
return f"""
|
||||
<config>
|
||||
<routing-policy xmlns="{RPL_NS}">
|
||||
<route-policies>
|
||||
<route-policy>
|
||||
<route-policy-name>{name}</route-policy-name>
|
||||
<rpl-route-policy>{body}</rpl-route-policy>
|
||||
</route-policy>
|
||||
</route-policies>
|
||||
</routing-policy>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def isis_passive_xml(intf_name):
|
||||
"""Add a loopback to IS-IS instance 1 (passive by default for loopbacks)."""
|
||||
return f"""
|
||||
<config>
|
||||
<isis xmlns="{ISIS_NS}">
|
||||
<instances>
|
||||
<instance>
|
||||
<instance-name>1</instance-name>
|
||||
<interfaces>
|
||||
<interface>
|
||||
<interface-name>{intf_name}</interface-name>
|
||||
<running/>
|
||||
<interface-afs>
|
||||
<interface-af>
|
||||
<af-name>ipv4</af-name>
|
||||
<saf-name>unicast</saf-name>
|
||||
<interface-af-data/>
|
||||
</interface-af>
|
||||
</interface-afs>
|
||||
</interface>
|
||||
</interfaces>
|
||||
</instance>
|
||||
</instances>
|
||||
</isis>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def bgp_redistribute_xml():
|
||||
"""Configure redistribute connected + static with REDIST-TO-BGP policy."""
|
||||
return f"""
|
||||
<config>
|
||||
<bgp xmlns="{BGP_NS}">
|
||||
<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>ipv4-unicast</af-name>
|
||||
<enable/>
|
||||
<connected-routes>
|
||||
<route-policy-name>{ROUTE_POLICY_NAME}</route-policy-name>
|
||||
</connected-routes>
|
||||
<static-routes>
|
||||
<route-policy-name>{ROUTE_POLICY_NAME}</route-policy-name>
|
||||
</static-routes>
|
||||
</global-af>
|
||||
</global-afs>
|
||||
</global>
|
||||
</default-vrf>
|
||||
</four-byte-as>
|
||||
</instance-as>
|
||||
</instance>
|
||||
</bgp>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Rollback XML builders (delete operations)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
NC_NS = 'urn:ietf:params:xml:ns:netconf:base:1.0'
|
||||
|
||||
|
||||
def delete_loopback_xml(name):
|
||||
return f"""
|
||||
<config>
|
||||
<interface-configurations xmlns="{IFMGR_NS}">
|
||||
<interface-configuration xmlns:nc="{NC_NS}" nc:operation="delete">
|
||||
<active>act</active>
|
||||
<interface-name>{name}</interface-name>
|
||||
</interface-configuration>
|
||||
</interface-configurations>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def delete_static_route_xml(prefix, prefix_len):
|
||||
return f"""
|
||||
<config>
|
||||
<router-static xmlns="{STATIC_NS}">
|
||||
<default-vrf>
|
||||
<address-family>
|
||||
<vrfipv4>
|
||||
<vrf-unicast>
|
||||
<vrf-prefixes>
|
||||
<vrf-prefix xmlns:nc="{NC_NS}" nc:operation="delete">
|
||||
<prefix>{prefix}</prefix>
|
||||
<prefix-length>{prefix_len}</prefix-length>
|
||||
</vrf-prefix>
|
||||
</vrf-prefixes>
|
||||
</vrf-unicast>
|
||||
</vrfipv4>
|
||||
</address-family>
|
||||
</default-vrf>
|
||||
</router-static>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def delete_bgp_redistribute_xml():
|
||||
return f"""
|
||||
<config>
|
||||
<bgp xmlns="{BGP_NS}">
|
||||
<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>ipv4-unicast</af-name>
|
||||
<enable/>
|
||||
<connected-routes xmlns:nc="{NC_NS}" nc:operation="delete"/>
|
||||
<static-routes xmlns:nc="{NC_NS}" nc:operation="delete"/>
|
||||
</global-af>
|
||||
</global-afs>
|
||||
</global>
|
||||
</default-vrf>
|
||||
</four-byte-as>
|
||||
</instance-as>
|
||||
</instance>
|
||||
</bgp>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def delete_isis_interface_xml(intf_name):
|
||||
return f"""
|
||||
<config>
|
||||
<isis xmlns="{ISIS_NS}">
|
||||
<instances>
|
||||
<instance>
|
||||
<instance-name>1</instance-name>
|
||||
<interfaces>
|
||||
<interface xmlns:nc="{NC_NS}" nc:operation="delete">
|
||||
<interface-name>{intf_name}</interface-name>
|
||||
</interface>
|
||||
</interfaces>
|
||||
</instance>
|
||||
</instances>
|
||||
</isis>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
def delete_route_policy_xml(name):
|
||||
return f"""
|
||||
<config>
|
||||
<routing-policy xmlns="{RPL_NS}">
|
||||
<route-policies>
|
||||
<route-policy xmlns:nc="{NC_NS}" nc:operation="delete">
|
||||
<route-policy-name>{name}</route-policy-name>
|
||||
</route-policy>
|
||||
</route-policies>
|
||||
</routing-policy>
|
||||
</config>
|
||||
"""
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 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"""<filter>
|
||||
<interface-configurations xmlns="{IFMGR_NS}"/>
|
||||
</filter>"""
|
||||
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"""<filter>
|
||||
<routing-policy xmlns="{RPL_NS}"/>
|
||||
</filter>"""
|
||||
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"""<filter>
|
||||
<bgp xmlns="{BGP_NS}">
|
||||
<instance><instance-name>default</instance-name></instance>
|
||||
</bgp>
|
||||
</filter>"""
|
||||
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()
|
||||
418
obmp-grafana/dashboards/Learning/rr_locrib_diff.json
Normal file
418
obmp-grafana/dashboards/Learning/rr_locrib_diff.json
Normal file
@ -0,0 +1,418 @@
|
||||
{
|
||||
"annotations": {"list": [{"builtIn": 1,"datasource": {"type": "datasource","uid": "grafana"},"enable": true,"hide": true,"iconColor": "rgba(0, 211, 255, 1)","name": "Annotations & Alerts","target": {"limit": 100,"matchAny": false,"tags": [],"type": "dashboard"},"type": "dashboard"}]},
|
||||
"description": "Compare Route Reflector Loc-RIB tables via BMP Adj-RIB-In. Surfaces missing prefixes, attribute differences, and per-client consistency between two RRs.",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 1,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {"type": "datasource","uid": "grafana"},
|
||||
"gridPos": {"h": 5,"w": 24,"x": 0,"y": 0},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"content": "## Route Reflector Loc-RIB Diff\n\nCompares the RIB tables of two Route Reflectors via BMP. Select your two RRs using the **RR1** and **RR2** dropdowns above.\n\n**What this shows:**\n- **Summary stats** — prefix counts and diff totals\n- **Missing prefixes** — routes present on one RR but not the other (expected for each RR's own locally-originated routes)\n- **Attribute differences** — same prefix on both RRs but with different next-hop, AS path, or other attributes (indicates different best-path selection)\n- **Per-client consistency** — route counts each client sends to each RR (should be identical in a healthy RR cluster)",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"title": "RR Loc-RIB Diff — Overview",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Total active (non-withdrawn) prefixes on RR1",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"fixedColor": "blue","mode": "fixed"},"thresholds": {"mode": "absolute","steps": [{"color": "blue","value": null}]}},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4,"w": 4,"x": 0,"y": 5},
|
||||
"id": 10,
|
||||
"options": {"colorMode": "value","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT COUNT(DISTINCT r.prefix || '/' || r.prefix_len) AS \"prefixes\"\nFROM ip_rib r\nJOIN bgp_peers p ON p.hash_id = r.peer_hash_id\nJOIN routers rt ON rt.hash_id = p.router_hash_id\nWHERE rt.name = '$rr1' AND r.iswithdrawn = false",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "$rr1 Prefixes",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Total active (non-withdrawn) prefixes on RR2",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"fixedColor": "green","mode": "fixed"},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4,"w": 4,"x": 4,"y": 5},
|
||||
"id": 11,
|
||||
"options": {"colorMode": "value","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT COUNT(DISTINCT r.prefix || '/' || r.prefix_len) AS \"prefixes\"\nFROM ip_rib r\nJOIN bgp_peers p ON p.hash_id = r.peer_hash_id\nJOIN routers rt ON rt.hash_id = p.router_hash_id\nWHERE rt.name = '$rr2' AND r.iswithdrawn = false",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "$rr2 Prefixes",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Prefixes present on RR1 but missing from RR2",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"fixedColor": "orange","mode": "fixed"},"thresholds": {"mode": "absolute","steps": [{"color": "orange","value": null},{"color": "red","value": 1}]}},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4,"w": 4,"x": 8,"y": 5},
|
||||
"id": 12,
|
||||
"options": {"colorMode": "value","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT COUNT(*) AS \"only_rr1\"\nFROM (\n SELECT DISTINCT r1.prefix, r1.prefix_len\n FROM ip_rib r1\n JOIN bgp_peers p1 ON p1.hash_id = r1.peer_hash_id\n JOIN routers rt1 ON rt1.hash_id = p1.router_hash_id\n WHERE rt1.name = '$rr1' AND r1.iswithdrawn = false\n EXCEPT\n SELECT DISTINCT r2.prefix, r2.prefix_len\n FROM ip_rib r2\n JOIN bgp_peers p2 ON p2.hash_id = r2.peer_hash_id\n JOIN routers rt2 ON rt2.hash_id = p2.router_hash_id\n WHERE rt2.name = '$rr2' AND r2.iswithdrawn = false\n) sub",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Only on $rr1",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Prefixes present on RR2 but missing from RR1",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"fixedColor": "orange","mode": "fixed"},"thresholds": {"mode": "absolute","steps": [{"color": "orange","value": null},{"color": "red","value": 1}]}},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4,"w": 4,"x": 12,"y": 5},
|
||||
"id": 13,
|
||||
"options": {"colorMode": "value","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT COUNT(*) AS \"only_rr2\"\nFROM (\n SELECT DISTINCT r2.prefix, r2.prefix_len\n FROM ip_rib r2\n JOIN bgp_peers p2 ON p2.hash_id = r2.peer_hash_id\n JOIN routers rt2 ON rt2.hash_id = p2.router_hash_id\n WHERE rt2.name = '$rr2' AND r2.iswithdrawn = false\n EXCEPT\n SELECT DISTINCT r1.prefix, r1.prefix_len\n FROM ip_rib r1\n JOIN bgp_peers p1 ON p1.hash_id = r1.peer_hash_id\n JOIN routers rt1 ON rt1.hash_id = p1.router_hash_id\n WHERE rt1.name = '$rr1' AND r1.iswithdrawn = false\n) sub",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Only on $rr2",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Prefixes present on both RRs but with different best-path attributes (next-hop, AS path, MED, or local-pref)",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"fixedColor": "yellow","mode": "fixed"},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 10}]}},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4,"w": 4,"x": 16,"y": 5},
|
||||
"id": 14,
|
||||
"options": {"colorMode": "value","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "WITH c1 AS (\n SELECT DISTINCT ON (r.prefix, r.prefix_len)\n r.prefix, r.prefix_len, ba.next_hop, ba.as_path,\n COALESCE(ba.local_pref, 0) AS lp, COALESCE(ba.med, 0) AS med\n FROM ip_rib r\n JOIN bgp_peers p ON p.hash_id = r.peer_hash_id\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n JOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\n WHERE rt.name = '$rr1' AND r.iswithdrawn = false\n ORDER BY r.prefix, r.prefix_len, ba.local_pref DESC NULLS LAST\n),\nc2 AS (\n SELECT DISTINCT ON (r.prefix, r.prefix_len)\n r.prefix, r.prefix_len, ba.next_hop, ba.as_path,\n COALESCE(ba.local_pref, 0) AS lp, COALESCE(ba.med, 0) AS med\n FROM ip_rib r\n JOIN bgp_peers p ON p.hash_id = r.peer_hash_id\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n JOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\n WHERE rt.name = '$rr2' AND r.iswithdrawn = false\n ORDER BY r.prefix, r.prefix_len, ba.local_pref DESC NULLS LAST\n)\nSELECT COUNT(*) AS \"attr_diffs\"\nFROM c1 JOIN c2 ON c1.prefix = c2.prefix AND c1.prefix_len = c2.prefix_len\nWHERE c1.next_hop != c2.next_hop OR c1.as_path != c2.as_path\n OR c1.lp != c2.lp OR c1.med != c2.med",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Attribute Diffs",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Total RIB entries (including multiple paths per prefix) on each RR",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"fixedColor": "purple","mode": "fixed"},"thresholds": {"mode": "absolute","steps": [{"color": "purple","value": null}]}},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {"h": 4,"w": 4,"x": 20,"y": 5},
|
||||
"id": 15,
|
||||
"options": {"colorMode": "value","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"textMode": "auto"},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT\n SUM(CASE WHEN rt.name = '$rr1' THEN 1 ELSE 0 END) AS \"RR1 Total Paths\",\n SUM(CASE WHEN rt.name = '$rr2' THEN 1 ELSE 0 END) AS \"RR2 Total Paths\"\nFROM ip_rib r\nJOIN bgp_peers p ON p.hash_id = r.peer_hash_id\nJOIN routers rt ON rt.hash_id = p.router_hash_id\nWHERE rt.name IN ('$rr1', '$rr2') AND r.iswithdrawn = false",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Total RIB Paths",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {"h": 1,"w": 24,"x": 0,"y": 9},
|
||||
"id": 20,
|
||||
"title": "Missing Prefixes",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Prefixes present on RR1 but NOT on RR2. Expected: RR2's own locally-originated routes won't appear here (they're on RR2 only). Unexpected entries indicate a convergence or session issue.",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"mode": "thresholds"},"custom": {"align": "auto","displayMode": "auto","filterable": true},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName","options": "Learned From"},"properties": [{"id": "custom.width","value": 130}]},
|
||||
{"matcher": {"id": "byName","options": "Origin AS"},"properties": [{"id": "custom.width","value": 90}]},
|
||||
{"matcher": {"id": "byName","options": "Next Hop"},"properties": [{"id": "custom.width","value": 140}]}
|
||||
]
|
||||
},
|
||||
"gridPos": {"h": 10,"w": 12,"x": 0,"y": 10},
|
||||
"id": 21,
|
||||
"options": {"footer": {"fields": "","reducer": ["count"],"show": true},"showHeader": true,"sortBy": [{"desc": false,"displayName": "Prefix"}]},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT DISTINCT ON (r1.prefix, r1.prefix_len)\n r1.prefix::text AS \"Prefix\",\n ba.origin_as AS \"Origin AS\",\n ba.next_hop::text AS \"Next Hop\",\n ba.as_path::text AS \"AS Path\",\n p1.peer_addr::text AS \"Learned From\"\nFROM ip_rib r1\nJOIN bgp_peers p1 ON p1.hash_id = r1.peer_hash_id\nJOIN routers rt1 ON rt1.hash_id = p1.router_hash_id\nJOIN base_attrs ba ON ba.hash_id = r1.base_attr_hash_id\nWHERE rt1.name = '$rr1' AND r1.iswithdrawn = false\n AND NOT EXISTS (\n SELECT 1 FROM ip_rib r2\n JOIN bgp_peers p2 ON p2.hash_id = r2.peer_hash_id\n JOIN routers rt2 ON rt2.hash_id = p2.router_hash_id\n WHERE rt2.name = '$rr2'\n AND r2.prefix = r1.prefix AND r2.prefix_len = r1.prefix_len\n AND r2.iswithdrawn = false\n )\nORDER BY r1.prefix, r1.prefix_len, ba.local_pref DESC NULLS LAST",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Prefixes Only on $rr1",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Prefixes present on RR2 but NOT on RR1. Expected: RR1's own locally-originated routes won't appear here. Unexpected entries indicate a convergence or session issue.",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"mode": "thresholds"},"custom": {"align": "auto","displayMode": "auto","filterable": true},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName","options": "Learned From"},"properties": [{"id": "custom.width","value": 130}]},
|
||||
{"matcher": {"id": "byName","options": "Origin AS"},"properties": [{"id": "custom.width","value": 90}]},
|
||||
{"matcher": {"id": "byName","options": "Next Hop"},"properties": [{"id": "custom.width","value": 140}]}
|
||||
]
|
||||
},
|
||||
"gridPos": {"h": 10,"w": 12,"x": 12,"y": 10},
|
||||
"id": 22,
|
||||
"options": {"footer": {"fields": "","reducer": ["count"],"show": true},"showHeader": true,"sortBy": [{"desc": false,"displayName": "Prefix"}]},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT DISTINCT ON (r2.prefix, r2.prefix_len)\n r2.prefix::text AS \"Prefix\",\n ba.origin_as AS \"Origin AS\",\n ba.next_hop::text AS \"Next Hop\",\n ba.as_path::text AS \"AS Path\",\n p2.peer_addr::text AS \"Learned From\"\nFROM ip_rib r2\nJOIN bgp_peers p2 ON p2.hash_id = r2.peer_hash_id\nJOIN routers rt2 ON rt2.hash_id = p2.router_hash_id\nJOIN base_attrs ba ON ba.hash_id = r2.base_attr_hash_id\nWHERE rt2.name = '$rr2' AND r2.iswithdrawn = false\n AND NOT EXISTS (\n SELECT 1 FROM ip_rib r1\n JOIN bgp_peers p1 ON p1.hash_id = r1.peer_hash_id\n JOIN routers rt1 ON rt1.hash_id = p1.router_hash_id\n WHERE rt1.name = '$rr1'\n AND r1.prefix = r2.prefix AND r1.prefix_len = r2.prefix_len\n AND r1.iswithdrawn = false\n )\nORDER BY r2.prefix, r2.prefix_len, ba.local_pref DESC NULLS LAST",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Prefixes Only on $rr2",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {"h": 1,"w": 24,"x": 0,"y": 20},
|
||||
"id": 30,
|
||||
"title": "Attribute Differences (Same Prefix, Different Best Path)",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Prefixes present on both RRs but where the selected best path differs in next-hop, AS path, local-pref, or MED. This is normal for multi-homed link subnets where each RR selects a different best path based on router-id tiebreaker. Filter by address family using the AFI dropdown.",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"mode": "thresholds"},"custom": {"align": "auto","displayMode": "auto","filterable": true},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName","options": "RR1 Next Hop"},"properties": [{"id": "custom.displayMode","value": "color-text"},{"id": "color","value": {"mode": "fixed","fixedColor": "blue"}}]},
|
||||
{"matcher": {"id": "byName","options": "RR2 Next Hop"},"properties": [{"id": "custom.displayMode","value": "color-text"},{"id": "color","value": {"mode": "fixed","fixedColor": "green"}}]},
|
||||
{"matcher": {"id": "byName","options": "Diff Type"},"properties": [{"id": "custom.displayMode","value": "color-background"},{"id": "color","value": {"mode": "thresholds"}},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "yellow","value": null}]}}]}
|
||||
]
|
||||
},
|
||||
"gridPos": {"h": 12,"w": 24,"x": 0,"y": 21},
|
||||
"id": 31,
|
||||
"options": {"footer": {"fields": "","reducer": ["count"],"show": true},"showHeader": true,"sortBy": [{"desc": false,"displayName": "Prefix"}]},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "WITH c1 AS (\n SELECT DISTINCT ON (r.prefix, r.prefix_len)\n r.prefix, r.prefix_len, r.isipv4,\n ba.next_hop, ba.as_path, ba.origin_as,\n COALESCE(ba.local_pref, 0) AS lp, COALESCE(ba.med, 0) AS med,\n ba.community_list, p.peer_addr AS learned_from\n FROM ip_rib r\n JOIN bgp_peers p ON p.hash_id = r.peer_hash_id\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n JOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\n WHERE rt.name = '$rr1' AND r.iswithdrawn = false\n ORDER BY r.prefix, r.prefix_len, ba.local_pref DESC NULLS LAST\n),\nc2 AS (\n SELECT DISTINCT ON (r.prefix, r.prefix_len)\n r.prefix, r.prefix_len, r.isipv4,\n ba.next_hop, ba.as_path, ba.origin_as,\n COALESCE(ba.local_pref, 0) AS lp, COALESCE(ba.med, 0) AS med,\n ba.community_list, p.peer_addr AS learned_from\n FROM ip_rib r\n JOIN bgp_peers p ON p.hash_id = r.peer_hash_id\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n JOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\n WHERE rt.name = '$rr2' AND r.iswithdrawn = false\n ORDER BY r.prefix, r.prefix_len, ba.local_pref DESC NULLS LAST\n)\nSELECT\n c1.prefix::text AS \"Prefix\",\n CASE WHEN c1.isipv4 THEN 'IPv4' ELSE 'IPv6' END AS \"AFI\",\n c1.next_hop::text AS \"RR1 Next Hop\",\n c2.next_hop::text AS \"RR2 Next Hop\",\n c1.as_path::text AS \"RR1 AS Path\",\n c2.as_path::text AS \"RR2 AS Path\",\n c1.lp AS \"RR1 LP\",\n c2.lp AS \"RR2 LP\",\n c1.med AS \"RR1 MED\",\n c2.med AS \"RR2 MED\",\n CASE\n WHEN c1.next_hop != c2.next_hop AND c1.as_path != c2.as_path THEN 'NH+ASPath'\n WHEN c1.next_hop != c2.next_hop THEN 'Next-Hop'\n WHEN c1.as_path != c2.as_path THEN 'AS Path'\n WHEN c1.lp != c2.lp THEN 'Local-Pref'\n WHEN c1.med != c2.med THEN 'MED'\n ELSE 'Other'\n END AS \"Diff Type\"\nFROM c1 JOIN c2 ON c1.prefix = c2.prefix AND c1.prefix_len = c2.prefix_len\nWHERE (c1.next_hop != c2.next_hop OR c1.as_path != c2.as_path\n OR c1.lp != c2.lp OR c1.med != c2.med)\n AND ('$afi' = 'All' OR ('$afi' = 'IPv4' AND c1.isipv4 = true) OR ('$afi' = 'IPv6' AND c1.isipv4 = false))\nORDER BY c1.prefix",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Attribute Differences — $rr1 vs $rr2",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {"h": 1,"w": 24,"x": 0,"y": 33},
|
||||
"id": 40,
|
||||
"title": "Per-Client Consistency",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Route counts each RR client sends to each RR. In a healthy cluster, every client should send the same number of routes to both RRs. Mismatches indicate session issues, policy differences, or BMP reporting gaps.",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"mode": "thresholds"},"custom": {"align": "auto","displayMode": "auto","filterable": true},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName","options": "Delta"},"properties": [{"id": "custom.displayMode","value": "color-background"},{"id": "color","value": {"mode": "thresholds"}},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "yellow","value": 1},{"color": "red","value": 5}]}}]},
|
||||
{"matcher": {"id": "byName","options": "Status"},"properties": [{"id": "custom.displayMode","value": "color-background"},{"id": "mappings","value": [{"options": {"Match": {"color": "green","index": 0,"text": "Match"},"MISMATCH": {"color": "red","index": 1,"text": "MISMATCH"}},"type": "value"}]}]}
|
||||
]
|
||||
},
|
||||
"gridPos": {"h": 10,"w": 12,"x": 0,"y": 34},
|
||||
"id": 41,
|
||||
"options": {"footer": {"fields": "","reducer": ["sum"],"show": false},"showHeader": true,"sortBy": [{"desc": true,"displayName": "Delta"}]},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "WITH rr1_peers AS (\n SELECT p.peer_addr, p.hash_id\n FROM bgp_peers p\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n WHERE rt.name = '$rr1'\n),\nrr2_peers AS (\n SELECT p.peer_addr, p.hash_id\n FROM bgp_peers p\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n WHERE rt.name = '$rr2'\n),\nrr1_counts AS (\n SELECT pp.peer_addr, COUNT(*) AS cnt\n FROM rr1_peers pp\n JOIN ip_rib r ON r.peer_hash_id = pp.hash_id AND r.iswithdrawn = false\n GROUP BY pp.peer_addr\n),\nrr2_counts AS (\n SELECT pp.peer_addr, COUNT(*) AS cnt\n FROM rr2_peers pp\n JOIN ip_rib r ON r.peer_hash_id = pp.hash_id AND r.iswithdrawn = false\n GROUP BY pp.peer_addr\n)\nSELECT\n COALESCE(c1.peer_addr, c2.peer_addr)::text AS \"Client\",\n COALESCE(c1.cnt, 0) AS \"Routes to RR1\",\n COALESCE(c2.cnt, 0) AS \"Routes to RR2\",\n ABS(COALESCE(c1.cnt, 0) - COALESCE(c2.cnt, 0)) AS \"Delta\",\n CASE WHEN COALESCE(c1.cnt, 0) = COALESCE(c2.cnt, 0) THEN 'Match' ELSE 'MISMATCH' END AS \"Status\"\nFROM rr1_counts c1\nFULL OUTER JOIN rr2_counts c2 ON c1.peer_addr = c2.peer_addr\nWHERE c1.peer_addr IS NOT NULL AND c2.peer_addr IS NOT NULL\nORDER BY ABS(COALESCE(c1.cnt, 0) - COALESCE(c2.cnt, 0)) DESC, COALESCE(c1.peer_addr, c2.peer_addr)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Per-Client Route Consistency",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "For clients with route count mismatches, shows which specific prefixes differ. Select a client from the Client dropdown to drill down.",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"mode": "thresholds"},"custom": {"align": "auto","displayMode": "auto","filterable": true},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName","options": "Present On"},"properties": [{"id": "custom.displayMode","value": "color-text"},{"id": "color","value": {"mode": "thresholds"}},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "blue","value": null}]}}]}
|
||||
]
|
||||
},
|
||||
"gridPos": {"h": 10,"w": 12,"x": 12,"y": 34},
|
||||
"id": 42,
|
||||
"options": {"footer": {"fields": "","reducer": ["count"],"show": true},"showHeader": true,"sortBy": [{"desc": false,"displayName": "Prefix"}]},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "WITH rr1_client_routes AS (\n SELECT r.prefix, r.prefix_len\n FROM ip_rib r\n JOIN bgp_peers p ON p.hash_id = r.peer_hash_id\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n WHERE rt.name = '$rr1' AND p.peer_addr::text = '$client'\n AND r.iswithdrawn = false\n),\nrr2_client_routes AS (\n SELECT r.prefix, r.prefix_len\n FROM ip_rib r\n JOIN bgp_peers p ON p.hash_id = r.peer_hash_id\n JOIN routers rt ON rt.hash_id = p.router_hash_id\n WHERE rt.name = '$rr2' AND p.peer_addr::text = '$client'\n AND r.iswithdrawn = false\n)\nSELECT r1.prefix::text AS \"Prefix\", 'RR1 only' AS \"Present On\"\nFROM rr1_client_routes r1\nWHERE NOT EXISTS (\n SELECT 1 FROM rr2_client_routes r2\n WHERE r2.prefix = r1.prefix AND r2.prefix_len = r1.prefix_len\n)\nUNION ALL\nSELECT r2.prefix::text AS \"Prefix\", 'RR2 only' AS \"Present On\"\nFROM rr2_client_routes r2\nWHERE NOT EXISTS (\n SELECT 1 FROM rr1_client_routes r1\n WHERE r1.prefix = r2.prefix AND r1.prefix_len = r2.prefix_len\n)\nORDER BY \"Prefix\"",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Client Prefix Diff — $client",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {"h": 1,"w": 24,"x": 0,"y": 44},
|
||||
"id": 50,
|
||||
"title": "Full RIB Comparison (All Paths)",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"description": "Side-by-side view of all paths for a specific prefix on both RRs. Select a prefix from the Prefix dropdown to drill down.",
|
||||
"fieldConfig": {
|
||||
"defaults": {"color": {"mode": "thresholds"},"custom": {"align": "auto","displayMode": "auto","filterable": true},"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null}]}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName","options": "Router"},"properties": [{"id": "custom.displayMode","value": "color-text"},{"id": "color","value": {"mode": "thresholds"}},{"id": "thresholds","value": {"mode": "absolute","steps": [{"color": "blue","value": null}]}}]}
|
||||
]
|
||||
},
|
||||
"gridPos": {"h": 10,"w": 24,"x": 0,"y": 45},
|
||||
"id": 51,
|
||||
"options": {"footer": {"fields": "","reducer": ["count"],"show": true},"showHeader": true,"sortBy": [{"desc": false,"displayName": "Router"}]},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"format": "table",
|
||||
"rawSql": "SELECT\n rt.name AS \"Router\",\n p.peer_addr::text AS \"Learned From\",\n ba.next_hop::text AS \"Next Hop\",\n ba.as_path::text AS \"AS Path\",\n ba.origin_as AS \"Origin AS\",\n COALESCE(ba.local_pref, 0) AS \"Local Pref\",\n COALESCE(ba.med, 0) AS \"MED\",\n ba.community_list::text AS \"Communities\",\n ba.cluster_list::text AS \"Cluster List\",\n ba.originator_id::text AS \"Originator ID\",\n r.labels AS \"Labels\",\n r.timestamp AS \"Last Update\"\nFROM ip_rib r\nJOIN bgp_peers p ON p.hash_id = r.peer_hash_id\nJOIN routers rt ON rt.hash_id = p.router_hash_id\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nWHERE rt.name IN ('$rr1', '$rr2')\n AND r.iswithdrawn = false\n AND r.prefix::text = '$prefix'\nORDER BY rt.name, p.peer_addr",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "All Paths for $prefix",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 38,
|
||||
"tags": ["openbmp", "rr-diff", "bgp"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"definition": "SELECT name FROM routers WHERE state = 'up' ORDER BY name",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "RR1",
|
||||
"multi": false,
|
||||
"name": "rr1",
|
||||
"options": [],
|
||||
"query": "SELECT name FROM routers WHERE state = 'up' ORDER BY name",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"definition": "SELECT name FROM routers WHERE state = 'up' ORDER BY name",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "RR2",
|
||||
"multi": false,
|
||||
"name": "rr2",
|
||||
"options": [],
|
||||
"query": "SELECT name FROM routers WHERE state = 'up' ORDER BY name",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {"selected": true,"text": "All","value": "All"},
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "AFI",
|
||||
"multi": false,
|
||||
"name": "afi",
|
||||
"options": [
|
||||
{"selected": true,"text": "All","value": "All"},
|
||||
{"selected": false,"text": "IPv4","value": "IPv4"},
|
||||
{"selected": false,"text": "IPv6","value": "IPv6"}
|
||||
],
|
||||
"query": "All,IPv4,IPv6",
|
||||
"skipUrlSync": false,
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"definition": "SELECT DISTINCT p.peer_addr::text FROM bgp_peers p JOIN routers rt ON rt.hash_id = p.router_hash_id WHERE rt.name IN ('$rr1', '$rr2') AND p.state = 'up' ORDER BY 1",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Client",
|
||||
"multi": false,
|
||||
"name": "client",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT p.peer_addr::text FROM bgp_peers p JOIN routers rt ON rt.hash_id = p.router_hash_id WHERE rt.name IN ('$rr1', '$rr2') AND p.state = 'up' ORDER BY 1",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||||
"definition": "SELECT DISTINCT r.prefix::text FROM ip_rib r JOIN bgp_peers p ON p.hash_id = r.peer_hash_id JOIN routers rt ON rt.hash_id = p.router_hash_id WHERE rt.name IN ('$rr1', '$rr2') AND r.iswithdrawn = false ORDER BY 1",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Prefix",
|
||||
"multi": false,
|
||||
"name": "prefix",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT r.prefix::text FROM ip_rib r JOIN bgp_peers p ON p.hash_id = r.peer_hash_id JOIN routers rt ON rt.hash_id = p.router_hash_id WHERE rt.name IN ('$rr1', '$rr2') AND r.iswithdrawn = false ORDER BY 1",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {"from": "now-6h","to": "now"},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "RR Loc-RIB Diff",
|
||||
"uid": "rr-locrib-diff",
|
||||
"version": 1
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user