netx/tests/test_zte_config_intent.py
oliver 6b35add5a4 Match BGP discover afi exactly so ipv4 does not hit vpnv4.
Apply the same global peer-group member expand to address-family ipv4; keep VRF direct-only. Cover IPv4 wrapped neighbor routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 15:15:27 +08:00

458 lines
16 KiB
Python

"""Synthetic tests for ZTE config-intent parsers (no live secrets / host data)."""
from __future__ import annotations
import unittest
from netx_api.biz_state.command_match import match_command
from netx_api.biz_state.compare_service import _default_zte_config_sheets, sheet_key
from netx_api.biz_state.parsers.zte import (
normalize_config_bgp_peer,
normalize_config_interface,
normalize_config_isis,
normalize_config_l2vpn_pw,
normalize_config_ospf,
normalize_config_static_route,
normalize_config_vrf,
)
from netx_api.biz_state.parsers.zte.config_bgp_peer import _is_ip_neighbor
from netx_api.biz_state.profiles import get_profile, metric_field_map, reload_profiles
_CFG_VRF = """
!<vrf>
ip vrf CUST_A
rd 100:1
description "Customer A"
address-family ipv4
route-target export 100:1
route-target import 100:1
$
address-family ipv6
route-target export 100:2
$
$
ip vrf CUST_B
rd 200:2
address-family ipv4
route-target import 200:2
$
$
!</vrf>
"""
_CFG_IF = """
!<if-intf>
interface gei-0/0/0/1
description uplink
ip vrf forwarding CUST_A
ip address 10.0.0.1 255.255.255.0
ip address 10.0.0.2 255.255.255.0 secondary
ipv6 address 2001:db8::1/64
ipv6 address 2001:db8::2/64 secondary
mtu 9000
$
interface gei-0/0/0/2
shutdown
ip address 10.0.1.1 255.255.255.0
$
!</if-intf>
"""
_CFG_BGP = """
!<bgp>
router bgp 65000
neighbor 10.0.0.1 remote-as 65001
neighbor 10.0.0.1 update-source loopback1
neighbor 10.0.0.1 password cipher SKIPME
neighbor 10.0.0.1 route-map RM_IN in
neighbor 10.0.0.1 peer-group CORE_RR
neighbor 10.0.0.9 peer-group CORE_RR
neighbor 10.0.0.9 remote-as 65019
neighbor CORE_RR peer-group
neighbor CORE_RR remote-as 65009
neighbor FC00:1::1 remote-as 65002
address-family vpnv4
neighbor 10.0.0.1 activate
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
$
address-family l2vpn evpn
neighbor 10.0.0.1 activate
$
$
!</bgp>
"""
_CFG_L2VPN = """
!<l2vpn>
vpws VPN_A
description "pw site a"
access-point gei-0/0/0/1.100
pseudo-wire pw1
neighbour 10.0.0.1 vcid 1001
encapsulation mpls
$
$
$
vpls VPN_B
access-point smartgroup1
pseudo-wire pw2
neighbour 10.0.0.2 vcid 2002
encapsulation mpls
$
$
$
!</l2vpn>
"""
_CFG_STATIC_V4 = """
!<static>
ip route 0.0.0.0 0.0.0.0 10.0.0.1 name default_gw
ip route 10.9.9.0 255.255.255.0 null1
ip route 10.8.8.0 255.255.255.0 nexthop-vrf OTHER_VRF
ip route vrf CUST_A 10.1.1.0 255.255.255.0 bvi12.100 10.1.1.1
ip route vrf CUST_A 10.2.2.0 255.255.255.0 gei-0/0/0/1.10 10.2.2.1 bfd enable
ip route vrf CUST_B 10.3.3.0 255.255.255.0 null1 metric 255
ip route vrf CUST_A 10.4.4.0 255.255.255.0 smartgroup1.1 10.4.4.1 track LINK_A
ip route 10.5.5.0 255.255.255.0 xxvgei-0/2/0/1
ip route 10.6.6.0 255.255.255.0 te_tunnel36
ip route 10.7.7.0 255.255.255.0 10.7.7.1 xxvgei-0/2/0/2 name nh_then_if
!</static>
"""
_CFG_STATIC_V6 = """
!<ipv6-static-route>
ipv6 route ::/0 null1 track LINK_BFD
ipv6 route vrf CUST_A 2001:db8:1::/64 2001:db8:1::1
!</ipv6-static-route>
"""
_CFG_OSPF = """
!<ospfv2>
router ospf 3 vrf CUST_A
router-id 1.1.1.1
area 0.0.0.0
interface smartgroup1.10
cost 20
hello-interval 10
dead-interval 40
network point-to-point
bfd interval 50 min-rx 50
authentication message-digest
message-digest-key 1 md5 encrypted SKIPME
$
$
area 10.0.0.1
nssa default-information-originate no-summary
interface gei-0/0/0/1
cost 10
$
$
area 20.0.0.1
stub no-summary
$
redistribute static route-map RM1
redistribute connected
$
!</ospfv2>
"""
_CFG_OSPF_V3 = """
!<ospfv3>
ipv6 router ospf 120 vrf CORE
router-id 2.2.2.2
redistribute connected
area 0.0.0.0
interface vlan918
cost 10
network point-to-point
$
$
$
!</ospfv3>
"""
_CFG_ISIS = """
!<isis>
router isis 1
area 62.0047
system-id 1241.9509.6248
router-id 1.1.1.1
is-type level-2-only
authentication encrypted SKIPME
address-family ipv6
multi-topology
segment-routing srv6 locator MAIN
end-sid func-code ::1
$
$
interface loopback0
ipv6 router isis
passive-mode
$
interface smartgroup1
circuit-type level-2-only
ipv6 bfd-enable
ipv6 metric 5
ipv6 router isis
network point-to-point
hello-authentication encrypted SKIPME
bfd-enable
$
$
!</isis>
"""
class ZteConfigIntentTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
reload_profiles()
def test_profiles_and_field_schema(self) -> None:
for pid, mid in (
("zte.config_vrf", "config_vrf"),
("zte.config_interface", "config_interface"),
("zte.config_bgp_peer", "config_bgp_peer"),
("zte.config_l2vpn_pw", "config_l2vpn_pw"),
("zte.config_static_route", "config_static_route"),
("zte.config_static_route_v6", "config_static_route"),
("zte.config_ospf", "config_ospf"),
("zte.config_ospf_v3", "config_ospf"),
("zte.config_isis", "config_isis"),
):
p = get_profile(pid)
self.assertIsNotNone(p)
assert p is not None
self.assertEqual(p.metric_id, mid)
self.assertTrue(p.enabled)
fields = metric_field_map().get(mid) or []
self.assertTrue(any(f.is_key for f in fields))
def test_if_intf_cli_prefers_config_interface(self) -> None:
hit = match_command(vendor_key="zte", command="show running-config if-intf")
self.assertIsNotNone(hit)
assert hit is not None
self.assertEqual(hit.profile.profile_id, "zte.config_interface")
ii = get_profile("zte.if_intf")
assert ii is not None
self.assertFalse(ii.enabled)
def test_config_vrf(self) -> None:
rows = normalize_config_vrf(raw_text=_CFG_VRF, command="show running-config vrf")
by = {r["vrf_name"]: r for r in rows}
self.assertEqual(set(by), {"CUST_A", "CUST_B"})
self.assertEqual(by["CUST_A"]["rd"], "100:1")
self.assertIn("ipv4", by["CUST_A"]["address_families"])
self.assertIn("ipv6", by["CUST_A"]["address_families"])
self.assertIn("100:1", by["CUST_A"]["rt_export"])
self.assertIn("100:2", by["CUST_A"]["rt_export"])
self.assertEqual(by["CUST_A"]["description"], "Customer A")
self.assertIn("200:2", by["CUST_B"]["rt_import"])
def test_config_interface(self) -> None:
rows = normalize_config_interface(
raw_text=_CFG_IF, command="show running-config if-intf"
)
by = {r["interface"]: r for r in rows}
self.assertEqual(by["gei-0/0/0/1"]["vrf"], "CUST_A")
self.assertEqual(by["gei-0/0/0/1"]["admin"], "up")
self.assertEqual(
by["gei-0/0/0/1"]["ip_address"],
"10.0.0.1/255.255.255.0,10.0.0.2/255.255.255.0",
)
self.assertEqual(by["gei-0/0/0/1"]["secondary_tag"], "M,S")
self.assertEqual(
by["gei-0/0/0/1"]["ipv6_address"],
"2001:db8::1/64,2001:db8::2/64",
)
self.assertEqual(by["gei-0/0/0/1"]["ipv6_secondary_tag"], "M,S")
self.assertEqual(by["gei-0/0/0/1"]["mtu"], "9000")
self.assertEqual(by["gei-0/0/0/2"]["admin"], "down")
self.assertEqual(by["gei-0/0/0/2"]["secondary_tag"], "M")
self.assertEqual(by["gei-0/0/0/2"]["ipv6_secondary_tag"], "")
def test_config_bgp_peer_skips_password(self) -> None:
rows = normalize_config_bgp_peer(
raw_text=_CFG_BGP, command="show running-config bgp"
)
blob = " ".join(str(v) for r in rows for v in r.values())
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")
# 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"], "")
# 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")
# 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")
# 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)
self.assertIn(("l2vpn-evpn", "", "10.0.0.1", "CORE_RR"), by)
# global IPv6 neighbor without AF activate
self.assertIn(("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))
def test_config_l2vpn_pw(self) -> None:
rows = normalize_config_l2vpn_pw(
raw_text=_CFG_L2VPN, command="show running-config l2vpn"
)
by = {(r["vpn_name"], r["pw_name"]): r for r in rows}
self.assertEqual(by[("VPN_A", "pw1")]["peer"], "10.0.0.1")
self.assertEqual(by[("VPN_A", "pw1")]["vcid"], "1001")
self.assertEqual(by[("VPN_A", "pw1")]["vpn_type"], "vpws")
self.assertEqual(by[("VPN_A", "pw1")]["access_point"], "gei-0/0/0/1.100")
self.assertEqual(by[("VPN_B", "pw2")]["vpn_type"], "vpls")
self.assertEqual(by[("VPN_B", "pw2")]["vcid"], "2002")
def test_config_interface_merges_duplicate_blocks(self) -> None:
raw = """
!<if-intf>
interface te_tunnel36
description LSP
shutdown
$
interface te_tunnel36
ip unnumbered loopback0
$
!</if-intf>
"""
rows = normalize_config_interface(raw_text=raw, command="show running-config if-intf")
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["admin"], "down")
self.assertEqual(rows[0]["description"], "LSP")
def test_config_l2vpn_backup_pw_not_overwrite(self) -> None:
raw = """
!<l2vpn>
vpws 18163
access-point gei-0/0/1/9.1751
pseudo-wire pw_primary
neighbour 10.0.0.1 vcid 18163
encapsulation raw
$
backup-pw pw_backup protect pw_primary
neighbour 10.0.0.2 vcid 18163
$
$
$
!</l2vpn>
"""
rows = normalize_config_l2vpn_pw(raw_text=raw, command="show running-config l2vpn")
by = {r["pw_name"]: r for r in rows}
self.assertEqual(by["pw_primary"]["peer"], "10.0.0.1")
self.assertEqual(by["pw_backup"]["peer"], "10.0.0.2")
self.assertEqual(by["pw_primary"]["vcid"], "18163")
def test_config_static_route_v4(self) -> None:
rows = normalize_config_static_route(
raw_text=_CFG_STATIC_V4, command="show running-config static"
)
self.assertTrue(all(r["af"] == "ipv4" for r in rows))
by = {(r["vrf"], r["prefix"], r["interface"], r["next_hop"]): r for r in rows}
self.assertEqual(by[("", "0.0.0.0", "", "10.0.0.1")]["route_name"], "default_gw")
self.assertEqual(by[("", "10.9.9.0", "null1", "")]["mask"], "255.255.255.0")
self.assertEqual(by[("", "10.8.8.0", "", "")]["nexthop_vrf"], "OTHER_VRF")
self.assertEqual(
by[("CUST_A", "10.1.1.0", "bvi12.100", "10.1.1.1")]["mask"], "255.255.255.0"
)
self.assertEqual(by[("CUST_A", "10.2.2.0", "gei-0/0/0/1.10", "10.2.2.1")]["bfd"], "enable")
self.assertEqual(by[("CUST_B", "10.3.3.0", "null1", "")]["metric"], "255")
self.assertEqual(by[("CUST_A", "10.4.4.0", "smartgroup1.1", "10.4.4.1")]["track"], "LINK_A")
# Interface-only (no next-hop address) — including xxvgei / te_tunnel.
self.assertEqual(by[("", "10.5.5.0", "xxvgei-0/2/0/1", "")]["mask"], "255.255.255.0")
self.assertEqual(by[("", "10.6.6.0", "te_tunnel36", "")]["interface"], "te_tunnel36")
# next-hop then interface
self.assertEqual(
by[("", "10.7.7.0", "xxvgei-0/2/0/2", "10.7.7.1")]["route_name"], "nh_then_if"
)
def test_config_static_route_v6(self) -> None:
rows = normalize_config_static_route(
raw_text=_CFG_STATIC_V6, command="show running-config ipv6-static-route"
)
self.assertEqual(len(rows), 2)
self.assertTrue(all(r["af"] == "ipv6" for r in rows))
by = {(r["vrf"], r["prefix"]): r for r in rows}
self.assertEqual(by[("", "::/0")]["interface"], "null1")
self.assertEqual(by[("", "::/0")]["track"], "LINK_BFD")
self.assertEqual(by[("CUST_A", "2001:db8:1::/64")]["next_hop"], "2001:db8:1::1")
def test_compare_config_sheets(self) -> None:
sheets = _default_zte_config_sheets()
ids = [sheet_key(s) for s in sheets]
self.assertIn("config_vrf", ids)
self.assertIn("config_interface", ids)
self.assertIn("config_bgp_peer", ids)
self.assertIn("config_l2vpn_pw", ids)
self.assertIn("config_static_route.ipv4", ids)
self.assertIn("config_static_route.ipv6", ids)
self.assertIn("config_ospf.ipv4", ids)
self.assertIn("config_ospf.ipv6", ids)
self.assertIn("config_isis", ids)
for s in sheets:
self.assertTrue(s.get("key_fields"))
self.assertTrue(s.get("compare_fields") or s.get("key_fields"))
def test_config_ospf(self) -> None:
rows = normalize_config_ospf(raw_text=_CFG_OSPF, command="show running-config ospfv2")
blob = " ".join(str(v) for r in rows for v in r.values())
self.assertNotIn("SKIPME", blob)
by = {(r["area"], r["interface"]): r for r in rows}
self.assertEqual(by[("0.0.0.0", "smartgroup1.10")]["cost"], "20")
self.assertEqual(by[("0.0.0.0", "smartgroup1.10")]["network_type"], "point-to-point")
self.assertEqual(by[("0.0.0.0", "smartgroup1.10")]["bfd"], "enable")
self.assertEqual(by[("0.0.0.0", "smartgroup1.10")]["vrf"], "CUST_A")
self.assertIn("static", by[("0.0.0.0", "smartgroup1.10")]["redistribute"])
self.assertEqual(by[("10.0.0.1", "gei-0/0/0/1")]["area_type"], "nssa")
self.assertEqual(by[("20.0.0.1", "")]["area_type"], "stub")
v3 = normalize_config_ospf(raw_text=_CFG_OSPF_V3, command="show running-config ospfv3")
self.assertEqual(len(v3), 1)
self.assertEqual(v3[0]["af"], "ipv6")
self.assertEqual(v3[0]["interface"], "vlan918")
def test_config_isis(self) -> None:
rows = normalize_config_isis(raw_text=_CFG_ISIS, command="show running-config isis")
blob = " ".join(str(v) for r in rows for v in r.values())
self.assertNotIn("SKIPME", blob)
by = {r["interface"]: r for r in rows}
self.assertEqual(set(by), {"loopback0", "smartgroup1"})
self.assertEqual(by["loopback0"]["passive"], "yes")
self.assertEqual(by["loopback0"]["ipv6_enable"], "yes")
self.assertEqual(by["loopback0"]["area"], "62.0047")
self.assertEqual(by["smartgroup1"]["circuit_type"], "level-2-only")
self.assertEqual(by["smartgroup1"]["ipv6_metric"], "5")
self.assertEqual(by["smartgroup1"]["ipv6_bfd"], "enable")
self.assertEqual(by["smartgroup1"]["bfd"], "enable")
self.assertEqual(by["smartgroup1"]["network_type"], "point-to-point")
if __name__ == "__main__":
unittest.main()