mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Fix vpnv6 ECMP under-parse and persist full CLI line counts.
RR vpnv6 wrap lines with next-hop+metrics were collapsing ECMP legs; store raw_line_count/declared_total before DB truncate so collect stats stay accurate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
86fcb98218
commit
ac53af4a5d
16 changed files with 531 additions and 35 deletions
|
|
@ -123,6 +123,64 @@ class BatchWorkbookApiTests(unittest.TestCase):
|
|||
self.assertEqual(out["message"], "stopped")
|
||||
self.assertEqual(out["commands"][0]["raw_line_count"], 3)
|
||||
self.assertEqual(out["commands"][0]["row_count"], 3)
|
||||
self.assertEqual(out["commands"][0]["declared_total"], 0)
|
||||
|
||||
def test_get_batch_prefers_stored_raw_line_count(self) -> None:
|
||||
"""DB raw_text may be truncated; API must use persisted full-file line count."""
|
||||
batch = BizStateBatch(
|
||||
id="b1",
|
||||
task_id="t1",
|
||||
status="ok",
|
||||
command_count=1,
|
||||
row_count=100,
|
||||
)
|
||||
cmd = BizStateBatchCommand(
|
||||
id="c1",
|
||||
batch_id="b1",
|
||||
profile_id="zte.bgp_vpnv4_neighbor_in",
|
||||
parser_id="bgp_route",
|
||||
metric_id="bgp_route",
|
||||
raw_command="show bgp vpnv4 unicast neighbor in 1.1.1.1",
|
||||
parse_status="ok",
|
||||
row_count=100,
|
||||
raw_text="a\nb\nc", # truncated stub (3 lines)
|
||||
raw_line_count=119303,
|
||||
declared_total=1669101,
|
||||
message="declared=1669101;parsed=100",
|
||||
)
|
||||
db = MagicMock()
|
||||
db.get.side_effect = lambda model, pk: batch if pk == "b1" else None
|
||||
cmd_q = MagicMock()
|
||||
cmd_q.filter.return_value.order_by.return_value.all.return_value = [cmd]
|
||||
metric_count_q = MagicMock()
|
||||
metric_count_q.filter.return_value.group_by.return_value.all.return_value = [
|
||||
("bgp_route", 100)
|
||||
]
|
||||
lldp_count_q = MagicMock()
|
||||
lldp_count_q.filter.return_value.scalar.return_value = 0
|
||||
|
||||
def query(*_args, **_kwargs):
|
||||
n = query.n
|
||||
query.n += 1
|
||||
if n == 0:
|
||||
return cmd_q
|
||||
if n == 1:
|
||||
return metric_count_q
|
||||
return lldp_count_q
|
||||
|
||||
query.n = 0
|
||||
db.query.side_effect = query
|
||||
|
||||
with patch(
|
||||
"netx_api.biz_state.service.batch_protect_info",
|
||||
return_value={"protected": False, "reasons": []},
|
||||
):
|
||||
out = get_batch(db, "b1")
|
||||
|
||||
self.assertEqual(out["commands"][0]["raw_line_count"], 119303)
|
||||
self.assertEqual(out["commands"][0]["declared_total"], 1669101)
|
||||
self.assertEqual(out["sheets"][0]["commands"][0]["raw_line_count"], 119303)
|
||||
self.assertEqual(out["sheets"][0]["commands"][0]["declared_total"], 1669101)
|
||||
|
||||
def test_get_batch_command_and_raw_download(self) -> None:
|
||||
from netx_api.biz_state.service import get_batch_command
|
||||
|
|
|
|||
|
|
@ -79,6 +79,100 @@ class BizStateCollectFinalizeTests(unittest.TestCase):
|
|||
self.assertIn("biz_state_heavy_timeout", batch.message or "")
|
||||
self.assertIsNotNone(batch.ended_at)
|
||||
|
||||
def test_finalize_partial_message_from_failed_cmds_when_lane_errors_empty(self) -> None:
|
||||
from netx_api.models import BizStateBatchCommand
|
||||
|
||||
self.db.add(
|
||||
BizStateBatchCommand(
|
||||
id="c-ok",
|
||||
batch_id="b-finalize",
|
||||
profile_id="zte.arp",
|
||||
parser_id="arp",
|
||||
metric_id="arp",
|
||||
raw_command="show arp",
|
||||
parse_status="ok",
|
||||
row_count=10,
|
||||
)
|
||||
)
|
||||
self.db.add(
|
||||
BizStateBatchCommand(
|
||||
id="c-aux-fail",
|
||||
batch_id="b-finalize",
|
||||
profile_id="zte.if_intf",
|
||||
parser_id="if_intf",
|
||||
metric_id="if_intf",
|
||||
raw_command="show interface brief",
|
||||
parse_status="aux_failed",
|
||||
message="aux_for=x;parse boom",
|
||||
row_count=0,
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
with patch.object(runner, "SessionLocal", self.Session):
|
||||
with patch(
|
||||
"netx_api.biz_state.compare_service.schedule_auto_compare_for_task",
|
||||
):
|
||||
status = runner._finalize_batch_status(
|
||||
batch_id="b-finalize",
|
||||
task_id="t-finalize",
|
||||
cmd_count=2,
|
||||
total_rows=10,
|
||||
any_fail=True,
|
||||
any_ok=True,
|
||||
lane_errors=[],
|
||||
)
|
||||
self.assertEqual(status, "partial")
|
||||
self.db.expire_all()
|
||||
batch = self.db.get(BizStateBatch, "b-finalize")
|
||||
assert batch is not None
|
||||
self.assertEqual(batch.status, "partial")
|
||||
self.assertTrue(str(batch.message or "").strip())
|
||||
self.assertIn("aux", (batch.message or "").lower())
|
||||
|
||||
def test_finalize_partial_when_skipped_bindings_with_success(self) -> None:
|
||||
from netx_api.models import BizStateBatchCommand
|
||||
|
||||
self.db.add(
|
||||
BizStateBatchCommand(
|
||||
id="c-ok2",
|
||||
batch_id="b-finalize",
|
||||
profile_id="zte.arp",
|
||||
raw_command="show arp",
|
||||
parse_status="ok",
|
||||
row_count=1,
|
||||
)
|
||||
)
|
||||
self.db.add(
|
||||
BizStateBatchCommand(
|
||||
id="c-skip",
|
||||
batch_id="b-finalize",
|
||||
profile_id="zte.bgp_vpnv4_neighbor_in",
|
||||
raw_command="show bgp vpnv4 ...",
|
||||
parse_status="skipped",
|
||||
message="profile requires parameter bindings",
|
||||
row_count=0,
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
with patch.object(runner, "SessionLocal", self.Session):
|
||||
with patch(
|
||||
"netx_api.biz_state.compare_service.schedule_auto_compare_for_task",
|
||||
):
|
||||
status = runner._finalize_batch_status(
|
||||
batch_id="b-finalize",
|
||||
task_id="t-finalize",
|
||||
cmd_count=2,
|
||||
total_rows=1,
|
||||
any_fail=False,
|
||||
any_ok=True,
|
||||
lane_errors=[],
|
||||
)
|
||||
self.assertEqual(status, "partial")
|
||||
self.db.expire_all()
|
||||
batch = self.db.get(BizStateBatch, "b-finalize")
|
||||
assert batch is not None
|
||||
self.assertIn("skipped", (batch.message or "").lower())
|
||||
|
||||
def test_finalize_retries_once_on_operational_error(self) -> None:
|
||||
calls = {"n": 0}
|
||||
real_session = self.Session
|
||||
|
|
@ -93,6 +187,12 @@ class BizStateCollectFinalizeTests(unittest.TestCase):
|
|||
def get(self, *args, **kwargs):
|
||||
return self._inner.get(*args, **kwargs)
|
||||
|
||||
def query(self, *args, **kwargs):
|
||||
return self._inner.query(*args, **kwargs)
|
||||
|
||||
def add(self, *args, **kwargs):
|
||||
return self._inner.add(*args, **kwargs)
|
||||
|
||||
def commit(self) -> None:
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
|
|
|
|||
|
|
@ -1198,5 +1198,42 @@ Total number of routes: 2
|
|||
by_key = {(r["rd"], r["network"], r["next_hop"]) for r in routes}
|
||||
self.assertEqual(len(by_key), 2)
|
||||
|
||||
def test_bgp_route_vpnv6_ecmp_nh_with_metrics_same_line(self) -> None:
|
||||
"""RR vpnv6 wrap: indented 'NH LocPrf Path' must keep both ECMP legs."""
|
||||
raw = """
|
||||
Routes Learned From This Neighbor:
|
||||
Status codes: * valid, i - internal
|
||||
Total number of routes: 4
|
||||
Dest Next Hop Metric LocPrf InTag Path
|
||||
Route Distinguisher:10.0.0.1:100
|
||||
* i 2407:1::/48
|
||||
10.1.1.1 100 0 ?
|
||||
* i 2407:1::/48
|
||||
10.1.1.2 100 0 ?
|
||||
* i 2407:2::/48
|
||||
10.1.1.1 100 0 ?
|
||||
* i 2407:2::/48
|
||||
10.1.1.2 100 0 ?
|
||||
"""
|
||||
routes = normalize_bgp_route(
|
||||
raw_text=raw,
|
||||
command="show bgp vpnv6 unicast neighbor in 24.11.0.8 | one-line",
|
||||
vendor="ZTE",
|
||||
device_type="zte_zxros",
|
||||
params={"neighbor": "24.11.0.8", "direction": "in", "afi": "vpnv6"},
|
||||
)
|
||||
self.assertEqual(len(routes), 4, "empty next_hop must not collapse ECMP")
|
||||
self.assertEqual(
|
||||
{(r["network"], r["next_hop"]) for r in routes},
|
||||
{
|
||||
("2407:1::/48", "10.1.1.1"),
|
||||
("2407:1::/48", "10.1.1.2"),
|
||||
("2407:2::/48", "10.1.1.1"),
|
||||
("2407:2::/48", "10.1.1.2"),
|
||||
},
|
||||
)
|
||||
self.assertTrue(all(r.get("status_codes") == "*i" for r in routes))
|
||||
self.assertTrue(all(r.get("rd") == "10.0.0.1:100" for r in routes))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue