diff --git a/netx_api/biz_state/collect_runner.py b/netx_api/biz_state/collect_runner.py index e4bd1e7..32b0d13 100644 --- a/netx_api/biz_state/collect_runner.py +++ b/netx_api/biz_state/collect_runner.py @@ -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, diff --git a/netx_api/biz_state/compare_service.py b/netx_api/biz_state/compare_service.py index 726cc1a..79f7a03 100644 --- a/netx_api/biz_state/compare_service.py +++ b/netx_api/biz_state/compare_service.py @@ -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 diff --git a/netx_api/biz_state/parsers/__init__.py b/netx_api/biz_state/parsers/__init__.py index 7ee1e41..939b5ea 100644 --- a/netx_api/biz_state/parsers/__init__.py +++ b/netx_api/biz_state/parsers/__init__.py @@ -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) diff --git a/netx_api/biz_state/parsers/common/vrf_route_summary.py b/netx_api/biz_state/parsers/common/vrf_route_summary.py deleted file mode 100644 index e885d75..0000000 --- a/netx_api/biz_state/parsers/common/vrf_route_summary.py +++ /dev/null @@ -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 diff --git a/netx_api/biz_state/parsers/vrf.py b/netx_api/biz_state/parsers/vrf.py index b0add91..ed4a401 100644 --- a/netx_api/biz_state/parsers/vrf.py +++ b/netx_api/biz_state/parsers/vrf.py @@ -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"] diff --git a/netx_api/biz_state/profiles.py b/netx_api/biz_state/profiles.py index d16a335..b9f5bfe 100644 --- a/netx_api/biz_state/profiles.py +++ b/netx_api/biz_state/profiles.py @@ -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 summary", - r"(?i)^\s*show\s+ip\s+route\s+vrf\s+(?P\S+)\s+summary\s*$", - ), - "huawei": ( - "display ip routing-table vpn-instance statistics", - r"(?i)^\s*display\s+ip\s+routing-table\s+vpn-instance\s+(?P\S+)\s+statistics\s*$", - ), - "h3c": ( - "display ip routing-table vpn-instance statistics", - r"(?i)^\s*display\s+ip\s+routing-table\s+vpn-instance\s+(?P\S+)\s+statistics\s*$", - ), - "zte": ( - "show ip route vrf summary", - r"(?i)^\s*show\s+ip\s+route\s+vrf\s+(?P\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 diff --git a/netx_api/biz_state/service.py b/netx_api/biz_state/service.py index 06e269b..b20600b 100644 --- a/netx_api/biz_state/service.py +++ b/netx_api/biz_state/service.py @@ -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 diff --git a/tests/test_biz_state_vrf.py b/tests/test_biz_state_vrf.py index f7e8073..a8c5bb2 100644 --- a/tests/test_biz_state_vrf.py +++ b/tests/test_biz_state_vrf.py @@ -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" diff --git a/web/WEB.md b/web/WEB.md index 0c11b81..96fc016 100644 --- a/web/WEB.md +++ b/web/WEB.md @@ -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) diff --git a/web/src/pages/network/BizStatePage.tsx b/web/src/pages/network/BizStatePage.tsx index 2ef2369..d318d2e 100644 --- a/web/src/pages/network/BizStatePage.tsx +++ b/web/src/pages/network/BizStatePage.tsx @@ -154,7 +154,6 @@ function columnsFromRows(rows: Record[]): SheetCol[] { function metricSheetTitle(metricId: string, t: (k: string) => string): string { const map: Record = { 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[] if (mid === "lldp_neighbor") { return ((batch?.lldp_neighbors || []) as Record[]) || []; } - if (mid === "vrf_route_summary") { - return ((batch?.vrf_route_summary || []) as Record[]) || []; - } return Array.isArray(fromGeneric) ? (fromGeneric as Record[]) : []; } @@ -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);