mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 19:20:46 +08:00
Add BGP as <local_as> binding for multi-process ZTE collects.
Stamp local_as on peer/route/evpn results and discover (local_as, neighbor/vrf) pairs from config_bgp_peer so each router bgp process is collected separately. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b453c6871e
commit
262082ef08
10 changed files with 690 additions and 318 deletions
|
|
@ -25,8 +25,20 @@ class DiscoverCacheTests(unittest.TestCase):
|
|||
self.assertEqual(v6.placeholders[0].discover_profile_id, "zte.config_bgp_peer")
|
||||
|
||||
records = [
|
||||
{"afi": "vpnv4", "vrf": "", "neighbor": "10.0.0.1", "remote_as": "65001"},
|
||||
{"afi": "vpnv6", "vrf": "", "neighbor": "FC00::1", "remote_as": "65002"},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "vpnv4",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.1",
|
||||
"remote_as": "65001",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "vpnv6",
|
||||
"vrf": "",
|
||||
"neighbor": "FC00::1",
|
||||
"remote_as": "65002",
|
||||
},
|
||||
]
|
||||
db = MagicMock()
|
||||
|
||||
|
|
@ -53,7 +65,12 @@ class DiscoverCacheTests(unittest.TestCase):
|
|||
)
|
||||
self.assertTrue(first["ok"])
|
||||
self.assertFalse(first.get("cache_hit"))
|
||||
self.assertEqual([c["value"] for c in first["candidates"]], ["10.0.0.1"])
|
||||
self.assertTrue(first.get("pair_mode"))
|
||||
self.assertEqual(len(first["candidates"]), 1)
|
||||
self.assertEqual(
|
||||
first["candidates"][0]["bindings"],
|
||||
{"neighbor": "10.0.0.1", "local_as": "64900"},
|
||||
)
|
||||
self.assertEqual(open_conn.call_count, 1)
|
||||
self.assertEqual(send_cmd.call_count, 1)
|
||||
self.assertEqual(run_parser.call_count, 1)
|
||||
|
|
@ -67,7 +84,11 @@ class DiscoverCacheTests(unittest.TestCase):
|
|||
)
|
||||
self.assertTrue(second["ok"])
|
||||
self.assertTrue(second.get("cache_hit"))
|
||||
self.assertEqual([c["value"] for c in second["candidates"]], ["FC00::1"])
|
||||
self.assertEqual(len(second["candidates"]), 1)
|
||||
self.assertEqual(
|
||||
second["candidates"][0]["bindings"],
|
||||
{"neighbor": "FC00::1", "local_as": "64900"},
|
||||
)
|
||||
# Same discover profile → no second SSH/parse
|
||||
self.assertEqual(open_conn.call_count, 1)
|
||||
self.assertEqual(send_cmd.call_count, 1)
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
|||
cmds = plan["commands"]
|
||||
self.assertTrue(any("lldp" in c.lower() for c in cmds))
|
||||
self.assertTrue(any("CUST_A" in c and "summary" in c for c in cmds))
|
||||
# Default export includes discover-backed aux (config_vrf), not FIB
|
||||
self.assertTrue(any("running-config vrf" in c for c in cmds))
|
||||
# Default export includes discover-backed aux (config_bgp_peer)
|
||||
self.assertTrue(any("running-config bgp" in c for c in cmds))
|
||||
self.assertFalse(any("forwarding route" in c for c in cmds))
|
||||
# Disabled item excluded by default
|
||||
self.assertFalse(any("isis" in c.lower() for c in cmds))
|
||||
|
|
@ -91,14 +91,14 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
|||
primary_only = plan_task_collect_commands(
|
||||
self.db, self.task.id, include_aux=False
|
||||
)
|
||||
self.assertFalse(any("running-config vrf" in c for c in primary_only["commands"]))
|
||||
self.assertFalse(any("running-config bgp" in c for c in primary_only["commands"]))
|
||||
|
||||
text = export_task_commands_text(self.db, self.task.id)
|
||||
self.assertIn("task_id=t_export", text)
|
||||
self.assertIn("show lldp neighbor brief", text)
|
||||
self.assertIn("CUST_A", text)
|
||||
self.assertIn("# aux:", text)
|
||||
self.assertIn("running-config vrf", text)
|
||||
self.assertIn("running-config bgp", text)
|
||||
self.assertIn("# ---- flat unique commands ----", text)
|
||||
|
||||
def test_shared_aux_repeated_per_item_deduped_in_flat(self) -> None:
|
||||
|
|
@ -157,7 +157,7 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
|||
flat = text.split("# ---- flat unique commands ----", 1)[-1]
|
||||
self.assertEqual(flat.count(aux_cli), 1)
|
||||
|
||||
def test_unbound_required_placeholder_noted(self) -> None:
|
||||
def test_unbound_optional_placeholder_expand_all(self) -> None:
|
||||
item = BizStateTaskItem(
|
||||
id="i_exp",
|
||||
task_id=self.task.id,
|
||||
|
|
@ -173,24 +173,25 @@ class BizStateExportCommandsTests(unittest.TestCase):
|
|||
plan = plan_task_collect_commands(self.db, self.task.id)
|
||||
sec = plan["items"][0]
|
||||
notes = " ".join(sec.get("notes") or [])
|
||||
self.assertIn("requires parameter bindings", notes)
|
||||
# Primary is template-only; aux (config_vrf) still counts as executable
|
||||
self.assertIn("expand_all", notes)
|
||||
# Primary is template-only; aux (config_bgp_peer) still counts as executable
|
||||
self.assertEqual(plan["command_count"], 1)
|
||||
self.assertTrue(any("running-config vrf" in c for c in plan["commands"]))
|
||||
self.assertTrue(any("running-config bgp" in c for c in plan["commands"]))
|
||||
tmpl_cmds = [c for c in sec.get("commands") or [] if c.get("role") == "template"]
|
||||
self.assertEqual(len(tmpl_cmds), 1)
|
||||
self.assertIn("<vrf>", tmpl_cmds[0]["command"])
|
||||
self.assertIn("<local_as>", tmpl_cmds[0]["command"])
|
||||
aux_cmds = [c for c in sec.get("commands") or [] if c.get("role") == "aux"]
|
||||
self.assertEqual(len(aux_cmds), 1)
|
||||
|
||||
text = export_task_commands_text(self.db, self.task.id)
|
||||
self.assertIn("# show bgp vpnv4 unicast vrf <vrf> summary", text)
|
||||
self.assertIn("# show bgp vpnv4 unicast vrf <vrf> summary as <local_as>", text)
|
||||
self.assertIn("# aux:", text)
|
||||
self.assertIn("running-config vrf", text)
|
||||
self.assertIn("running-config bgp", text)
|
||||
# Template must not appear in the flat executable list
|
||||
flat = text.split("# ---- flat unique commands ----", 1)[-1]
|
||||
self.assertNotIn("<vrf>", flat)
|
||||
self.assertIn("running-config vrf", flat)
|
||||
self.assertIn("running-config bgp", flat)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -92,6 +92,74 @@ $
|
|||
!</bgp>
|
||||
"""
|
||||
|
||||
# Multi local-AS (trimmed from MER1 show running-config bgp)
|
||||
_CFG_BGP_MULTI_AS = """
|
||||
!<bgp>
|
||||
bgp as-notation plain
|
||||
router bgp 64900
|
||||
neighbor 2c0f:f7c0:0:500::50:2 remote-as 64900
|
||||
neighbor 2c0f:f7c0:0:500::50:2 update-source loopback130
|
||||
neighbor 172.16.0.131 remote-as 64900
|
||||
neighbor 172.16.0.131 update-source loopback130
|
||||
neighbor 10.229.147.93 remote-as 64900
|
||||
neighbor 10.229.147.93 update-source loopback130
|
||||
address-family link-state
|
||||
neighbor 10.229.147.93 activate
|
||||
neighbor 10.229.147.93 route-reflector-client
|
||||
$
|
||||
address-family vpnv4
|
||||
neighbor 172.16.0.131 activate
|
||||
neighbor 172.16.0.131 route-reflector-client
|
||||
$
|
||||
address-family ipv6
|
||||
neighbor 2c0f:f7c0:0:500::50:2 activate
|
||||
$
|
||||
address-family ipv6 sr-policy
|
||||
neighbor 10.229.147.93 activate
|
||||
$
|
||||
address-family vpnv6
|
||||
neighbor 2c0f:f7c0:0:500::50:2 activate
|
||||
$
|
||||
$
|
||||
router bgp 64600
|
||||
neighbor 444::2 remote-as 64600
|
||||
neighbor 444::2 activate disable
|
||||
neighbor 444::2 update-source loopback400
|
||||
address-family l2vpn evpn
|
||||
neighbor 444::2 activate
|
||||
neighbor 444::2 announce-sid
|
||||
$
|
||||
$
|
||||
router bgp 64580
|
||||
neighbor 22:22:22::22 remote-as 64580
|
||||
neighbor 22:22:22::22 activate
|
||||
neighbor 22:22:22::22 update-source loopback0
|
||||
$
|
||||
router bgp 100
|
||||
neighbor MAR_GROUP_V6_1 peer-group
|
||||
neighbor MAR_GROUP_V6_1 remote-as 100
|
||||
neighbor MAR_GROUP_V6_1 activate disable
|
||||
neighbor MAR_GROUP_V6_1 update-source loopback1001
|
||||
neighbor 2408:8121:8400:1:1000::4:0 remote-as 100
|
||||
neighbor 2408:8121:8400:1:1000::4:0 peer-group MAR_GROUP_V6_1
|
||||
neighbor 2408:8121:8400:1:1000::4:0 activate disable
|
||||
neighbor 100.0.0.2 remote-as 100
|
||||
neighbor 100.0.0.2 activate
|
||||
address-family l2vpn evpn
|
||||
neighbor MAR_GROUP_V6_1 activate
|
||||
neighbor MAR_GROUP_V6_1 route-map TO_MAR_EVPN_SRV6_GROUP_1 out
|
||||
$
|
||||
address-family vpnv4
|
||||
neighbor 100.0.0.2 activate
|
||||
$
|
||||
address-family ipv6 vrf SRv6
|
||||
neighbor 2000::1 remote-as 100
|
||||
neighbor 2000::1 activate
|
||||
$
|
||||
$
|
||||
!</bgp>
|
||||
"""
|
||||
|
||||
_CFG_L2VPN = """
|
||||
!<l2vpn>
|
||||
vpws VPN_A
|
||||
|
|
@ -289,36 +357,116 @@ class ZteConfigIntentTests(unittest.TestCase):
|
|||
self.assertNotIn("SKIPME", blob)
|
||||
self.assertNotIn("password", blob.lower())
|
||||
|
||||
by = {(r["afi"], r["vrf"], r["neighbor"], r["peer_group"]): r for r in rows}
|
||||
self.assertIn(("vpnv4", "", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.1", "CORE_RR")]["remote_as"], "65001")
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.1", "CORE_RR")]["activate"], "enable")
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.1", "CORE_RR")]["update_source"], "loopback1")
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.1", "CORE_RR")]["route_map_in"], "RM_IN")
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.1", "CORE_RR")]["route_map_out"], "RM_OUT")
|
||||
by = {
|
||||
(r["local_as"], r["afi"], r["vrf"], r["neighbor"], r["peer_group"]): r
|
||||
for r in rows
|
||||
}
|
||||
self.assertIn(("65000", "vpnv4", "", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.1", "CORE_RR")]["remote_as"], "65001")
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.1", "CORE_RR")]["activate"], "enable")
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.1", "CORE_RR")]["update_source"], "loopback1")
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.1", "CORE_RR")]["route_map_in"], "RM_IN")
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.1", "CORE_RR")]["route_map_out"], "RM_OUT")
|
||||
# AF-scoped RM must not bleed into other address-families
|
||||
self.assertEqual(by[("l2vpn-evpn", "", "10.0.0.1", "CORE_RR")]["route_map_in"], "RM_IN")
|
||||
self.assertEqual(by[("l2vpn-evpn", "", "10.0.0.1", "CORE_RR")]["route_map_out"], "")
|
||||
self.assertEqual(by[("65000", "l2vpn-evpn", "", "10.0.0.1", "CORE_RR")]["route_map_in"], "RM_IN")
|
||||
self.assertEqual(by[("65000", "l2vpn-evpn", "", "10.0.0.1", "CORE_RR")]["route_map_out"], "")
|
||||
# peer-group name is not an IP → neighbor empty, peer_group set
|
||||
self.assertIn(("vpnv4", "", "", "CORE_RR"), by)
|
||||
self.assertEqual(by[("vpnv4", "", "", "CORE_RR")]["remote_as"], "65009")
|
||||
self.assertIn(("65000", "vpnv4", "", "", "CORE_RR"), by)
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "", "CORE_RR")]["remote_as"], "65009")
|
||||
# Global AF expands peer-group members (10.0.0.9 only via group activate)
|
||||
self.assertIn(("vpnv4", "", "10.0.0.9", "CORE_RR"), by)
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.9", "CORE_RR")]["remote_as"], "65019")
|
||||
self.assertEqual(by[("vpnv4", "", "10.0.0.9", "CORE_RR")]["activate"], "enable")
|
||||
self.assertEqual(by[("ipv4", "CUST_A", "10.0.0.2", "")]["remote_as"], "65003")
|
||||
self.assertIn(("65000", "vpnv4", "", "10.0.0.9", "CORE_RR"), by)
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.9", "CORE_RR")]["remote_as"], "65019")
|
||||
self.assertEqual(by[("65000", "vpnv4", "", "10.0.0.9", "CORE_RR")]["activate"], "enable")
|
||||
self.assertEqual(by[("65000", "ipv4", "CUST_A", "10.0.0.2", "")]["remote_as"], "65003")
|
||||
# Global ipv4 AF: same peer-group expand as vpnv4 (not VRF)
|
||||
self.assertIn(("ipv4", "", "", "CORE_RR"), by)
|
||||
self.assertIn(("ipv4", "", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertIn(("ipv4", "", "10.0.0.9", "CORE_RR"), by)
|
||||
self.assertIn(("65000", "ipv4", "", "", "CORE_RR"), by)
|
||||
self.assertIn(("65000", "ipv4", "", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertIn(("65000", "ipv4", "", "10.0.0.9", "CORE_RR"), by)
|
||||
# VRF must not pick up global peer-group members
|
||||
self.assertNotIn(("ipv4", "CUST_A", "10.0.0.9", "CORE_RR"), by)
|
||||
self.assertNotIn(("ipv4", "CUST_A", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertIn(("l2vpn-evpn", "", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertNotIn(("65000", "ipv4", "CUST_A", "10.0.0.9", "CORE_RR"), by)
|
||||
self.assertNotIn(("65000", "ipv4", "CUST_A", "10.0.0.1", "CORE_RR"), by)
|
||||
self.assertIn(("65000", "l2vpn-evpn", "", "10.0.0.1", "CORE_RR"), by)
|
||||
# global IPv6 neighbor without AF activate
|
||||
self.assertIn(("global", "", "FC00:1::1", ""), by)
|
||||
self.assertIn(("65000", "global", "", "FC00:1::1", ""), by)
|
||||
# no name left in neighbor column
|
||||
self.assertTrue(all((not r["neighbor"]) or _is_ip_neighbor(r["neighbor"]) for r in rows))
|
||||
self.assertTrue(all(r["local_as"] == "65000" for r in rows))
|
||||
|
||||
def test_config_bgp_peer_multi_local_as(self) -> None:
|
||||
rows = normalize_config_bgp_peer(
|
||||
raw_text=_CFG_BGP_MULTI_AS, command="show running-config bgp"
|
||||
)
|
||||
local_ases = {r["local_as"] for r in rows}
|
||||
self.assertEqual(local_ases, {"64900", "64600", "64580", "100"})
|
||||
|
||||
by = {
|
||||
(r["local_as"], r["afi"], r["vrf"], r["neighbor"], r["peer_group"]): r
|
||||
for r in rows
|
||||
}
|
||||
# AS 64900 AF activates
|
||||
self.assertEqual(
|
||||
by[("64900", "vpnv4", "", "172.16.0.131", "")]["remote_as"], "64900"
|
||||
)
|
||||
self.assertEqual(
|
||||
by[("64900", "vpnv4", "", "172.16.0.131", "")]["update_source"],
|
||||
"loopback130",
|
||||
)
|
||||
self.assertEqual(
|
||||
by[("64900", "ipv6", "", "2c0f:f7c0:0:500::50:2", "")]["activate"], "enable"
|
||||
)
|
||||
self.assertEqual(
|
||||
by[("64900", "vpnv6", "", "2c0f:f7c0:0:500::50:2", "")]["activate"], "enable"
|
||||
)
|
||||
self.assertIn(("64900", "link-state", "", "10.229.147.93", ""), by)
|
||||
# Multi-word AF must not collapse into plain ipv6
|
||||
self.assertIn(("64900", "ipv6-sr-policy", "", "10.229.147.93", ""), by)
|
||||
self.assertNotEqual(
|
||||
by[("64900", "ipv6-sr-policy", "", "10.229.147.93", "")]["afi"],
|
||||
"ipv6",
|
||||
)
|
||||
# VRF CE under AS 100 (not 64900)
|
||||
self.assertEqual(
|
||||
by[("100", "ipv6", "SRv6", "2000::1", "")]["remote_as"], "100"
|
||||
)
|
||||
self.assertNotIn(("64900", "ipv6", "SRv6", "2000::1", ""), by)
|
||||
# AS 64600 EVPN peer (not mixed into 64900)
|
||||
self.assertEqual(
|
||||
by[("64600", "l2vpn-evpn", "", "444::2", "")]["remote_as"], "64600"
|
||||
)
|
||||
self.assertEqual(
|
||||
by[("64600", "l2vpn-evpn", "", "444::2", "")]["update_source"],
|
||||
"loopback400",
|
||||
)
|
||||
self.assertNotIn(("64900", "l2vpn-evpn", "", "444::2", ""), by)
|
||||
# AS 64580: top-level activate only → global row
|
||||
self.assertIn(("64580", "global", "", "22:22:22::22", ""), by)
|
||||
self.assertEqual(
|
||||
by[("64580", "global", "", "22:22:22::22", "")]["update_source"],
|
||||
"loopback0",
|
||||
)
|
||||
# AS 100: peer-group expand on EVPN
|
||||
self.assertIn(("100", "l2vpn-evpn", "", "", "MAR_GROUP_V6_1"), by)
|
||||
self.assertIn(
|
||||
("100", "l2vpn-evpn", "", "2408:8121:8400:1:1000::4:0", "MAR_GROUP_V6_1"),
|
||||
by,
|
||||
)
|
||||
self.assertEqual(
|
||||
by[
|
||||
("100", "l2vpn-evpn", "", "2408:8121:8400:1:1000::4:0", "MAR_GROUP_V6_1")
|
||||
]["route_map_out"],
|
||||
"TO_MAR_EVPN_SRV6_GROUP_1",
|
||||
)
|
||||
self.assertEqual(
|
||||
by[("100", "vpnv4", "", "100.0.0.2", "")]["remote_as"], "100"
|
||||
)
|
||||
# Peer-group members must not leak across local AS
|
||||
self.assertNotIn(
|
||||
("64900", "l2vpn-evpn", "", "2408:8121:8400:1:1000::4:0", "MAR_GROUP_V6_1"),
|
||||
by,
|
||||
)
|
||||
self.assertTrue(
|
||||
all((not r["neighbor"]) or _is_ip_neighbor(r["neighbor"]) for r in rows)
|
||||
)
|
||||
|
||||
def test_config_l2vpn_pw(self) -> None:
|
||||
rows = normalize_config_l2vpn_pw(
|
||||
|
|
|
|||
|
|
@ -233,27 +233,33 @@ class ZteExtendedParserTests(unittest.TestCase):
|
|||
|
||||
def test_bgp_peer_ipv4_and_ipv6_wrap(self) -> None:
|
||||
v4 = normalize_bgp_peer(
|
||||
raw_text=_BGP_V4_SUMMARY, command="show bgp vpnv4 unicast summary"
|
||||
raw_text=_BGP_V4_SUMMARY,
|
||||
command="show bgp vpnv4 unicast summary as 64900 | one-line",
|
||||
params={"local_as": "64900"},
|
||||
)
|
||||
self.assertEqual(len(v4), 2)
|
||||
self.assertEqual(v4[0]["afi"], "vpnv4")
|
||||
self.assertEqual(v4[0]["local_as"], "64900")
|
||||
self.assertEqual(v4[0]["state"], "Established")
|
||||
self.assertEqual(v4[0]["pfx_rcd"], "5")
|
||||
self.assertEqual(v4[1]["state"], "Connect")
|
||||
|
||||
v6 = normalize_bgp_peer(
|
||||
raw_text=_BGP_V6_SUMMARY, command="show bgp vpnv6 unicast summary"
|
||||
raw_text=_BGP_V6_SUMMARY,
|
||||
command="show bgp vpnv6 unicast summary as 64900",
|
||||
)
|
||||
self.assertEqual(len(v6), 2)
|
||||
self.assertTrue(all(r["afi"] == "vpnv6" for r in v6))
|
||||
self.assertTrue(all(r["local_as"] == "64900" for r in v6))
|
||||
self.assertEqual(v6[0]["neighbor"].upper(), "FC00:1::1")
|
||||
|
||||
vrf = normalize_bgp_peer(
|
||||
raw_text=_BGP_V4_SUMMARY,
|
||||
command="show bgp vpnv4 unicast vrf CUST_A summary",
|
||||
params={"vrf": "CUST_A"},
|
||||
command="show bgp vpnv4 unicast vrf CUST_A summary as 65000",
|
||||
params={"vrf": "CUST_A", "local_as": "65000"},
|
||||
)
|
||||
self.assertTrue(all(r["vrf"] == "CUST_A" for r in vrf))
|
||||
self.assertTrue(all(r["local_as"] == "65000" for r in vrf))
|
||||
|
||||
# Real wrapped vpnv6 summary + IPv6 neighbor in/out routes
|
||||
sample_dir = Path(__file__).resolve().parents[2] / "test" / "show-zte"
|
||||
|
|
@ -425,12 +431,14 @@ class ZteExtendedParserTests(unittest.TestCase):
|
|||
intent = normalize_config_bgp_peer(
|
||||
raw_text="""
|
||||
!<bgp>
|
||||
router bgp 65000
|
||||
neighbor 10.0.0.1 remote-as 65001
|
||||
address-family ipv4 vrf CUST_A
|
||||
neighbor 10.0.0.1 activate
|
||||
neighbor 10.0.0.1 route-map RM_IN in
|
||||
$
|
||||
$
|
||||
!</bgp>
|
||||
""",
|
||||
command="show running-config bgp",
|
||||
)
|
||||
|
|
@ -458,6 +466,7 @@ $
|
|||
intent2 = normalize_config_bgp_peer(
|
||||
raw_text="""
|
||||
!<bgp>
|
||||
router bgp 65000
|
||||
neighbor 10.0.0.1 remote-as 65001
|
||||
address-family ipv4 vrf CUST_A
|
||||
neighbor 10.0.0.1 activate
|
||||
|
|
@ -468,6 +477,7 @@ $
|
|||
neighbor 10.0.0.1 route-map RM_B in
|
||||
$
|
||||
$
|
||||
!</bgp>
|
||||
""",
|
||||
command="show running-config bgp",
|
||||
)
|
||||
|
|
@ -493,6 +503,7 @@ $
|
|||
intent3 = normalize_config_bgp_peer(
|
||||
raw_text="""
|
||||
!<bgp>
|
||||
router bgp 65000
|
||||
neighbor 10.0.0.1 remote-as 65001
|
||||
address-family ipv4
|
||||
neighbor 10.0.0.1 activate
|
||||
|
|
@ -503,6 +514,7 @@ $
|
|||
neighbor 10.0.0.1 route-map RM_VPNV4 in
|
||||
$
|
||||
$
|
||||
!</bgp>
|
||||
""",
|
||||
command="show running-config bgp",
|
||||
)
|
||||
|
|
@ -688,28 +700,32 @@ $
|
|||
self.assertEqual(p.aux_commands[0].profile_id, "zte.config_bgp_peer")
|
||||
pairs = expand_from_bindings(
|
||||
profile=p,
|
||||
bindings=[{"vrf": "CUST_A", "neighbor": "10.0.0.1"}],
|
||||
bindings=[{"vrf": "CUST_A", "neighbor": "10.0.0.1", "local_as": "64900"}],
|
||||
)
|
||||
self.assertEqual(len(pairs), 1)
|
||||
cmd, params = pairs[0]
|
||||
self.assertIn("vrf CUST_A", cmd)
|
||||
self.assertIn("10.0.0.1", cmd)
|
||||
self.assertIn("as 64900", cmd)
|
||||
hit = match_command(vendor_key="zte", command=cmd)
|
||||
self.assertIsNotNone(hit)
|
||||
assert hit is not None
|
||||
self.assertEqual(hit.params.get("vrf"), "CUST_A")
|
||||
self.assertEqual(hit.params.get("neighbor"), "10.0.0.1")
|
||||
self.assertEqual(hit.params.get("local_as"), "64900")
|
||||
|
||||
# BGP VRF summary / Forwarding VRF: required bind + config_vrf aux
|
||||
from netx_api.biz_state.command_match import expand_bindings_from_discover_records
|
||||
# Forwarding VRF: required bind + config_vrf aux
|
||||
# BGP VRF summary: optional (local_as, vrf) from config_bgp_peer
|
||||
from netx_api.biz_state.command_match import (
|
||||
EXPAND_ALL_COMMAND,
|
||||
expand_bindings_from_discover_records,
|
||||
filter_discover_records,
|
||||
normalize_binding_dicts,
|
||||
shared_discover_placeholders,
|
||||
)
|
||||
from netx_api.biz_state.collect_session import resolve_aux_command
|
||||
|
||||
for pid in (
|
||||
"zte.bgp_vpnv4_vrf_summary",
|
||||
"zte.bgp_vpnv6_vrf_summary",
|
||||
"zte.ip_route_vrf",
|
||||
"zte.ipv6_route_vrf",
|
||||
):
|
||||
for pid in ("zte.ip_route_vrf", "zte.ipv6_route_vrf"):
|
||||
prof = get_profile(pid)
|
||||
assert prof is not None
|
||||
self.assertTrue(prof.placeholders)
|
||||
|
|
@ -719,43 +735,76 @@ $
|
|||
|
||||
v4 = get_profile("zte.bgp_vpnv4_vrf_summary")
|
||||
assert v4 is not None
|
||||
self.assertEqual([a.key for a in v4.aux_commands], ["config_vrf"])
|
||||
with self.assertRaises(ValueError):
|
||||
expand_from_bindings(profile=v4, bindings=[])
|
||||
bound = expand_from_bindings(profile=v4, bindings=[{"vrf": "CUST_A"}])
|
||||
self.assertEqual(bound[0][0], "show bgp vpnv4 unicast vrf CUST_A summary | one-line")
|
||||
ra_cfg = resolve_aux_command(
|
||||
next(a for a in v4.aux_commands if a.key == "config_vrf"),
|
||||
params={"vrf": "CUST_A"},
|
||||
self.assertEqual([a.key for a in v4.aux_commands], ["config_bgp_peer"])
|
||||
self.assertFalse(any(ph.required for ph in v4.placeholders))
|
||||
expand_all = expand_from_bindings(profile=v4, bindings=[])
|
||||
self.assertEqual(expand_all[0][0], EXPAND_ALL_COMMAND)
|
||||
bound = expand_from_bindings(
|
||||
profile=v4, bindings=[{"vrf": "CUST_A", "local_as": "64900"}]
|
||||
)
|
||||
self.assertEqual(ra_cfg.command, "show running-config vrf | one-line")
|
||||
self.assertEqual(
|
||||
bound[0][0],
|
||||
"show bgp vpnv4 unicast vrf CUST_A summary as 64900 | one-line",
|
||||
)
|
||||
# Optional local_as omitted → strip `` as <local_as>``
|
||||
bound_no_as = expand_from_bindings(profile=v4, bindings=[{"vrf": "CUST_A"}])
|
||||
self.assertEqual(
|
||||
bound_no_as[0][0],
|
||||
"show bgp vpnv4 unicast vrf CUST_A summary | one-line",
|
||||
)
|
||||
ra_cfg = resolve_aux_command(
|
||||
next(a for a in v4.aux_commands if a.key == "config_bgp_peer"),
|
||||
params={"vrf": "CUST_A", "local_as": "64900"},
|
||||
)
|
||||
self.assertEqual(ra_cfg.command, "show running-config bgp | one-line")
|
||||
|
||||
v6 = get_profile("zte.bgp_vpnv6_vrf_summary")
|
||||
assert v6 is not None
|
||||
self.assertEqual([a.key for a in v6.aux_commands], ["config_vrf"])
|
||||
self.assertEqual([a.key for a in v6.aux_commands], ["config_bgp_peer"])
|
||||
|
||||
records = [
|
||||
peer_vrf_recs = [
|
||||
{"local_as": "64900", "afi": "ipv4", "vrf": "CUST_A", "neighbor": "10.0.0.2"},
|
||||
{"local_as": "64900", "afi": "ipv4", "vrf": "CUST_B", "neighbor": "10.0.0.3"},
|
||||
{"local_as": "64900", "afi": "ipv6", "vrf": "CUST_A", "neighbor": "FC00::2"},
|
||||
{"local_as": "64900", "afi": "ipv6", "vrf": "CUST_C", "neighbor": "FC00::3"},
|
||||
{"local_as": "65000", "afi": "ipv4", "vrf": "CUST_A", "neighbor": "10.0.0.9"},
|
||||
]
|
||||
ipv4_pairs = expand_bindings_from_discover_records(
|
||||
profile=v4, records=peer_vrf_recs
|
||||
)
|
||||
self.assertEqual(
|
||||
{(p[1]["local_as"], p[1]["vrf"]) for p in ipv4_pairs},
|
||||
{("64900", "CUST_A"), ("64900", "CUST_B"), ("65000", "CUST_A")},
|
||||
)
|
||||
ipv6_pairs = expand_bindings_from_discover_records(
|
||||
profile=v6, records=peer_vrf_recs
|
||||
)
|
||||
self.assertEqual(
|
||||
{(p[1]["local_as"], p[1]["vrf"]) for p in ipv6_pairs},
|
||||
{("64900", "CUST_A"), ("64900", "CUST_C")},
|
||||
)
|
||||
|
||||
# FIB VRF still expands from config_vrf records
|
||||
ip_vrf = get_profile("zte.ip_route_vrf")
|
||||
assert ip_vrf is not None
|
||||
vrf_records = [
|
||||
{"vrf_name": "CUST_A", "address_families": "ipv4,ipv6", "rd": "100:1"},
|
||||
{"vrf_name": "CUST_B", "address_families": "ipv4", "rd": "100:2"},
|
||||
{"vrf_name": "CUST_C", "address_families": "ipv6", "rd": "100:3"},
|
||||
]
|
||||
ipv4_pairs = expand_bindings_from_discover_records(profile=v4, records=records)
|
||||
self.assertEqual({p[1]["vrf"] for p in ipv4_pairs}, {"CUST_A", "CUST_B"})
|
||||
ipv6_pairs = expand_bindings_from_discover_records(profile=v6, records=records)
|
||||
self.assertEqual({p[1]["vrf"] for p in ipv6_pairs}, {"CUST_A", "CUST_C"})
|
||||
fib_pairs = expand_bindings_from_discover_records(
|
||||
profile=ip_vrf, records=vrf_records
|
||||
)
|
||||
self.assertEqual({p[1]["vrf"] for p in fib_pairs}, {"CUST_A", "CUST_B"})
|
||||
|
||||
# BGP neighbor in/out: discover from config_bgp_peer with AF filters
|
||||
from netx_api.biz_state.command_match import (
|
||||
filter_discover_records,
|
||||
normalize_binding_dicts,
|
||||
shared_discover_placeholders,
|
||||
)
|
||||
|
||||
glob_v4 = get_profile("zte.bgp_vpnv4_neighbor_in")
|
||||
assert glob_v4 is not None
|
||||
self.assertEqual(glob_v4.placeholders[0].discover_profile_id, "zte.config_bgp_peer")
|
||||
self.assertEqual(glob_v4.placeholders[0].discover_filter_contains, "vpnv4")
|
||||
self.assertEqual(glob_v4.placeholders[0].bind_mode, "discover_select")
|
||||
self.assertTrue(any(ph.name == "local_as" for ph in glob_v4.placeholders))
|
||||
self.assertIn("as <local_as>", glob_v4.command_template)
|
||||
|
||||
glob_v6 = get_profile("zte.bgp_vpnv6_neighbor_in")
|
||||
assert glob_v6 is not None
|
||||
|
|
@ -767,13 +816,57 @@ $
|
|||
self.assertEqual(glob_ipv4.placeholders[0].discover_require_empty, "vrf")
|
||||
|
||||
peer_recs = [
|
||||
{"afi": "vpnv4", "vrf": "", "neighbor": "10.0.0.1", "remote_as": "65001"},
|
||||
{"afi": "vpnv4", "vrf": "", "neighbor": "", "peer_group": "CORE_RR", "remote_as": "65009"},
|
||||
{"afi": "vpnv4", "vrf": "", "neighbor": "10.0.0.9", "peer_group": "CORE_RR", "remote_as": "65019"},
|
||||
{"afi": "vpnv6", "vrf": "", "neighbor": "FC00::1", "remote_as": "65002"},
|
||||
{"afi": "ipv4", "vrf": "CUST_A", "neighbor": "10.0.0.2", "remote_as": "65003"},
|
||||
{"afi": "ipv4", "vrf": "", "neighbor": "10.0.0.8", "remote_as": "65004"},
|
||||
{"afi": "ipv6", "vrf": "CUST_B", "neighbor": "FC00::2", "remote_as": "65005"},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "vpnv4",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.1",
|
||||
"remote_as": "65001",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "vpnv4",
|
||||
"vrf": "",
|
||||
"neighbor": "",
|
||||
"peer_group": "CORE_RR",
|
||||
"remote_as": "65009",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "vpnv4",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.9",
|
||||
"peer_group": "CORE_RR",
|
||||
"remote_as": "65019",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "vpnv6",
|
||||
"vrf": "",
|
||||
"neighbor": "FC00::1",
|
||||
"remote_as": "65002",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv4",
|
||||
"vrf": "CUST_A",
|
||||
"neighbor": "10.0.0.2",
|
||||
"remote_as": "65003",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv4",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.8",
|
||||
"remote_as": "65004",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv6",
|
||||
"vrf": "CUST_B",
|
||||
"neighbor": "FC00::2",
|
||||
"remote_as": "65005",
|
||||
},
|
||||
]
|
||||
self.assertEqual(
|
||||
filter_discover_records(peer_recs, glob_v4.placeholders[0]),
|
||||
|
|
@ -799,33 +892,40 @@ $
|
|||
)
|
||||
|
||||
shared = shared_discover_placeholders(vrf_nei)
|
||||
self.assertEqual(len(shared), 2)
|
||||
self.assertEqual(len(shared), 3)
|
||||
self.assertTrue(all(ph.discover_profile_id == "zte.config_bgp_peer" for ph in shared))
|
||||
pair_cmds = expand_bindings_from_discover_records(profile=vrf_nei, records=peer_recs)
|
||||
self.assertEqual(len(pair_cmds), 1)
|
||||
self.assertEqual(pair_cmds[0][1], {"vrf": "CUST_A", "neighbor": "10.0.0.2"})
|
||||
self.assertEqual(
|
||||
pair_cmds[0][1],
|
||||
{"vrf": "CUST_A", "neighbor": "10.0.0.2", "local_as": "64900"},
|
||||
)
|
||||
self.assertIn("vrf CUST_A", pair_cmds[0][0])
|
||||
self.assertIn("10.0.0.2", pair_cmds[0][0])
|
||||
self.assertIn("as 64900", pair_cmds[0][0])
|
||||
|
||||
# Interleaved placeholder/value rows zip into combined bindings
|
||||
zipped = normalize_binding_dicts(
|
||||
[
|
||||
{"placeholder": "vrf", "value": "CUST_A"},
|
||||
{"placeholder": "neighbor", "value": "10.0.0.2"},
|
||||
{"placeholder": "local_as", "value": "64900"},
|
||||
{"placeholder": "vrf", "value": "CUST_B"},
|
||||
{"placeholder": "neighbor", "value": "10.0.0.3"},
|
||||
{"placeholder": "local_as", "value": "64900"},
|
||||
],
|
||||
placeholders=vrf_nei.placeholders,
|
||||
)
|
||||
self.assertEqual(
|
||||
zipped,
|
||||
[
|
||||
{"vrf": "CUST_A", "neighbor": "10.0.0.2"},
|
||||
{"vrf": "CUST_B", "neighbor": "10.0.0.3"},
|
||||
{"vrf": "CUST_A", "neighbor": "10.0.0.2", "local_as": "64900"},
|
||||
{"vrf": "CUST_B", "neighbor": "10.0.0.3", "local_as": "64900"},
|
||||
],
|
||||
)
|
||||
expanded = expand_from_bindings(profile=vrf_nei, bindings=zipped)
|
||||
self.assertEqual(len(expanded), 2)
|
||||
self.assertTrue(all("as 64900" in c for c, _ in expanded))
|
||||
|
||||
def test_interface_detail_and_vpnv6_neighbor(self) -> None:
|
||||
from netx_api.biz_state.parsers.zte import normalize_interface_detail
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue