Remove fake VRF Route Summary collect item.

Drop the route-summary profiles/parser/persist path; keep VRF list discover for bindings and strip it from default cutover templates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-20 09:58:26 +08:00
parent 1642a0adec
commit a769e603b7
10 changed files with 18 additions and 320 deletions

View file

@ -24,7 +24,6 @@ from ..models import (
BizStateTask,
BizStateTaskItem,
BizStateTaskItemBinding,
BizStateVrfRouteSummary,
)
from ..ne_netmiko import disable_target_paging, send_show_command
from ..ne_session_factory import close_netmiko_connection, open_netmiko_connection
@ -116,45 +115,6 @@ def _persist_lldp_rows(
return n
def _persist_vrf_route_summary(
db,
*,
batch: BizStateBatch,
cmd_row: BizStateBatchCommand,
records: list[dict[str, Any]],
) -> int:
n = 0
seen: set[tuple[str, str]] = set()
for rec in records:
vrf = str(rec.get("vrf") or "").strip()[:128]
source = str(rec.get("source") or "").strip()[:64]
if not vrf and not source:
continue
key = (vrf, source)
if key in seen:
continue
seen.add(key)
try:
networks = int(rec.get("networks") or 0)
except (TypeError, ValueError):
networks = 0
db.add(
BizStateVrfRouteSummary(
id=uuid4().hex,
batch_id=batch.id,
batch_command_id=cmd_row.id,
task_id=batch.task_id,
ne_id=batch.ne_id,
vrf=vrf,
source=source,
networks=networks,
collected_at=_utcnow(),
)
)
n += 1
return n
_GENERIC_METRICS = {
"isis_adjacency",
"interface_brief",
@ -636,10 +596,6 @@ def _run_collect_session(
n = _persist_lldp_rows(
sdb, batch=batch_row, cmd_row=cmd_row, records=records
)
elif hit.profile.metric_id == "vrf_route_summary":
n = _persist_vrf_route_summary(
sdb, batch=batch_row, cmd_row=cmd_row, records=records
)
elif hit.profile.metric_id in _GENERIC_METRICS:
n = _persist_metric_rows(
sdb,

View file

@ -371,21 +371,6 @@ def _default_lldp_sheet() -> dict[str, Any]:
)
def _default_vrf_sheet() -> dict[str, Any]:
fields = metric_field_map().get("vrf_route_summary") or []
keys = [f.name for f in fields if f.is_key] or ["vrf", "source"]
ifaces = [f.name for f in fields if f.is_interface]
compare = [f.name for f in fields if not f.is_key and f.role in ("state", "meta", "identity")]
if not compare:
compare = [n for n in ("networks",) if n not in keys]
return _sheet_def(
metric_id="vrf_route_summary",
key_fields=keys,
iface_fields=ifaces,
compare_fields=compare,
)
def _default_sheet_for_metric(metric_id: str, *, compare_roles: tuple[str, ...] = ("state",)) -> dict[str, Any]:
fields = metric_field_map().get(metric_id) or []
keys = [f.name for f in fields if f.is_key]
@ -724,20 +709,22 @@ def ensure_default_cutover_template(db: Session) -> BizCompareTemplate:
.one_or_none()
)
if row:
# Upgrade legacy single-sheet cutover if needed
sheets = template_metrics(row)
if len(sheets) < 2:
_apply_sheets_to_row(row, [_default_lldp_sheet(), _default_vrf_sheet()])
row.note = "Built-in multi-metric cutover template (LLDP + VRF)"
cleaned = [s for s in sheets if str(s.get("metric_id") or "") != "vrf_route_summary"]
if not cleaned:
cleaned = [_default_lldp_sheet()]
if cleaned != sheets:
_apply_sheets_to_row(row, cleaned)
row.note = "Built-in cutover template (LLDP)"
row.updated_at = _utcnow()
db.commit()
db.refresh(row)
return row
sheets = [_default_lldp_sheet(), _default_vrf_sheet()]
sheets = [_default_lldp_sheet()]
row = BizCompareTemplate(
id=uuid4().hex,
name="Cutover default",
note="Built-in multi-metric cutover template (LLDP + VRF)",
note="Built-in cutover template (LLDP)",
created_at=_utcnow(),
updated_at=_utcnow(),
)
@ -775,33 +762,6 @@ def ensure_default_lldp_template(db: Session) -> BizCompareTemplate:
return row
def ensure_default_vrf_template(db: Session) -> BizCompareTemplate:
row = (
db.query(BizCompareTemplate)
.filter(BizCompareTemplate.name == "VRF route summary default")
.one_or_none()
)
if row:
if not template_metrics(row):
_apply_sheets_to_row(row, [_default_vrf_sheet()])
row.updated_at = _utcnow()
db.commit()
db.refresh(row)
return row
row = BizCompareTemplate(
id=uuid4().hex,
name="VRF route summary default",
note="Built-in template for per-VRF route summary cutover compare",
created_at=_utcnow(),
updated_at=_utcnow(),
)
_apply_sheets_to_row(row, [_default_vrf_sheet()])
db.add(row)
db.commit()
db.refresh(row)
return row
def ensure_default_zte_status_template(db: Session) -> BizCompareTemplate:
name = "ZTE status default"
row = db.query(BizCompareTemplate).filter(BizCompareTemplate.name == name).one_or_none()
@ -1202,30 +1162,6 @@ def _load_metric_rows(db: Session, *, batch_id: str, metric_id: str) -> list[dic
}
for n in rows
]
if metric_id == "vrf_route_summary":
from ..models import BizStateVrfRouteSummary
rows = (
db.query(BizStateVrfRouteSummary)
.filter(BizStateVrfRouteSummary.batch_id == batch_id)
.all()
)
return [
{
"vrf": r.vrf,
"source": r.source,
"networks": r.networks,
"_netx": {
"batch_id": batch_id,
"batch_command_id": r.batch_command_id or "",
"task_id": r.task_id or "",
"ne_id": r.ne_id or "",
"collected_at": r.collected_at.isoformat() + "Z" if r.collected_at else None,
"row_id": r.id,
},
}
for r in rows
]
# Generic tabular metrics (ISIS / interface / ARP / ND6 / BGP …)
from ..models import BizStateMetricRow

View file

@ -41,7 +41,6 @@ from ...ntc_parse import apply_rules, resolve_cli_platform, rules_for_command
from .cisco import PARSERS as _CISCO_PARSERS
from .common.lldp_neighbors import normalize_lldp_neighbors
from .common.vrf_list import normalize_vrf_list
from .common.vrf_route_summary import normalize_vrf_route_summary
from .ericsson import PARSERS as _ERICSSON_PARSERS
from .h3c import PARSERS as _H3C_PARSERS
from .huawei import PARSERS as _HUAWEI_PARSERS
@ -64,7 +63,6 @@ _VENDOR_PARSERS: list[dict[str, NormalizeFn]] = [
_REGISTRY: dict[str, NormalizeFn] = {
"lldp_neighbors": normalize_lldp_neighbors,
"vrf_list": normalize_vrf_list,
"vrf_route_summary": normalize_vrf_route_summary,
}
for _pack in _VENDOR_PARSERS:
_REGISTRY.update(_pack)

View file

@ -1,60 +0,0 @@
"""Per-VRF route summary counts (multi-vendor line scrape)."""
from __future__ import annotations
import re
from typing import Any
_SOURCE_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
("connected", re.compile(r"(?i)^\s*connected\s+(\d+)\s*$")),
("static", re.compile(r"(?i)^\s*static\s+(\d+)\s*$")),
("local", re.compile(r"(?i)^\s*local\s+(\d+)\s*$")),
("ospf", re.compile(r"(?i)^\s*ospf(?:\s+\S+)?\s+(\d+)\s*$")),
("isis", re.compile(r"(?i)^\s*isis(?:\s+\S+)?\s+(\d+)\s*$")),
("bgp", re.compile(r"(?i)^\s*bgp(?:\s+\S+)?\s+(\d+)\s*$")),
("rip", re.compile(r"(?i)^\s*rip(?:\s+\S+)?\s+(\d+)\s*$")),
("total", re.compile(r"(?i)^\s*(?:total|totals?)\s+(?:routes?\s+)?(\d+)\s*$")),
]
RULE_KEYS: tuple[str, ...] = ()
def normalize_vrf_route_summary(
*,
raw_text: str,
fsm_tables=None,
vendor: str = "",
device_type: str = "",
command: str = "",
params: dict[str, str] | None = None,
) -> list[dict[str, Any]]:
_ = (vendor, device_type, command, fsm_tables)
vrf = str((params or {}).get("vrf") or (params or {}).get("vrf_name") or "").strip()
text = str(raw_text or "")
found: dict[str, int] = {}
for src, pat in _SOURCE_PATTERNS:
for line in text.splitlines():
m = pat.match(line.strip())
if m:
found[src] = int(m.group(1))
break
if not found:
n = 0
for line in text.splitlines():
s = line.strip()
if not s or s.startswith("-") or s.lower().startswith(("code", "codes", "gateway", "routing table")):
continue
if re.match(r"^[A-Z*+]>?\s+\S+", s) or re.match(r"^\S+\s+\d+\.\d+\.\d+\.\d+", s):
n += 1
if n:
found["routes"] = n
rows: list[dict[str, Any]] = []
for src, count in found.items():
rows.append({"vrf": vrf[:128], "source": src[:64], "networks": count})
if vrf and not rows:
rows.append({"vrf": vrf[:128], "source": "empty", "networks": 0})
return rows
normalize_vrf_route_summary.RULE_KEYS = RULE_KEYS

View file

@ -1,8 +1,7 @@
"""Compat shim — prefer ``parsers.common.vrf_list`` / ``vrf_route_summary``."""
"""Compat shim — prefer ``parsers.common.vrf_list``."""
from __future__ import annotations
from .common.vrf_list import normalize_vrf_list
from .common.vrf_route_summary import normalize_vrf_route_summary
__all__ = ["normalize_vrf_list", "normalize_vrf_route_summary"]
__all__ = ["normalize_vrf_list"]

View file

@ -133,15 +133,8 @@ def _lldp_profiles() -> list[ParseProfile]:
return out
_VRF_ROUTE_FIELDS: list[FieldDef] = [
FieldDef("vrf", length=128, indexed=True, is_key=True, display_name="VRF", from_command_param=True),
FieldDef("source", length=64, indexed=True, is_key=True, display_name="路由来源"),
FieldDef("networks", dtype="int", role="state", display_name="路由条数"),
]
def _vrf_profiles() -> list[ParseProfile]:
"""Discover VRF list + parameterized route-summary collect (Phase3)."""
"""Discover VRF list for parameterized collect bindings (e.g. BGP VRF)."""
discover_cmds: dict[str, tuple[str, str, str]] = {
# vendor_key: (command, match, textfsm_command)
"cisco": ("show vrf", r"(?i)^\s*show\s+vrf\s*$", "show vrf"),
@ -161,24 +154,6 @@ def _vrf_profiles() -> list[ParseProfile]:
"show ip vrf",
),
}
collect_cmds: dict[str, tuple[str, str]] = {
"cisco": (
"show ip route vrf <vrf> summary",
r"(?i)^\s*show\s+ip\s+route\s+vrf\s+(?P<vrf>\S+)\s+summary\s*$",
),
"huawei": (
"display ip routing-table vpn-instance <vrf> statistics",
r"(?i)^\s*display\s+ip\s+routing-table\s+vpn-instance\s+(?P<vrf>\S+)\s+statistics\s*$",
),
"h3c": (
"display ip routing-table vpn-instance <vrf> statistics",
r"(?i)^\s*display\s+ip\s+routing-table\s+vpn-instance\s+(?P<vrf>\S+)\s+statistics\s*$",
),
"zte": (
"show ip route vrf <vrf> summary",
r"(?i)^\s*show\s+ip\s+route\s+vrf\s+(?P<vrf>\S+)\s+summary\s*$",
),
}
out: list[ParseProfile] = []
order = 200
for key, (cmd, match, fsm_cmd) in discover_cmds.items():
@ -207,40 +182,6 @@ def _vrf_profiles() -> list[ParseProfile]:
)
)
order += 5
order = 220
for key, (tmpl, match) in collect_cmds.items():
disc_id = f"{key}.vrf_list"
out.append(
ParseProfile(
profile_id=f"{key}.route_vrf_summary",
vendor_key=key,
metric_id="vrf_route_summary",
parser_id="vrf_route_summary",
title="VRF Route Summary",
command_template=tmpl,
match=match,
textfsm_command="",
description="Per-VRF route source counts (discover VRF → select bindings → collect).",
placeholders=[
PlaceholderDef(
name="vrf",
schema_field="vrf",
required=True,
bind_mode="discover_select",
discover_profile_id=disc_id,
discover_value_field="vrf_name",
discover_label_field="vrf_name",
)
],
fields=list(_VRF_ROUTE_FIELDS),
tags=["vrf", "route", "l3"],
sort_order=order,
enabled=True,
kind="collect",
)
)
order += 5
return out

View file

@ -22,7 +22,6 @@ from ..models import (
BizStateTask,
BizStateTaskItem,
BizStateTaskItemBinding,
BizStateVrfRouteSummary,
ManagedNE,
)
from ..timeutil import utcnow_naive
@ -557,13 +556,6 @@ def get_batch(db: Session, batch_id: str) -> dict[str, Any]:
.limit(5000)
.all()
)
vrf_rows = (
db.query(BizStateVrfRouteSummary)
.filter(BizStateVrfRouteSummary.batch_id == batch_id)
.order_by(BizStateVrfRouteSummary.vrf.asc(), BizStateVrfRouteSummary.source.asc())
.limit(5000)
.all()
)
metric_rows = (
db.query(BizStateMetricRow)
.filter(BizStateMetricRow.batch_id == batch_id)
@ -620,9 +612,6 @@ def get_batch(db: Session, batch_id: str) -> dict[str, Any]:
}
for n in neighbors
],
"vrf_route_summary": [
{"vrf": r.vrf, "source": r.source, "networks": r.networks} for r in vrf_rows
],
"metrics": metrics_by_id,
}
@ -707,13 +696,6 @@ def export_batch_zip(db: Session, batch_id: str) -> bytes:
)
zf.writestr("tables/lldp_neighbor.csv", "\n".join(csv_lines) + "\n")
vrf_csv = ["vrf,source,networks"]
for r in detail.get("vrf_route_summary") or []:
vrf_csv.append(
",".join([_csv(r["vrf"]), _csv(r["source"]), _csv(str(r["networks"]))])
)
zf.writestr("tables/vrf_route_summary.csv", "\n".join(vrf_csv) + "\n")
for mid, rows in sorted((detail.get("metrics") or {}).items()):
if not rows:
continue

View file

@ -1,11 +1,10 @@
"""Unit tests for Phase3 VRF binding expand / route summary parse."""
"""Unit tests for VRF list discover profiles."""
from __future__ import annotations
import unittest
from netx_api.biz_state.command_match import expand_from_bindings, match_command, preview_task_item
from netx_api.biz_state.parsers.vrf import normalize_vrf_list, normalize_vrf_route_summary
from netx_api.biz_state.parsers.vrf import normalize_vrf_list
from netx_api.biz_state.profiles import get_profile, profiles_for_vendor, reload_profiles
@ -13,60 +12,12 @@ class BizStateVrfTests(unittest.TestCase):
def setUp(self) -> None:
reload_profiles()
def test_vrf_profiles_registered(self) -> None:
def test_vrf_list_profiles_registered(self) -> None:
cisco = profiles_for_vendor("cisco")
self.assertTrue(any(p.profile_id == "cisco.vrf_list" and p.kind == "discover" for p in cisco))
self.assertTrue(any(p.profile_id == "cisco.route_vrf_summary" for p in cisco))
def test_expand_multi_vrf_bindings(self) -> None:
p = get_profile("cisco.route_vrf_summary")
assert p is not None
pairs = expand_from_bindings(
profile=p,
bindings=[
{"placeholder": "vrf", "value": "CUST_A"},
{"placeholder": "vrf", "value": "CUST_B"},
],
)
self.assertEqual(len(pairs), 2)
self.assertEqual(pairs[0][0], "show ip route vrf CUST_A summary")
self.assertEqual(pairs[1][0], "show ip route vrf CUST_B summary")
hit = match_command(vendor_key="cisco", command=pairs[0][0])
self.assertIsNotNone(hit)
assert hit is not None
self.assertEqual(hit.params.get("vrf"), "CUST_A")
def test_expand_requires_bindings(self) -> None:
p = get_profile("zte.route_vrf_summary")
assert p is not None
with self.assertRaises(ValueError):
expand_from_bindings(profile=p, bindings=[])
def test_preview_with_bindings(self) -> None:
prev = preview_task_item(
vendor_key="huawei",
profile_id="huawei.route_vrf_summary",
bindings=[{"vrf": "VPN1"}],
)
self.assertTrue(prev["ok"])
self.assertEqual(len(prev["commands"]), 1)
self.assertIn("VPN1", prev["commands"][0]["command"])
def test_route_summary_parser(self) -> None:
raw = """
IP routing table name is CUST_A
Route Source Networks
connected 3
static 1
bgp 65001 42
Total 46
"""
rows = normalize_vrf_route_summary(raw_text=raw, params={"vrf": "CUST_A"})
by_src = {r["source"]: r["networks"] for r in rows}
self.assertEqual(by_src["connected"], 3)
self.assertEqual(by_src["bgp"], 42)
self.assertEqual(by_src["total"], 46)
self.assertTrue(all(r["vrf"] == "CUST_A" for r in rows))
self.assertFalse(any(p.metric_id == "vrf_route_summary" for p in cisco))
self.assertIsNone(get_profile("cisco.route_vrf_summary"))
self.assertIsNone(get_profile("zte.route_vrf_summary"))
def test_vrf_list_fallback(self) -> None:
raw = "CUST_A 100:1 ipv4\nCUST_B 100:2 ipv4\n"

View file

@ -133,7 +133,7 @@ src/
- Phase1:LLDP 邻居快照;建任务默认启用对应厂商 LLDP profile;支持自定义只采不解析行
- Phase2:比对模板(多监控表 / Key·接口·比对字段;空比对=仅条目)、端口映射、CompareJob 列表与详情、先选批次再按 Excel sheet 看 pre/post、`/network/tasks/biz-compare`
- API 补充:`GET /v1/biz-state/compare/metrics`;模板 `metrics[]` 多表结构
- Phase3:VRF 发现→人选关联→带参采集(`*.route_vrf_summary`);`PUT …/items/{id}/bindings`
- Phase3:VRF 发现(`*.vrf_list`)供带参采集绑定;`PUT …/items/{id}/bindings`
- 调度:`NETX_BIZ_STATE_SCHEDULER_ENABLED`(默认开),tick `NETX_BIZ_STATE_SCHEDULER_TICK_SEC`
- 前端:`/network/tasks/biz-state`(勾选监控项 / 发现 VRF / 批次 / 导出 zip)

View file

@ -154,7 +154,6 @@ function columnsFromRows(rows: Record<string, unknown>[]): SheetCol[] {
function metricSheetTitle(metricId: string, t: (k: string) => string): string {
const map: Record<string, string> = {
lldp_neighbor: t("bizState.sheetLldp"),
vrf_route_summary: t("bizState.sheetVrfRoute"),
isis_adjacency: t("bizState.sheetIsis"),
interface_brief: t("bizState.sheetIfaceBrief"),
arp: t("bizState.sheetArp"),
@ -174,9 +173,6 @@ function rowsForMetric(batch: any, metricId: string): Record<string, unknown>[]
if (mid === "lldp_neighbor") {
return ((batch?.lldp_neighbors || []) as Record<string, unknown>[]) || [];
}
if (mid === "vrf_route_summary") {
return ((batch?.vrf_route_summary || []) as Record<string, unknown>[]) || [];
}
return Array.isArray(fromGeneric) ? (fromGeneric as Record<string, unknown>[]) : [];
}
@ -215,7 +211,6 @@ function buildBatchSheets(batch: any, t: (k: string) => string): SheetDef[] {
for (const c of cmds) pushMid(String(c.metric_id || ""));
for (const mid of Object.keys(batch?.metrics || {})) pushMid(mid);
if ((batch?.lldp_neighbors || []).length) pushMid("lldp_neighbor");
if ((batch?.vrf_route_summary || []).length) pushMid("vrf_route_summary");
for (const mid of metricOrder) {
const rows = rowsForMetric(batch, mid);