mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 06:40:45 +08:00
Derive discover-backed aux automatically and fix BGP enrich join keys.
Overwrite explicit aux from discover_select at load time; join VRF CE on vrf,neighbor and global neighbors on afi,neighbor to avoid cross-AF collisions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c14d4e81ff
commit
f274d8ea72
5 changed files with 420 additions and 50 deletions
72
tests/test_biz_state_enrich.py
Normal file
72
tests/test_biz_state_enrich.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Unit tests for EnrichJoin single- and composite-key joins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from netx_api.biz_state.enrich import EnrichJoin, apply_enrich_joins
|
||||
|
||||
|
||||
class EnrichJoinTests(unittest.TestCase):
|
||||
def test_single_key(self) -> None:
|
||||
rows = [{"neighbor": "10.0.0.1", "network": "1.1.1.0/24"}]
|
||||
apply_enrich_joins(
|
||||
rows,
|
||||
{
|
||||
"config_bgp_peer": [
|
||||
{"neighbor": "10.0.0.1", "remote_as": "65001", "activate": "enable"},
|
||||
]
|
||||
},
|
||||
[EnrichJoin(from_aux="config_bgp_peer", on="neighbor", take=("remote_as", "activate"))],
|
||||
)
|
||||
self.assertEqual(rows[0]["remote_as"], "65001")
|
||||
|
||||
def test_composite_vrf_neighbor(self) -> None:
|
||||
rows = [
|
||||
{"vrf": "A", "neighbor": "10.0.0.1", "network": "1.0.0.0/24"},
|
||||
{"vrf": "B", "neighbor": "10.0.0.1", "network": "2.0.0.0/24"},
|
||||
]
|
||||
apply_enrich_joins(
|
||||
rows,
|
||||
{
|
||||
"config_bgp_peer": [
|
||||
{"vrf": "A", "neighbor": "10.0.0.1", "remote_as": "1"},
|
||||
{"vrf": "B", "neighbor": "10.0.0.1", "remote_as": "2"},
|
||||
]
|
||||
},
|
||||
[
|
||||
EnrichJoin(
|
||||
from_aux="config_bgp_peer",
|
||||
left_on="vrf,neighbor",
|
||||
right_on="vrf,neighbor",
|
||||
take=("remote_as",),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(rows[0]["remote_as"], "1")
|
||||
self.assertEqual(rows[1]["remote_as"], "2")
|
||||
|
||||
def test_afi_neighbor_avoids_wrong_af(self) -> None:
|
||||
rows = [{"afi": "vpnv4", "neighbor": "10.0.0.1"}]
|
||||
apply_enrich_joins(
|
||||
rows,
|
||||
{
|
||||
"config_bgp_peer": [
|
||||
{"afi": "ipv4", "neighbor": "10.0.0.1", "route_map_in": "RM_V4"},
|
||||
{"afi": "vpnv4", "neighbor": "10.0.0.1", "route_map_in": "RM_VPN"},
|
||||
]
|
||||
},
|
||||
[
|
||||
EnrichJoin(
|
||||
from_aux="config_bgp_peer",
|
||||
left_on="afi,neighbor",
|
||||
right_on="afi,neighbor",
|
||||
take=("route_map_in",),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(rows[0]["route_map_in"], "RM_VPN")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -6,12 +6,18 @@ import unittest
|
|||
|
||||
from netx_api.biz_state.command_match import expand_from_bindings, match_command, preview_task_item
|
||||
from netx_api.biz_state.profiles import (
|
||||
AuxCommand,
|
||||
ParseProfile,
|
||||
all_profiles,
|
||||
get_profile,
|
||||
profile_to_public_dict,
|
||||
profiles_for_vendor,
|
||||
reload_profiles,
|
||||
_finalize_profiles,
|
||||
_normalize_discover_aux,
|
||||
)
|
||||
from netx_api.biz_state.parsers import normalize_lldp_neighbors
|
||||
from netx_api.biz_state.profiles import PlaceholderDef
|
||||
from netx_api.lldp_shared import parse_neighbor_output
|
||||
import re
|
||||
|
||||
|
|
@ -64,8 +70,13 @@ class BizStateProfileTests(unittest.TestCase):
|
|||
hits = parse_neighbor_output("", vendor="zte", device_type="zte_zxros")
|
||||
self.assertEqual(hits, [])
|
||||
|
||||
def test_vrf_neighbor_discover_and_aux_both_config_bgp(self) -> None:
|
||||
def test_bgp_neighbor_discover_and_aux_both_config_bgp(self) -> None:
|
||||
reload_profiles()
|
||||
for pid in (
|
||||
"zte.bgp_vpnv4_neighbor_in",
|
||||
"zte.bgp_vpnv4_neighbor_out",
|
||||
"zte.bgp_vpnv6_neighbor_in",
|
||||
"zte.bgp_vpnv6_neighbor_out",
|
||||
"zte.bgp_vpnv4_vrf_neighbor_in",
|
||||
"zte.bgp_vpnv4_vrf_neighbor_out",
|
||||
"zte.bgp_vpnv6_vrf_neighbor_in",
|
||||
|
|
@ -81,6 +92,87 @@ class BizStateProfileTests(unittest.TestCase):
|
|||
self.assertEqual(len(aux), 1)
|
||||
self.assertIn("running-config bgp", aux[0]["command_template"])
|
||||
|
||||
def test_discover_backed_profiles_omit_explicit_aux(self) -> None:
|
||||
"""Source declarations should rely on framework derive (no redundant aux)."""
|
||||
import inspect
|
||||
from netx_api.biz_state import profiles as profiles_mod
|
||||
|
||||
src = inspect.getsource(profiles_mod._zte_status_profiles)
|
||||
# ARP may keep explicit aux; discover-backed must not hardcode config_bgp_peer aux
|
||||
self.assertNotIn(
|
||||
'AuxCommand(key="config_bgp_peer"',
|
||||
src,
|
||||
"discover-backed profiles must not list config_bgp_peer aux explicitly",
|
||||
)
|
||||
reload_profiles()
|
||||
p = get_profile("zte.bgp_vpnv4_neighbor_in")
|
||||
assert p is not None
|
||||
self.assertEqual([a.profile_id for a in p.aux_commands], ["zte.config_bgp_peer"])
|
||||
|
||||
def test_normalize_discover_aux_overwrites_wrong_aux(self) -> None:
|
||||
"""Framework replaces explicit aux when discover_select is present."""
|
||||
fake = ParseProfile(
|
||||
profile_id="zte.test_neighbor_routes",
|
||||
vendor_key="zte",
|
||||
metric_id="bgp_route",
|
||||
parser_id="bgp_route",
|
||||
title="test",
|
||||
command_template="show bgp neighbor in <neighbor> | one-line",
|
||||
match=r".*",
|
||||
placeholders=[
|
||||
PlaceholderDef(
|
||||
name="neighbor",
|
||||
schema_field="neighbor",
|
||||
bind_mode="discover_select",
|
||||
discover_profile_id="zte.config_bgp_peer",
|
||||
discover_value_field="neighbor",
|
||||
)
|
||||
],
|
||||
# Intentionally wrong — summary instead of config
|
||||
aux_commands=[
|
||||
AuxCommand(key="bgp_summary", profile_id="zte.bgp_vpnv4_summary"),
|
||||
],
|
||||
)
|
||||
_normalize_discover_aux(fake)
|
||||
self.assertEqual(
|
||||
[(a.key, a.profile_id) for a in fake.aux_commands],
|
||||
[("config_bgp_peer", "zte.config_bgp_peer")],
|
||||
)
|
||||
|
||||
def test_arp_keeps_explicit_aux_without_discover(self) -> None:
|
||||
reload_profiles()
|
||||
arp = get_profile("zte.arp")
|
||||
assert arp is not None
|
||||
self.assertTrue(any(a.profile_id == "zte.config_interface" for a in arp.aux_commands))
|
||||
self.assertFalse(
|
||||
any(str(ph.bind_mode or "") == "discover_select" for ph in (arp.placeholders or []))
|
||||
)
|
||||
|
||||
def test_finalize_rejects_aux_placeholder_outside_primary(self) -> None:
|
||||
target = ParseProfile(
|
||||
profile_id="zte.fake_vrf_summary",
|
||||
vendor_key="zte",
|
||||
metric_id="bgp_peer",
|
||||
parser_id="bgp_peer",
|
||||
title="fake",
|
||||
command_template="show bgp vpnv4 unicast vrf <vrf> summary | one-line",
|
||||
match=r".*",
|
||||
)
|
||||
bad = ParseProfile(
|
||||
profile_id="zte.bad_aux",
|
||||
vendor_key="zte",
|
||||
metric_id="x",
|
||||
parser_id="x",
|
||||
title="bad",
|
||||
command_template="show foo",
|
||||
match=r".*",
|
||||
placeholders=[],
|
||||
aux_commands=[AuxCommand(key="vrf_sum", profile_id="zte.fake_vrf_summary")],
|
||||
)
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
_finalize_profiles([bad, target])
|
||||
self.assertIn("placeholders", str(ctx.exception))
|
||||
|
||||
def test_aux_templates_placeholders_subset_of_primary(self) -> None:
|
||||
"""Aux CLI must not introduce placeholders outside primary params.
|
||||
|
||||
|
|
|
|||
|
|
@ -396,13 +396,85 @@ $
|
|||
[
|
||||
EnrichJoin(
|
||||
from_aux="config_bgp_peer",
|
||||
on="neighbor",
|
||||
left_on="vrf,neighbor",
|
||||
right_on="vrf,neighbor",
|
||||
take=("remote_as", "activate", "route_map_in", "route_map_out"),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(routes[0]["remote_as"], "65001")
|
||||
self.assertEqual(routes[0]["activate"], "enable")
|
||||
self.assertEqual(routes[0]["route_map_in"], "RM_IN")
|
||||
|
||||
# Same neighbor in two VRFs must not collide
|
||||
routes_b = [
|
||||
{"vrf": "CUST_A", "neighbor": "10.0.0.1", "afi": "vpnv4"},
|
||||
{"vrf": "CUST_B", "neighbor": "10.0.0.1", "afi": "vpnv4"},
|
||||
]
|
||||
intent2 = normalize_config_bgp_peer(
|
||||
raw_text="""
|
||||
!<bgp>
|
||||
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_A in
|
||||
$
|
||||
address-family ipv4 vrf CUST_B
|
||||
neighbor 10.0.0.1 activate
|
||||
neighbor 10.0.0.1 route-map RM_B in
|
||||
$
|
||||
$
|
||||
""",
|
||||
command="show running-config bgp",
|
||||
)
|
||||
apply_enrich_joins(
|
||||
routes_b,
|
||||
{"config_bgp_peer": intent2},
|
||||
[
|
||||
EnrichJoin(
|
||||
from_aux="config_bgp_peer",
|
||||
left_on="vrf,neighbor",
|
||||
right_on="vrf,neighbor",
|
||||
take=("route_map_in",),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(routes_b[0]["route_map_in"], "RM_A")
|
||||
self.assertEqual(routes_b[1]["route_map_in"], "RM_B")
|
||||
|
||||
# Global VPNv4 must not pick ipv4-unicast AF row for same neighbor
|
||||
global_routes = [
|
||||
{"afi": "vpnv4", "vrf": "", "neighbor": "10.0.0.1", "network": "1.0.0.0/24"},
|
||||
]
|
||||
intent3 = normalize_config_bgp_peer(
|
||||
raw_text="""
|
||||
!<bgp>
|
||||
neighbor 10.0.0.1 remote-as 65001
|
||||
address-family ipv4
|
||||
neighbor 10.0.0.1 activate
|
||||
neighbor 10.0.0.1 route-map RM_IPV4 in
|
||||
$
|
||||
address-family vpnv4
|
||||
neighbor 10.0.0.1 activate
|
||||
neighbor 10.0.0.1 route-map RM_VPNV4 in
|
||||
$
|
||||
$
|
||||
""",
|
||||
command="show running-config bgp",
|
||||
)
|
||||
apply_enrich_joins(
|
||||
global_routes,
|
||||
{"config_bgp_peer": intent3},
|
||||
[
|
||||
EnrichJoin(
|
||||
from_aux="config_bgp_peer",
|
||||
left_on="afi,neighbor",
|
||||
right_on="afi,neighbor",
|
||||
take=("route_map_in",),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(global_routes[0]["route_map_in"], "RM_VPNV4")
|
||||
|
||||
ra = resolve_aux_command(
|
||||
AuxCommand(key="config_bgp_peer", profile_id="zte.config_bgp_peer"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue