diff --git a/netx_api/biz_state/command_match.py b/netx_api/biz_state/command_match.py index b71e57c..d27cf12 100644 --- a/netx_api/biz_state/command_match.py +++ b/netx_api/biz_state/command_match.py @@ -137,7 +137,12 @@ def _record_passes_discover_filter(rec: dict[str, Any], ph: PlaceholderDef) -> b filt_contains = str(ph.discover_filter_contains or "").strip().lower() if filt_field and filt_contains: hay = str(rec.get(filt_field) or "").strip().lower() - if filt_contains not in hay: + # ``afi`` must be exact (``ipv4`` must not match ``vpnv4``). + # CSV fields like ``address_families`` still use substring/token contains. + if filt_field == "afi": + if hay != filt_contains: + return False + elif filt_contains not in hay: return False require = str(ph.discover_require_nonempty or "").strip() if require and not str(rec.get(require) or "").strip(): diff --git a/tests/test_zte_config_intent.py b/tests/test_zte_config_intent.py index 320b025..d288a7d 100644 --- a/tests/test_zte_config_intent.py +++ b/tests/test_zte_config_intent.py @@ -78,6 +78,9 @@ router bgp 65000 neighbor 10.0.0.1 route-map RM_OUT out neighbor CORE_RR activate $ + address-family ipv4 + neighbor CORE_RR activate + $ address-family ipv4 vrf CUST_A neighbor 10.0.0.2 remote-as 65003 neighbor 10.0.0.2 activate @@ -304,6 +307,10 @@ class ZteConfigIntentTests(unittest.TestCase): 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") + # 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) # 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) diff --git a/tests/test_zte_extended_parsers.py b/tests/test_zte_extended_parsers.py index 24ca2d9..337505e 100644 --- a/tests/test_zte_extended_parsers.py +++ b/tests/test_zte_extended_parsers.py @@ -774,9 +774,16 @@ $ filter_discover_records(peer_recs, glob_v6.placeholders[0]), ["FC00::1"], ) - + # afi filter is exact: ipv4 must not match vpnv4 vrf_nei = get_profile("zte.bgp_vpnv4_vrf_neighbor_in") assert vrf_nei is not None + ipv4_nei_ph = next(ph for ph in vrf_nei.placeholders if ph.name == "neighbor") + self.assertEqual(ipv4_nei_ph.discover_filter_contains, "ipv4") + self.assertEqual( + filter_discover_records(peer_recs, ipv4_nei_ph), + ["10.0.0.2"], + ) + shared = shared_discover_placeholders(vrf_nei) self.assertEqual(len(shared), 2) self.assertTrue(all(ph.discover_profile_id == "zte.config_bgp_peer" for ph in shared)) @@ -919,6 +926,23 @@ gei-0/0/0/2 is up, ifindex: 2 self.assertEqual(routes[0]["afi"], "vpnv6") self.assertEqual(routes[0]["direction"], "in") + # IPv4 neighbor routes: same wrap join as vpnv6 + v4_wrap = normalize_bgp_route( + raw_text=""" +Routes Learned From This Neighbor: + Network Next Hop Metric LocPrf RtPrf Path +* 10.1.0.0/24 + 10.0.0.1 + 20 65001 ? +""", + command="show bgp ipv4 unicast neighbor in 10.0.0.1", + params={"neighbor": "10.0.0.1", "direction": "in", "afi": "ipv4"}, + ) + self.assertEqual(len(v4_wrap), 1) + self.assertEqual(v4_wrap[0]["network"], "10.1.0.0/24") + self.assertEqual(v4_wrap[0]["next_hop"], "10.0.0.1") + self.assertEqual(v4_wrap[0]["afi"], "ipv4") + if __name__ == "__main__": unittest.main()