mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 00:50:46 +08:00
Split BGP global activate discover by neighbor IP family.
IPv4/IPv6 neighbor in/out keep AF peers and peer-group expand, while top-level global activate is routed to ipv4 or ipv6 discover by address literal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
262082ef08
commit
4c5738e48e
5 changed files with 167 additions and 25 deletions
|
|
@ -132,18 +132,60 @@ def _optional_discover_placeholders(profile: ParseProfile) -> list[PlaceholderDe
|
|||
return [ph for ph in _discover_placeholders(profile) if not ph.required]
|
||||
|
||||
|
||||
def _neighbor_ip_family(addr: str) -> str:
|
||||
"""Return ``ipv4`` / ``ipv6`` / ```` for a neighbor literal."""
|
||||
s = str(addr or "").strip()
|
||||
if not s:
|
||||
return ""
|
||||
if re.fullmatch(r"\d{1,3}(?:\.\d{1,3}){3}", s):
|
||||
return "ipv4"
|
||||
if ":" not in s:
|
||||
return ""
|
||||
try:
|
||||
import ipaddress
|
||||
|
||||
ipaddress.ip_address(s.split("%", 1)[0])
|
||||
return "ipv6"
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
def _record_passes_discover_filter(rec: dict[str, Any], ph: PlaceholderDef) -> bool:
|
||||
filt_field = str(ph.discover_filter_field or "").strip()
|
||||
filt_contains = str(ph.discover_filter_contains or "").strip().lower()
|
||||
hay = ""
|
||||
if filt_field and filt_contains:
|
||||
hay = str(rec.get(filt_field) or "").strip().lower()
|
||||
# ``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:
|
||||
# Comma-separated exact OR (e.g. ``ipv4,global``).
|
||||
allowed = {x.strip() for x in filt_contains.split(",") if x.strip()}
|
||||
if hay not in allowed:
|
||||
return False
|
||||
elif filt_contains not in hay:
|
||||
return False
|
||||
equals_raw = str(getattr(ph, "discover_equals", "") or "").strip()
|
||||
if equals_raw:
|
||||
for part in equals_raw.split(","):
|
||||
part = part.strip()
|
||||
if not part or "=" not in part:
|
||||
continue
|
||||
field, expected = part.split("=", 1)
|
||||
field = field.strip()
|
||||
expected = expected.strip().lower()
|
||||
if not field:
|
||||
continue
|
||||
actual = str(rec.get(field) or "").strip().lower()
|
||||
if actual != expected:
|
||||
return False
|
||||
# Top-level/global activate: split by neighbor IP family (legacy global≈AF).
|
||||
gfam = str(getattr(ph, "discover_global_ip_family", "") or "").strip().lower()
|
||||
if gfam and (hay == "global" or str(rec.get("afi") or "").strip().lower() == "global"):
|
||||
if str(rec.get("activate") or "").strip().lower() != "enable":
|
||||
return False
|
||||
if _neighbor_ip_family(str(rec.get("neighbor") or "")) != gfam:
|
||||
return False
|
||||
require = str(ph.discover_require_nonempty or "").strip()
|
||||
if require and not str(rec.get(require) or "").strip():
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -233,6 +233,14 @@ def normalize_config_bgp_peer(
|
|||
act = "disable" if m.group(2) else "enable"
|
||||
activations.append((local_as, afi, vrf, nei, act))
|
||||
continue
|
||||
# Top-level activate (outside address-family): older ZTE builds treat
|
||||
# global context as IPv4 unicast — record as afi=global.
|
||||
m = _NEI_ACT_RE.match(line)
|
||||
if m and not afi:
|
||||
nei = m.group(1).strip()
|
||||
act = "disable" if m.group(2) else "enable"
|
||||
activations.append((local_as, "global", "", nei, act))
|
||||
continue
|
||||
m = _NEI_RM_RE.match(line)
|
||||
if m and afi:
|
||||
nei = m.group(1).strip()
|
||||
|
|
@ -266,8 +274,10 @@ def normalize_config_bgp_peer(
|
|||
)
|
||||
_append(row)
|
||||
|
||||
# Global AF: peer-group activate → expand to member Neighbor IPs.
|
||||
# VRF AF: do not expand from global peer-group membership.
|
||||
# Global AF: peer-group activate → expand to member Neighbor IPs
|
||||
# configured at global (``neighbor <ip> peer-group <name>``). Discover
|
||||
# for vpnv4/vpnv6/ipv6 then only filters this AF and still sees those
|
||||
# global members. VRF AF: do not expand from global peer-group membership.
|
||||
if vrf_s:
|
||||
continue
|
||||
pg = str(row.get("peer_group") or "").strip()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ class PlaceholderDef:
|
|||
# When required=False and no bindings: collect may expand all discover values.
|
||||
discover_filter_field: str = ""
|
||||
discover_filter_contains: str = ""
|
||||
# Extra exact matches: ``activate=enable`` or ``activate=enable,foo=bar``.
|
||||
discover_equals: str = ""
|
||||
# When afi=global is allowed: only keep activate=enable neighbors whose IP
|
||||
# matches this family (``ipv4`` / ``ipv6``). AF rows are unchanged.
|
||||
discover_global_ip_family: 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).
|
||||
|
|
@ -329,6 +334,12 @@ _BGP_LOCAL_AS = PlaceholderDef(
|
|||
|
||||
# BGP neighbor in/out: discover peers from Config BGP Peer Intent, filtered by AF.
|
||||
# required=False with local_as → unbound collect expands all (local_as, neighbor) pairs.
|
||||
#
|
||||
# vpnv4 / vpnv6: filter that AF only. Peer-group activate under the AF is expanded
|
||||
# into member Neighbor IPs (membership configured at global).
|
||||
#
|
||||
# ipv4 / ipv6: own AF rows + top-level/global activate=enable, split by neighbor
|
||||
# address family (IPv4 literals → ipv4 discover, IPv6 literals → ipv6 discover).
|
||||
_BGP_NEIGHBOR_VPNV4 = PlaceholderDef(
|
||||
name="neighbor",
|
||||
schema_field="neighbor",
|
||||
|
|
@ -366,7 +377,9 @@ _BGP_NEIGHBOR_IPV4 = PlaceholderDef(
|
|||
discover_value_field="neighbor",
|
||||
discover_label_field="neighbor",
|
||||
discover_filter_field="afi",
|
||||
discover_filter_contains="ipv4",
|
||||
discover_filter_contains="ipv4,global",
|
||||
discover_equals="activate=enable",
|
||||
discover_global_ip_family="ipv4",
|
||||
discover_require_empty="vrf",
|
||||
discover_require_nonempty="neighbor",
|
||||
)
|
||||
|
|
@ -380,7 +393,8 @@ _BGP_NEIGHBOR_IPV6 = PlaceholderDef(
|
|||
discover_value_field="neighbor",
|
||||
discover_label_field="neighbor",
|
||||
discover_filter_field="afi",
|
||||
discover_filter_contains="ipv6",
|
||||
discover_filter_contains="ipv6,global",
|
||||
discover_global_ip_family="ipv6",
|
||||
discover_require_empty="vrf",
|
||||
discover_require_nonempty="neighbor",
|
||||
)
|
||||
|
|
@ -1140,8 +1154,9 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
|||
match=r"(?i)^\s*show\s+bgp\s+ipv4\s+unicast\s+neighbor\s+(?P<direction>in)\s+(?P<neighbor>\S+)(?:\s+as\s+(?P<local_as>\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."
|
||||
"Routes learned from IPv4 unicast neighbor; discover ipv4 AF "
|
||||
"activate plus top-level/global activate for IPv4 neighbors; "
|
||||
"aux: show running-config bgp."
|
||||
),
|
||||
placeholders=[_BGP_NEIGHBOR_IPV4, _BGP_LOCAL_AS],
|
||||
fields=list(_BGP_ROUTE_FIELDS),
|
||||
|
|
@ -1161,7 +1176,8 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
|||
match=r"(?i)^\s*show\s+bgp\s+ipv4\s+unicast\s+neighbor\s+(?P<direction>out)\s+(?P<neighbor>\S+)(?:\s+as\s+(?P<local_as>\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; "
|
||||
"Routes advertised to IPv4 unicast neighbor; discover ipv4 AF "
|
||||
"activate plus top-level/global activate for IPv4 neighbors; "
|
||||
"aux: show running-config bgp."
|
||||
),
|
||||
placeholders=[_BGP_NEIGHBOR_IPV4, _BGP_LOCAL_AS],
|
||||
|
|
@ -1183,8 +1199,9 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
|||
match=r"(?i)^\s*show\s+bgp\s+ipv6\s+unicast\s+neighbor\s+(?P<direction>in)\s+(?P<neighbor>\S+)(?:\s+as\s+(?P<local_as>\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."
|
||||
"Routes learned from IPv6 unicast neighbor; discover ipv6 AF "
|
||||
"activate (incl. peer-group members) plus top-level/global "
|
||||
"activate for IPv6 neighbors; aux: show running-config bgp."
|
||||
),
|
||||
placeholders=[_BGP_NEIGHBOR_IPV6, _BGP_LOCAL_AS],
|
||||
fields=list(_BGP_ROUTE_FIELDS),
|
||||
|
|
@ -1204,7 +1221,8 @@ def _zte_status_profiles() -> list[ParseProfile]:
|
|||
match=r"(?i)^\s*show\s+bgp\s+ipv6\s+unicast\s+neighbor\s+(?P<direction>out)\s+(?P<neighbor>\S+)(?:\s+as\s+(?P<local_as>\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; "
|
||||
"Routes advertised to IPv6 unicast neighbor; discover ipv6 AF "
|
||||
"activate plus top-level/global activate for IPv6 neighbors; "
|
||||
"aux: show running-config bgp."
|
||||
),
|
||||
placeholders=[_BGP_NEIGHBOR_IPV6, _BGP_LOCAL_AS],
|
||||
|
|
@ -1824,6 +1842,8 @@ def profile_to_public_dict(p: ParseProfile, *, overrides: dict[str, Any] | None
|
|||
"discover_label_field": ph.discover_label_field,
|
||||
"discover_filter_field": ph.discover_filter_field,
|
||||
"discover_filter_contains": ph.discover_filter_contains,
|
||||
"discover_equals": ph.discover_equals,
|
||||
"discover_global_ip_family": ph.discover_global_ip_family,
|
||||
"discover_require_nonempty": ph.discover_require_nonempty,
|
||||
"discover_require_empty": ph.discover_require_empty,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -438,27 +438,29 @@ class ZteConfigIntentTests(unittest.TestCase):
|
|||
"loopback400",
|
||||
)
|
||||
self.assertNotIn(("64900", "l2vpn-evpn", "", "444::2", ""), by)
|
||||
# AS 64580: top-level activate only → global row
|
||||
# AS 64580: top-level activate only → global row with activate=enable
|
||||
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",
|
||||
by[("64580", "global", "", "22:22:22::22", "")]["activate"], "enable"
|
||||
)
|
||||
# AS 100: top-level activate → global (legacy ≈ ipv4) plus vpnv4 AF row
|
||||
self.assertEqual(
|
||||
by[("100", "global", "", "100.0.0.2", "")]["activate"], "enable"
|
||||
)
|
||||
self.assertEqual(
|
||||
by[("100", "vpnv4", "", "100.0.0.2", "")]["remote_as"], "100"
|
||||
)
|
||||
# Top-level activate disable must not look like enable
|
||||
self.assertEqual(
|
||||
by[("100", "global", "", "2408:8121:8400:1:1000::4:0", "MAR_GROUP_V6_1")][
|
||||
"activate"
|
||||
],
|
||||
"disable",
|
||||
)
|
||||
# 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"),
|
||||
|
|
|
|||
|
|
@ -812,8 +812,10 @@ $
|
|||
|
||||
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_filter_contains, "ipv4,global")
|
||||
self.assertEqual(glob_ipv4.placeholders[0].discover_require_empty, "vrf")
|
||||
self.assertEqual(glob_ipv4.placeholders[0].discover_equals, "activate=enable")
|
||||
self.assertEqual(glob_ipv4.placeholders[0].discover_global_ip_family, "ipv4")
|
||||
|
||||
peer_recs = [
|
||||
{
|
||||
|
|
@ -822,6 +824,7 @@ $
|
|||
"vrf": "",
|
||||
"neighbor": "10.0.0.1",
|
||||
"remote_as": "65001",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
|
|
@ -830,6 +833,7 @@ $
|
|||
"neighbor": "",
|
||||
"peer_group": "CORE_RR",
|
||||
"remote_as": "65009",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
|
|
@ -838,6 +842,7 @@ $
|
|||
"neighbor": "10.0.0.9",
|
||||
"peer_group": "CORE_RR",
|
||||
"remote_as": "65019",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
|
|
@ -845,6 +850,7 @@ $
|
|||
"vrf": "",
|
||||
"neighbor": "FC00::1",
|
||||
"remote_as": "65002",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
|
|
@ -852,6 +858,7 @@ $
|
|||
"vrf": "CUST_A",
|
||||
"neighbor": "10.0.0.2",
|
||||
"remote_as": "65003",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
|
|
@ -859,6 +866,39 @@ $
|
|||
"vrf": "",
|
||||
"neighbor": "10.0.0.8",
|
||||
"remote_as": "65004",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "global",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.7",
|
||||
"remote_as": "65007",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "global",
|
||||
"vrf": "",
|
||||
"neighbor": "FC00::7",
|
||||
"remote_as": "65017",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv4",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.88",
|
||||
"remote_as": "65088",
|
||||
"activate": "disable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv4",
|
||||
"vrf": "",
|
||||
"neighbor": "10.0.0.99",
|
||||
"remote_as": "65099",
|
||||
"activate": "",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
|
|
@ -866,20 +906,48 @@ $
|
|||
"vrf": "CUST_B",
|
||||
"neighbor": "FC00::2",
|
||||
"remote_as": "65005",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv6",
|
||||
"vrf": "",
|
||||
"neighbor": "FC00::8",
|
||||
"remote_as": "65006",
|
||||
"activate": "enable",
|
||||
},
|
||||
{
|
||||
"local_as": "64900",
|
||||
"afi": "ipv6",
|
||||
"vrf": "",
|
||||
"neighbor": "172.16.0.8",
|
||||
"remote_as": "65018",
|
||||
"activate": "enable",
|
||||
},
|
||||
]
|
||||
self.assertEqual(
|
||||
filter_discover_records(peer_recs, glob_v4.placeholders[0]),
|
||||
["10.0.0.1", "10.0.0.9"],
|
||||
)
|
||||
# vpnv4: direct AF neighbor + peer-group member (membership at global,
|
||||
# already expanded onto afi=vpnv4 by config_bgp_peer).
|
||||
self.assertEqual(
|
||||
filter_discover_records(peer_recs, glob_v6.placeholders[0]),
|
||||
["FC00::1"],
|
||||
)
|
||||
# Global ipv4: only empty-vrf peers (not VRF CE)
|
||||
# IPv4: ipv4 AF + global activate only for IPv4 literals
|
||||
self.assertEqual(
|
||||
filter_discover_records(peer_recs, glob_ipv4.placeholders[0]),
|
||||
["10.0.0.8"],
|
||||
["10.0.0.8", "10.0.0.7"],
|
||||
)
|
||||
glob_ipv6 = get_profile("zte.bgp_ipv6_neighbor_in")
|
||||
assert glob_ipv6 is not None
|
||||
self.assertEqual(glob_ipv6.placeholders[0].discover_filter_contains, "ipv6,global")
|
||||
self.assertEqual(glob_ipv6.placeholders[0].discover_global_ip_family, "ipv6")
|
||||
# IPv6 AF (any IP under AF) + global activate only for IPv6 literals
|
||||
self.assertEqual(
|
||||
set(filter_discover_records(peer_recs, glob_ipv6.placeholders[0])),
|
||||
{"FC00::8", "172.16.0.8", "FC00::7"},
|
||||
)
|
||||
# afi filter is exact: ipv4 must not match vpnv4
|
||||
vrf_nei = get_profile("zte.bgp_vpnv4_vrf_neighbor_in")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue