diff --git a/netx_api/biz_state/command_match.py b/netx_api/biz_state/command_match.py index d27cf12..683f299 100644 --- a/netx_api/biz_state/command_match.py +++ b/netx_api/biz_state/command_match.py @@ -147,6 +147,9 @@ def _record_passes_discover_filter(rec: dict[str, Any], ph: PlaceholderDef) -> b require = str(ph.discover_require_nonempty or "").strip() if require and not str(rec.get(require) or "").strip(): return False + require_empty = str(getattr(ph, "discover_require_empty", "") or "").strip() + if require_empty and str(rec.get(require_empty) or "").strip(): + return False return True diff --git a/netx_api/biz_state/profiles.py b/netx_api/biz_state/profiles.py index 38fa4ce..85d0519 100644 --- a/netx_api/biz_state/profiles.py +++ b/netx_api/biz_state/profiles.py @@ -40,6 +40,8 @@ class PlaceholderDef: discover_filter_contains: str = "" # Skip rows where this field is empty (e.g. CE peers require non-empty vrf). discover_require_nonempty: str = "" + # Skip rows where this field is non-empty (e.g. global AF peers require empty vrf). + discover_require_empty: str = "" @dataclass(frozen=True) @@ -324,6 +326,7 @@ _BGP_NEIGHBOR_VPNV4 = PlaceholderDef( discover_label_field="neighbor", discover_filter_field="afi", discover_filter_contains="vpnv4", + discover_require_empty="vrf", ) _BGP_NEIGHBOR_VPNV6 = PlaceholderDef( @@ -336,6 +339,33 @@ _BGP_NEIGHBOR_VPNV6 = PlaceholderDef( discover_label_field="neighbor", discover_filter_field="afi", discover_filter_contains="vpnv6", + discover_require_empty="vrf", +) + +_BGP_NEIGHBOR_IPV4 = PlaceholderDef( + name="neighbor", + schema_field="neighbor", + required=True, + bind_mode="discover_select", + discover_profile_id="zte.config_bgp_peer", + discover_value_field="neighbor", + discover_label_field="neighbor", + discover_filter_field="afi", + discover_filter_contains="ipv4", + discover_require_empty="vrf", +) + +_BGP_NEIGHBOR_IPV6 = PlaceholderDef( + name="neighbor", + schema_field="neighbor", + required=True, + bind_mode="discover_select", + discover_profile_id="zte.config_bgp_peer", + discover_value_field="neighbor", + discover_label_field="neighbor", + discover_filter_field="afi", + discover_filter_contains="ipv6", + discover_require_empty="vrf", ) # Per-VRF CE peers live under address-family ipv4/ipv6 vrf . @@ -1075,6 +1105,120 @@ def _zte_status_profiles() -> list[ParseProfile]: ), ], ), + ParseProfile( + profile_id="zte.bgp_ipv4_neighbor_in", + vendor_key="zte", + metric_id="bgp_route", + parser_id="bgp_route", + title="BGP IPv4 Neighbor In", + command_template="show bgp ipv4 unicast neighbor in | one-line", + match=r"(?i)^\s*show\s+bgp\s+ipv4\s+unicast\s+neighbor\s+(?Pin)\s+(?P\S+)(?:\s*\|\s*one-line)?\s*$", + textfsm_command="show bgp ipv4 unicast neighbor in", + description=( + "Routes learned from IPv4 unicast neighbor; discover from BGP peer intent " + "(direct + peer-group members); aux: show running-config bgp." + ), + placeholders=[_BGP_NEIGHBOR_IPV4], + fields=list(_BGP_ROUTE_FIELDS), + tags=["bgp", "ipv4", "route"], + sort_order=428, + enabled=True, + kind="collect", + enrich_joins=[ + EnrichJoin( + from_aux="config_bgp_peer", + left_on="afi,neighbor", + right_on="afi,neighbor", + take=("remote_as", "activate", "route_map_in", "route_map_out"), + ), + ], + ), + ParseProfile( + profile_id="zte.bgp_ipv4_neighbor_out", + vendor_key="zte", + metric_id="bgp_route", + parser_id="bgp_route", + title="BGP IPv4 Neighbor Out", + command_template="show bgp ipv4 unicast neighbor out | one-line", + match=r"(?i)^\s*show\s+bgp\s+ipv4\s+unicast\s+neighbor\s+(?Pout)\s+(?P\S+)(?:\s*\|\s*one-line)?\s*$", + textfsm_command="show bgp ipv4 unicast neighbor out", + description=( + "Routes advertised to IPv4 unicast neighbor; discover from BGP peer intent; " + "aux: show running-config bgp." + ), + placeholders=[_BGP_NEIGHBOR_IPV4], + fields=list(_BGP_ROUTE_FIELDS), + tags=["bgp", "ipv4", "route"], + sort_order=429, + enabled=True, + kind="collect", + collect_lane="heavy", + enrich_joins=[ + EnrichJoin( + from_aux="config_bgp_peer", + left_on="afi,neighbor", + right_on="afi,neighbor", + take=("remote_as", "activate", "route_map_in", "route_map_out"), + ), + ], + ), + ParseProfile( + profile_id="zte.bgp_ipv6_neighbor_in", + vendor_key="zte", + metric_id="bgp_route", + parser_id="bgp_route", + title="BGP IPv6 Neighbor In", + command_template="show bgp ipv6 unicast neighbor in | one-line", + match=r"(?i)^\s*show\s+bgp\s+ipv6\s+unicast\s+neighbor\s+(?Pin)\s+(?P\S+)(?:\s*\|\s*one-line)?\s*$", + textfsm_command="show bgp ipv6 unicast neighbor in", + description=( + "Routes learned from IPv6 unicast neighbor; discover from BGP peer intent " + "(direct + peer-group members); aux: show running-config bgp." + ), + placeholders=[_BGP_NEIGHBOR_IPV6], + fields=list(_BGP_ROUTE_FIELDS), + tags=["bgp", "ipv6", "route"], + sort_order=430, + enabled=True, + kind="collect", + enrich_joins=[ + EnrichJoin( + from_aux="config_bgp_peer", + left_on="afi,neighbor", + right_on="afi,neighbor", + take=("remote_as", "activate", "route_map_in", "route_map_out"), + ), + ], + ), + ParseProfile( + profile_id="zte.bgp_ipv6_neighbor_out", + vendor_key="zte", + metric_id="bgp_route", + parser_id="bgp_route", + title="BGP IPv6 Neighbor Out", + command_template="show bgp ipv6 unicast neighbor out | one-line", + match=r"(?i)^\s*show\s+bgp\s+ipv6\s+unicast\s+neighbor\s+(?Pout)\s+(?P\S+)(?:\s*\|\s*one-line)?\s*$", + textfsm_command="show bgp ipv6 unicast neighbor out", + description=( + "Routes advertised to IPv6 unicast neighbor; discover from BGP peer intent; " + "aux: show running-config bgp." + ), + placeholders=[_BGP_NEIGHBOR_IPV6], + fields=list(_BGP_ROUTE_FIELDS), + tags=["bgp", "ipv6", "route"], + sort_order=431, + enabled=True, + kind="collect", + collect_lane="heavy", + enrich_joins=[ + EnrichJoin( + from_aux="config_bgp_peer", + left_on="afi,neighbor", + right_on="afi,neighbor", + take=("remote_as", "activate", "route_map_in", "route_map_out"), + ), + ], + ), ParseProfile( profile_id="zte.bgp_vpnv4_vrf_neighbor_in", vendor_key="zte", @@ -1094,7 +1238,7 @@ def _zte_status_profiles() -> list[ParseProfile]: placeholders=[_BGP_VRF_PEER_IPV4, _BGP_NEIGHBOR_IPV4_VRF], fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv4", "vrf", "route"], - sort_order=430, + sort_order=440, enabled=True, kind="collect", enrich_joins=[ @@ -1125,7 +1269,7 @@ def _zte_status_profiles() -> list[ParseProfile]: placeholders=[_BGP_VRF_PEER_IPV4, _BGP_NEIGHBOR_IPV4_VRF], fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv4", "vrf", "route"], - sort_order=435, + sort_order=445, enabled=True, kind="collect", collect_lane="heavy", @@ -1157,7 +1301,7 @@ def _zte_status_profiles() -> list[ParseProfile]: placeholders=[_BGP_VRF_PEER_IPV6, _BGP_NEIGHBOR_IPV6_VRF], fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv6", "vrf", "route"], - sort_order=440, + sort_order=450, enabled=True, kind="collect", enrich_joins=[ @@ -1188,7 +1332,7 @@ def _zte_status_profiles() -> list[ParseProfile]: placeholders=[_BGP_VRF_PEER_IPV6, _BGP_NEIGHBOR_IPV6_VRF], fields=list(_BGP_ROUTE_FIELDS), tags=["bgp", "vpnv6", "vrf", "route"], - sort_order=445, + sort_order=455, enabled=True, kind="collect", collect_lane="heavy", @@ -1707,6 +1851,7 @@ def profile_to_public_dict(p: ParseProfile, *, overrides: dict[str, Any] | None "discover_filter_field": ph.discover_filter_field, "discover_filter_contains": ph.discover_filter_contains, "discover_require_nonempty": ph.discover_require_nonempty, + "discover_require_empty": ph.discover_require_empty, } for ph in p.placeholders ], diff --git a/tests/test_biz_state_profiles.py b/tests/test_biz_state_profiles.py index 19c600c..d27dc0b 100644 --- a/tests/test_biz_state_profiles.py +++ b/tests/test_biz_state_profiles.py @@ -77,6 +77,10 @@ class BizStateProfileTests(unittest.TestCase): "zte.bgp_vpnv4_neighbor_out", "zte.bgp_vpnv6_neighbor_in", "zte.bgp_vpnv6_neighbor_out", + "zte.bgp_ipv4_neighbor_in", + "zte.bgp_ipv4_neighbor_out", + "zte.bgp_ipv6_neighbor_in", + "zte.bgp_ipv6_neighbor_out", "zte.bgp_vpnv4_vrf_neighbor_in", "zte.bgp_vpnv4_vrf_neighbor_out", "zte.bgp_vpnv6_vrf_neighbor_in", diff --git a/tests/test_zte_extended_parsers.py b/tests/test_zte_extended_parsers.py index 337505e..3294605 100644 --- a/tests/test_zte_extended_parsers.py +++ b/tests/test_zte_extended_parsers.py @@ -677,6 +677,10 @@ $ self.assertIsNotNone(get_profile("zte.l2vpn_pw_detail")) self.assertIsNotNone(get_profile("zte.bgp_vpnv6_neighbor_in")) self.assertIsNotNone(get_profile("zte.bgp_vpnv6_neighbor_out")) + self.assertIsNotNone(get_profile("zte.bgp_ipv4_neighbor_in")) + self.assertIsNotNone(get_profile("zte.bgp_ipv4_neighbor_out")) + self.assertIsNotNone(get_profile("zte.bgp_ipv6_neighbor_in")) + self.assertIsNotNone(get_profile("zte.bgp_ipv6_neighbor_out")) p = get_profile("zte.bgp_vpnv4_vrf_neighbor_in") assert p is not None @@ -757,13 +761,18 @@ $ assert glob_v6 is not None self.assertEqual(glob_v6.placeholders[0].discover_filter_contains, "vpnv6") + glob_ipv4 = get_profile("zte.bgp_ipv4_neighbor_in") + assert glob_ipv4 is not None + self.assertEqual(glob_ipv4.placeholders[0].discover_filter_contains, "ipv4") + 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.9", "remote_as": "65004"}, + {"afi": "ipv4", "vrf": "", "neighbor": "10.0.0.8", "remote_as": "65004"}, {"afi": "ipv6", "vrf": "CUST_B", "neighbor": "FC00::2", "remote_as": "65005"}, ] self.assertEqual( @@ -774,6 +783,11 @@ $ filter_discover_records(peer_recs, glob_v6.placeholders[0]), ["FC00::1"], ) + # Global ipv4: only empty-vrf peers (not VRF CE) + self.assertEqual( + filter_discover_records(peer_recs, glob_ipv4.placeholders[0]), + ["10.0.0.8"], + ) # 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 @@ -926,6 +940,15 @@ gei-0/0/0/2 is up, ifindex: 2 self.assertEqual(routes[0]["afi"], "vpnv6") self.assertEqual(routes[0]["direction"], "in") + hit4 = match_command( + vendor_key="zte", + command="show bgp ipv4 unicast neighbor in 10.0.0.1 | one-line", + ) + self.assertIsNotNone(hit4) + assert hit4 is not None + self.assertEqual(hit4.profile.profile_id, "zte.bgp_ipv4_neighbor_in") + self.assertEqual(hit4.params.get("neighbor"), "10.0.0.1") + # IPv4 neighbor routes: same wrap join as vpnv6 v4_wrap = normalize_bgp_route( raw_text="""