mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 23:20:51 +08:00
Elegant multi-command collect: slim aux, declarative enrich, CollectSession.
AuxCommand is key+profile_id only; EnrichJoin copies fields after primary parse; ARP no longer hardcodes if_intf join. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1f70f7fc25
commit
17d136cf6b
8 changed files with 600 additions and 227 deletions
135
tests/test_enrich_framework.py
Normal file
135
tests/test_enrich_framework.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""Tests for declarative enrich + slim aux (profile_id only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from netx_api.biz_state.collect_session import (
|
||||
CachedCommand,
|
||||
CollectSession,
|
||||
build_parse_bundle,
|
||||
resolve_aux_command,
|
||||
run_primary_with_bundle,
|
||||
)
|
||||
from netx_api.biz_state.enrich import EnrichJoin, apply_enrich_joins
|
||||
from netx_api.biz_state.parsers import run_parser
|
||||
from netx_api.biz_state.profiles import AuxCommand, get_profile, reload_profiles
|
||||
from netx_api.ntc_parse import _all_index_entries
|
||||
|
||||
|
||||
IF_INTF_SAMPLE = """\
|
||||
!<if-intf>
|
||||
interface cdgei-0/1/0/1.1
|
||||
ip vrf forwarding IuB_UP-evpn
|
||||
$
|
||||
interface cdgei-0/1/1/2.1
|
||||
ip vrf forwarding 400G-1
|
||||
$
|
||||
"""
|
||||
|
||||
ARP_MATCHING = """\
|
||||
IP Hardware Exter Inter Sub
|
||||
Address Age Address Interface VlanID VlanID Interface
|
||||
--------------------------------------------------------------------------------
|
||||
131.1.1.2 03:22:07 0011.2233.4455 cdgei-0/1/0/1.1 N/A N/A cdgei-0/1/0/1.1
|
||||
11.1.1.2 H 00aa.bbcc.ddee cdgei-0/1/1/2.1 N/A N/A N/A
|
||||
10.0.0.1 01:00:00 aabb.ccdd.eeff gei-0/0/0/1 N/A N/A N/A
|
||||
"""
|
||||
|
||||
|
||||
class EnrichJoinTests(unittest.TestCase):
|
||||
def test_apply_enrich(self) -> None:
|
||||
rows = [
|
||||
{"interface": "a", "ip": "1.1.1.1", "vrf": ""},
|
||||
{"interface": "b", "ip": "2.2.2.2", "vrf": ""},
|
||||
]
|
||||
aux = {"if_intf": [{"interface": "a", "vrf": "V1"}]}
|
||||
apply_enrich_joins(
|
||||
rows,
|
||||
aux,
|
||||
[EnrichJoin(from_aux="if_intf", on="interface", take=("vrf",))],
|
||||
)
|
||||
self.assertEqual(rows[0]["vrf"], "V1")
|
||||
self.assertEqual(rows[1]["vrf"], "")
|
||||
|
||||
|
||||
class AuxResolveTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
reload_profiles()
|
||||
|
||||
def test_resolve_from_profile_id(self) -> None:
|
||||
ra = resolve_aux_command(AuxCommand(key="if_intf", profile_id="zte.if_intf"))
|
||||
self.assertEqual(ra.command, "show running-config if-intf")
|
||||
self.assertEqual(ra.parser_id, "if_intf")
|
||||
self.assertIn("zte_zxros_show_running_config_if_intf", ra.rule_keys)
|
||||
|
||||
def test_arp_profile_slim(self) -> None:
|
||||
p = get_profile("zte.arp")
|
||||
assert p is not None
|
||||
self.assertEqual(p.aux_commands[0].key, "if_intf")
|
||||
self.assertEqual(p.aux_commands[0].profile_id, "zte.if_intf")
|
||||
self.assertEqual(len(p.enrich_joins), 1)
|
||||
self.assertEqual(p.enrich_joins[0].take, ("vrf",))
|
||||
|
||||
|
||||
class CollectSessionCacheTests(unittest.TestCase):
|
||||
def test_cache_hit(self) -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
def send(_conn, cmd, read_timeout=0):
|
||||
calls.append(cmd)
|
||||
return "RAW"
|
||||
|
||||
sess = CollectSession(None, send_fn=send, read_timeout=1)
|
||||
e1, hit1 = sess.fetch_and_parse("show running-config if-intf", parser_id="")
|
||||
e2, hit2 = sess.fetch_and_parse("show running-config if-intf", parser_id="")
|
||||
self.assertFalse(hit1)
|
||||
self.assertTrue(hit2)
|
||||
self.assertEqual(calls, ["show running-config if-intf"])
|
||||
self.assertEqual(e2.raw, "RAW")
|
||||
|
||||
|
||||
class ArpEnrichPipelineTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
_all_index_entries.cache_clear()
|
||||
reload_profiles()
|
||||
|
||||
def test_run_primary_with_enrich(self) -> None:
|
||||
if_recs, if_fsm, _ = run_parser(
|
||||
"if_intf",
|
||||
raw_text=IF_INTF_SAMPLE,
|
||||
vendor="zte",
|
||||
device_type="zte_zxros",
|
||||
command="show running-config if-intf",
|
||||
)
|
||||
ra = resolve_aux_command(AuxCommand(key="if_intf", profile_id="zte.if_intf"))
|
||||
bundle = build_parse_bundle(
|
||||
primary_raw=ARP_MATCHING,
|
||||
primary_parser_id="arp",
|
||||
aux_results={
|
||||
"if_intf": CachedCommand(
|
||||
raw=IF_INTF_SAMPLE, fsm_tables=if_fsm, records=if_recs, ok=True
|
||||
)
|
||||
},
|
||||
resolved_aux=[ra],
|
||||
)
|
||||
p = get_profile("zte.arp")
|
||||
assert p is not None
|
||||
records, _tables, _keys = run_primary_with_bundle(
|
||||
"arp",
|
||||
bundle=bundle,
|
||||
vendor="zte",
|
||||
device_type="zte_zxros",
|
||||
command="show arp",
|
||||
enrich_joins=list(p.enrich_joins),
|
||||
)
|
||||
by_ip = {r["ip"]: r for r in records}
|
||||
self.assertEqual(by_ip["131.1.1.2"]["vrf"], "IuB_UP-evpn")
|
||||
self.assertEqual(by_ip["11.1.1.2"]["vrf"], "400G-1")
|
||||
self.assertEqual(by_ip["10.0.0.1"]["vrf"], "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -97,11 +97,18 @@ class ArpMultiCommandTests(unittest.TestCase):
|
|||
assert p is not None
|
||||
self.assertEqual(len(p.aux_commands), 1)
|
||||
self.assertEqual(p.aux_commands[0].key, "if_intf")
|
||||
self.assertEqual(p.aux_commands[0].parser_id, "if_intf")
|
||||
self.assertIsNotNone(get_profile("zte.if_intf"))
|
||||
self.assertEqual(p.aux_commands[0].profile_id, "zte.if_intf")
|
||||
self.assertEqual(get_parser_meta("if_intf")["rule_keys"], ("zte_zxros_show_running_config_if_intf",))
|
||||
|
||||
def test_arp_enriches_vrf_via_aux_records(self) -> None:
|
||||
from netx_api.biz_state.collect_session import (
|
||||
CachedCommand,
|
||||
build_parse_bundle,
|
||||
resolve_aux_command,
|
||||
run_primary_with_bundle,
|
||||
)
|
||||
from netx_api.biz_state.profiles import AuxCommand
|
||||
|
||||
if_recs, if_fsm, _ = run_parser(
|
||||
"if_intf",
|
||||
raw_text=IF_INTF_SAMPLE,
|
||||
|
|
@ -109,20 +116,26 @@ class ArpMultiCommandTests(unittest.TestCase):
|
|||
device_type="zte_zxros",
|
||||
command="show running-config if-intf",
|
||||
)
|
||||
records, tables, keys = run_parser(
|
||||
ra = resolve_aux_command(AuxCommand(key="if_intf", profile_id="zte.if_intf"))
|
||||
bundle = build_parse_bundle(
|
||||
primary_raw=ARP_MATCHING,
|
||||
primary_parser_id="arp",
|
||||
aux_results={
|
||||
"if_intf": CachedCommand(
|
||||
raw=IF_INTF_SAMPLE, fsm_tables=if_fsm, records=if_recs, ok=True
|
||||
)
|
||||
},
|
||||
resolved_aux=[ra],
|
||||
)
|
||||
p = get_profile("zte.arp")
|
||||
assert p is not None
|
||||
records, tables, keys = run_primary_with_bundle(
|
||||
"arp",
|
||||
raw_text=ARP_MATCHING,
|
||||
bundle=bundle,
|
||||
vendor="zte",
|
||||
device_type="zte_zxros",
|
||||
command="show arp | one-line",
|
||||
textfsm_command="show arp",
|
||||
raws={"primary": ARP_MATCHING, "if_intf": IF_INTF_SAMPLE},
|
||||
command_rules={
|
||||
"primary": ["zte_zxros_show_arp"],
|
||||
"if_intf": ["zte_zxros_show_running_config_if_intf"],
|
||||
},
|
||||
aux_records={"if_intf": if_recs},
|
||||
fsm_tables_extra=if_fsm,
|
||||
command="show arp",
|
||||
enrich_joins=list(p.enrich_joins),
|
||||
)
|
||||
self.assertIn("zte_zxros_show_arp", keys)
|
||||
by_ip = {r["ip"]: r for r in records}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue