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:
oliver 2026-09-22 19:50:17 +08:00
parent b453c6871e
commit 262082ef08
10 changed files with 690 additions and 318 deletions

View file

@ -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)

View file

@ -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__":

View file

@ -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(

View file

@ -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